Vi siete mai chiesti il motivo per il quale, ad esempio, quando create il wizard di una Windows Application, la funzione main viene marcata con l'attributo [STAThread]?
namespace WindowsFormsApplication2
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
Troverete la risposta alle vostre domande consultando la seguente
pagina del SDK .NET di ArcGIS Desktop/Engine.
Se vi può essere d'aiuto, qui sotto trovate una mia libera traduzione di questo documento. Spero anche che questo post possa favorire uno scambio di opinioni su un argomento certamente ostico.
Il multithreading consente ad una applicazione di effettuare più operazioni alla volta all'interno di singolo processo. In questo articolo vediamo cosa significa il multithreading nel contesto degli arcobjects e quali sono le regole che devono essere seguite per integrare il threading nelle applicazione arcobjects.
Il multithreading è normalmente utilizzato per migliorare la reattività delle applicazioni. Questa reattività può essere il risultato di reali prestazioni migliorate o la percezione di prestazioni migliorate. Usando multipli threads di esecuzione nel codice, puoi separare l'elaborazione dei dati e operazioni di input/output dalla gestione dell'interfaccia grafica (UI). Questo eviterà che lunghe operazioni di elaborazione dei dati riducano la reattività della tua UI.
I vantaggi delle prestazioni del multithreading hanno come aspetto negativo il fatto che si incrementa la complessità nel disegno e nel mantenimento del codice. I threads di una applicazione condividono lo stesso spazio di memoria, così devi garantire che l'accesso a strutture condivise sia sincronizzato per prevenire che l'applicazione entri in uno stato invalido, si blocchi o si chiuda improvvisamente. Questa sincronizzazione è spesso chiamata controllo concorrente.
Il controllo concorrente può essere ottenuto a due livelli: a livello di oggetto o di applicazione. Può essere ottenuto a livello di oggetto quando l'oggetto condiviso è
thread safe, cioè l'oggetto forza tutti i threads che provano ad accedere ad esso, ad attendere fino a che il corrente thread abbia finito di modificare lo stato di quest'ultimo.
Il controllo concorrente può essere fatto a livello di applicazione acquisendo un esclusivo lock sull'oggetto condiviso, consentendo ad un thread alla volta di modificare lo stato dell'oggetto. Occorre fare attenzione ad un sovrautilizzo dei lock per proteggere i dati poichè può anche decrementare le prestazioni. Bilanciare le prestazioni e la protezione richiede un'attenta analisi della struttura dei dati ed un utilizzo di pattern adeguati per i tuoi threads.
Ci sono due cose da considerare quando sviluppi applicazioni multithreaded: thread safety e scalabilità. E' importante che tutti gli oggetti siano
thread safe, ma avere oggetti
thread safe non significa automaticamente che l'applicazione sia multithreaded o che il risultato dell'applicazione garantisca prestazioni migliori.
Il .NET Framework consente facilmente di generare threads nella tua applicazione; comunque, scrivere codice multithreaded ArcObjects dovrebbe essere fatto attentamente. La sottostante architettura degli ArcObjects è COM (Component Object Model). Per questa ragione, scrivere applicazioni multithreading ArcObjects richiede la conoscenza del NET. multithreading e del modello threading di COM.
Il multithreading non sempre farà in modo che il tuo codice sia più veloce; in molti casi aggiunge extra overhead e complessità che eventualmente potranno ridurre la velocità di esecuzione del tuo codice. Il multithreading dovrebbe solo essere usato quando la complessità aggiunta vale il costo. Una regola generale è che un task è adatto al multithreading se può essere diviso in differenti task indipendenti.
Tutti i componenti ArcObjects sono marcati come
single threaded apartment (STA). Lo STA è un contenitore logico dove possiamo avere un singolo thread. Gli STA sono limitati ad un thread ognuno ma i COM non pongono limiti sul numero di STA per processo. Quando una chiamata di metodo entra in uno STA, viene trasferita nell'unico thread della STA. Di conseguenza, un oggetto in uno STA riceverà e processerà solo una chiamata di metodo alla volta ed ogni chiamata di metodo che riceve arriverà sullo stesso thread. I componenti ArcObjects sono
thread safe, e gli sviluppatori possono usarli in ambienti multithreaded. Perchè le applicazioni ArcObjects siano eseguite efficientemente in un ambiente multithreaded, deve essere considerato il Threads in Isolation con l'apartment threading model degli ArcObjects. Questo modello lavora eliminando le comunicazioni cross-thread. Tutti i riferimenti ArcObjects con un thread soltanto comunicheranno con gli oggetti nello stesso thread.
Perchè questo modello funzioni, i
singleton objects in ArcGis 9.x sono stati disegnati per essere singleton per thread e non singleton per processo. Lo svantaggio di ospitare multipli oggetti singleton in un processo è compensato dal guadagno in prestazioni evitando la comunicazione cross-thread che occorrerebbe se un singleton fosse creato in un thread e poi reso accessibile ad altri thread. Come sviluppatore nel sistema ArcGIS, tutti gli oggetti, inclusi quelli che tu scrivi, devono stare a questa regola per far lavorare il modello Threads in Isolation. Se stai creando un oggetto singleton come parte del tuo sviluppo, devi assicurarti che questi oggetti siano singleton per thread e non per processo.
Per riuscire ad utilizzare gli ArcObjects in un ambiente multithreaded, i programmatori devono seguire il modello Threads in Isolation mentre, scrivendo il codice multithreaded, in ogni modo occorre evitare errori nell'applicazione come situazioni di blocchi, prestazioni negative dovute a
marshalling, e altri comportamenti inaspettati. Sebbene ci siano numerosi modi di implementare applicazioni multithreading, i seguenti sono alcuni dei più comuni scenari che gli sviluppatori incontrano.
Quando ti è richiesto di eseguire una lunga operazione, è conveniente eseguire l'operazione in un background thread mentre lasci libera l'applicazione di gestire altri task e lasci reattiva la UI. Degli esempi di queste operazioni includono iterazioni attraverso un FeatureCursor per caricare informazioni in un DataTable ed eseguire complesse operazioni topologiche mentre stai scrivendo i risultati in una nuova FeatureClass.
Per eseguire questo task, tieni in mente i seguenti punti:
- in accordo con il modello Thread in Isolation, non puoi condividere componenti ArcObjects tra threads. Invece puoi considerare il fatto che oggetti singleton sono per thread e, in un background thread, devi istanziare tutti i Factory richiesti per aprire FeatureClass, creare nuove FeatureClass, impostare riferimenti spaziali, e così via;
- tutta l'informazione passata al thread deve essere nella forma di tipo semplice o tipo gestito;
- nei casi in cui devi passare componenti ArcObjects dal thread main al worker thread, serializza l'oggetto in una string, passa la stringa al target thread, e deserializza l'oggetto indietro. Per esempio, puoi utilizzare
XmlSerializerClass per serializzare l'oggetto, come una workspace connection properties (un
IPropertySet), in una stringa; passa la stringa con la connection properties al worker thread; e deserializza la connection properties nel worker thread usando
XmlSerializeClass. In questo modo, l'oggetto connection properties è creato nel background thread e le chiamate cross apartment sono evitate;
- mentre esegui il background thread, puoi riportare una task progress in una dialog box sullo stato di avanzamento dell'elaborazione (vedi più avanti come fare).
Il seguente esempio dimostra l'utilizzo di un background thread che è usato per ciclare mediante un FeatureCursor e popolare una
DataTable che poi sarà usata più tardi dall'applicazione. Questo fa in modo che l'applicazione sia libera senza attendere che la tabella sia popolata.
// Generate the thread that populates the locations table.
Thread t = new Thread(new ThreadStart(PopulateLocationsTableProc));
// Mark the thread as a single threaded apartment (STA) to efficiently run ArcObjects.
t.SetApartmentState(ApartmentState.STA);
// Start the thread.
t.Start();
/// <summary>
/// Load the information from the MajorCities feature class to the locations table.
/// </summary>
private void PopulateLocationsTableProc()
{
//Get the ArcGIS path from the registry.
RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\ESRI\ArcGIS");
string path = Convert.ToString(key.GetValue("InstallDir"));
//Open the feature class. The workspace factory must be instantiated since it is a singleton per-thread.
IWorkspaceFactory wf = new ShapefileWorkspaceFactoryClass()as
IWorkspaceFactory;
IWorkspace ws = wf.OpenFromFile(System.IO.Path.Combine(path, @"Samples Net\Data\USZipCodeData"), 0);
IFeatureWorkspace fw = ws as IFeatureWorkspace;
IFeatureClass featureClass = fw.OpenFeatureClass(m_sShapefileName);
//Map the name and ZIP code fields.
int zipIndex = featureClass.FindField("ZIP");
int nameIndex = featureClass.FindField("NAME");
string cityName;
long zip;
try
{
//Iterate through the features and add the information to the table.
IFeatureCursor fCursor = null;
fCursor = featureClass.Search(null, true);
IFeature feature = fCursor.NextFeature();
int index = 0;
while (null != feature)
{
object obj = feature.get_Value(nameIndex);
if (obj == null)
continue;
cityName = Convert.ToString(obj);
obj = feature.get_Value(zipIndex);
if (obj == null)
continue;
zip = long.Parse(Convert.ToString(obj));
if (zip <= 0)
continue;
//Add the current location to the location table.
//m_locations is a DataTable that contains the cities and ZIP codes.
//It is defined in the full code before this excerpt starts.
DataRow r = m_locations.Rows.Find(zip);
if (null == r)
{
r = m_locations.NewRow();
r[1] = zip;
r[2] = cityName;
lock(m_locations)
{
m_locations.Rows.Add(r);
}
}
feature = fCursor.NextFeature();
index++;
}
//Release the feature cursor.
Marshal.ReleaseComObject(fCursor);
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
Dal sito
MSDN: "In .NET Framework 2.0 i nuovi thread sono inizializzati come
Apartment.MTA , se il loro stato di apartment non è stato impostato prima che siano partiti. Il thread della main application è inizializzato di default a Apartment.MTA. Non puoi impostare il thread dell'application main ad ApartmentState.STA impostando la proprietà Thread.ApartmentState nella prima linea di codice. Utilizza invece STAThreadAttribute". Come sviluppatore ArcObjects, questo significa che se la tua applicazione non è inizializzata come una single threaded application, il .NET framework creerà uno speciale singolo threaded apartment (STA) per tutti gli ArcObjects che sono marcati come STA. Questo farà sì che un thread vada a questo thread su ogni chiamata dall'applicazione agli ArcObjects. Questo forza i componenti ArcObjects ad una operazione di
marshalling per ogni chiamata ed eventualmente potrebbe essere circa 50 volte più lento per una chiamata ad un componente COM. Fortunatamente, tutto ciò può essere evitato semplicemente marcando la funzione main come [STAThread].
Il seguente codice d'esempio marca una console application come STA:
namespace ConsoleApplication1
{
class Program
{
[STAThread]
static void Main(string[] args)
{
// ...
}
}
}
Thread pool threads sono background threads. Thread pooling ti abilita ad utilizzare threads in modo più efficiente fornendo alla tua applicazione un pool di worker threads che sono gestiti dal sistema. Il vantaggio di usare un thread pool piuttosto che creare un nuovo thread per ogni task è che la creazione e la distruzione sono negative, mentre il thread pool può dare prestazioni migliori e migliore stabilità al sistema. Comunque, per disegno tutti i ThreadPool threads sono in multithreaded apartment (MTA) e pertanto non sarebbero utilizzabili per eseguire ArcObjects, che sono single-threaded apartment. Per aggirare questo problema, abbiamo alcune opzioni. Una è implementare un dedicato thread ArcObjects che è marcato come STAThread e delegare ogni chiamata dall'MTA threads a questo dedicato thread ArcObjects. Un'altra soluzione è usare un'implementazione di una personalizzata STA thread pool, così come un array di threads marcati STAThread per eseguire ArcObjects. Il seguente esempio dimostra come usare un array di STAThread threads per prendere un sottoinsieme di un RasterDataset, usando un differente thread per estrarre un sottoinsieme di ogni banda raster.
public class TaskInfo
{
//defauld constructor
public TaskInfo()
{
}
public TaskInfo(int BandID, ManualResetEvent doneEvent)
{
m_bandID = BandID;
m_doneEvent = doneEvent;
}
public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
try
{
//use rubber-band in order to getthe subset area from the user
IRubberBand2 rubberBand = new RubberEnvelopeClass();
IEnvelope envelope = (IEnvelope)rubberBand.TrackNew(m_pHookHelper.ActiveView.ScreenDisplay, null);
if (null == envelope envelope.IsEmpty)
return;
// verify that there are layers in the map
if (m_pHookHelper.FocusMap.LayerCount == 0)
return;
// get the top raster layer
m_rasterLayer = null;
IEnumLayer layers = m_pHookHelper.FocusMap.get_Layers(null, false);
layers.Reset();
ILayer layer = layers.Next();
while (layer != null)
{
if (layer is IRasterLayer)
{
m_rasterLayer = (IRasterLayer)layer;
break;
}
layer = layers.Next();
}
if (m_rasterLayer == null)
{
MessageBox.Show("There is no raster layer in the map");
return;
}
// assert if there is no overlap between the user envelope and the top raster
envelope.Intersect(m_rasterLayer.AreaOfInterest);
if (envelope.IsEmpty)
{
MessageBox.Show("The delineated envelope does not intersect the topmost raster in the map");
return;
}
//query number of bands in the input raster
m_intBandCount = m_rasterLayer.BandCount;
IDataset dataset = (IDataset)m_rasterLayer;
//get the workspace connection props since we'll need to connect it from each of the worker threads
IPropertySet connectionProps = dataset.Workspace.ConnectionProperties;
//get the workspace type
m_workSpaceType = dataset.Workspace.Type;
//serialize the connection properties into a string in order to avoid cross apartment calls
IXMLSerializer xmlSerializer = new XMLSerializerClass();
m_strInputRasterWorkspaceConnectionProps = xmlSerializer.SaveToString(connectionProps, null, null);
//m_inputRasterName = dataset.Name;
m_inputRasterName = m_rasterLayer.FilePath;
//open a folder-browser in order to get the output location of the subset raster
FolderBrowserDialog folderDlg = new FolderBrowserDialog();
folderDlg.Description = "Select output folder for the subset raster";
folderDlg.ShowNewFolderButton = true;
if (DialogResult.OK != folderDlg.ShowDialog())
return;
//get the output folder that the user had chosen
m_outputRasterPath = folderDlg.SelectedPath;
//get the input raster layer name (will be used in order to create the output raster)
string inputRasterLayerName = m_rasterLayer.Name;
if (inputRasterLayerName.IndexOf(".") != -1)
inputRasterLayerName = inputRasterLayerName.Substring(0, inputRasterLayerName.IndexOf("."));
//set the output format
m_outputFormat = FormatType.IMG;
//create the output raster dataset
CreateRasterDataset(m_rasterLayer, m_outputRasterPath, inputRasterLayerName + "_Subset.img", envelope);
//show the output progress form
m_progressDlg = new frmProgress();
//force the control creation in order to make sure that the form has a handle
m_progressDlg.CreateControl();
//set the statusbar message of the progress dialog
m_progressDlg.SetStatusBarMessage("Subsetting raster " + m_rasterLayer.Name);
//show the progress dialog
m_progressDlg.Show();
//run the subset thread. The subset process uses WaitHandle in order to
//wait on the subsetting threads to finish their task. In order to use WaitHandle.WaitAll()
//the call must be made from a MTA. The current command is STA and therefore a thread must be created
//in order to allow this(.NET threads gets created as MTA by default).
Thread t = new Thread(new ThreadStart(SubsetProc));
t.Start();
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
/// <summary>
/// main subset method
/// </summary>
/// <remarks>Please note that this sample does not copy colormap
/// in case where it is present. The outcome will be black and white</remarks>
private void SubsetProc()
{
try
{
m_lockObject = new object();
//set the message on the progress dialog statusbar
m_progressDlg.SetStatusBarMessage("starting subset process");
//create ManualResetEvent in order to notify the main threads that all threads
//are done with their task
ManualResetEvent[] doneEvents = new ManualResetEvent[m_intBandCount];
//create the subset threads
Thread[] threadTask = new Thread[m_intBandCount];
//create a thread for each of the bands of the input raster
//each task will subset a different raster-band. The information required for the
//subsetting the raster-band will be passed to the task by the user-defined
//class TaskInfo
for (int i = 0; i < m_intBandCount; i++)
{
//create the ManualResetEvent flad for the task in order to signal the waiting thread
//that the task had been completed
doneEvents[i] = new ManualResetEvent(false);
//create the Task-Information for the thread and pass in the relevant information
//Pleas note that all the information is passed as simple types
TaskInfo ti = new TaskInfo(i, doneEvents[i]);
ti.WorkspaceType = m_workSpaceType;
ti.Rows = m_intRows;
ti.Columns = m_intCols;
ti.I0 = m_i0;
ti.J0 = m_j0;
ti.InputRasterName = m_inputRasterName;
ti.OutputRasterPath = m_outputRasterPath;
ti.OutputRasterName = m_outputRasterName;
ti.InputRasterWSConnectionProps = m_strInputRasterWorkspaceConnectionProps;
// assign the subsetting thread for the rasterband.
threadTask[i] = new Thread(new ParameterizedThreadStart(SubsetRasterBand));
// Note the STA apartment which is required to run ArcObjects
threadTask[i].SetApartmentState(ApartmentState.STA);
threadTask[i].Name = "Subset_" + (i + 1).ToString();
// start the task and pass the task information
threadTask[i].Start((object)ti);
}
//Sets the state of the event to signaled, thus allowing one or more of the waiting threads to proceed
m_autoEvent.Set();
// Wait for all threads to complete their task...
WaitHandle.WaitAll(doneEvents);
// verify that all tasks are done and that the memory is cleaned
for (int i = 0; i < m_intBandCount; i++)
{
threadTask[i].Join();
}
threadTask = null;
// spin a new thread in order to claculate statistics and pyramid layers
Thread additionalDataTask = new Thread(new ParameterizedThreadStart(CalculateStatsAndPyramids));
additionalDataTask.SetApartmentState(ApartmentState.STA);
additionalDataTask.Name = "calc_aux_data";
additionalDataTask.Start(System.IO.Path.Combine(m_outputRasterPath, m_outputRasterName));
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
/// <summary>
/// subset task method
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
try
{
System.Diagnostics.Trace.WriteLine(Thread.CurrentThread.GetApartmentState());
// The state object must be cast to the correct type, because the
// signature of the WaitOrTimerCallback delegate specifies type
// Object.
TaskInfo ti = (TaskInfo)state;
//set the message on the progress dialog statusbar
m_progressDlg.SetStatusBarMessage("Copy RasterBand #" + (ti.BandID + 1).ToString());
IRasterDataset rasterataset = OpenRasterDataset(ti);
//cast the input raster bandCollection
IRasterBandCollection rasterBaneCollection = (IRasterBandCollection)rasterataset;
//get the raster band from which to copy the data
IRasterBand rasterBand = rasterBaneCollection.Item(ti.BandID);
IRawPixels rawPixels = (IRawPixels)rasterBand;
//open the newly created raster dataset
IRasterDataset newRasterDataset = OpenOutputRasterDataset(ti.OutputRasterPath, ti.OutputRasterName);
//get the raster-band into which data will be copied
IRasterBandCollection newRasterBandCollection = (IRasterBandCollection)newRasterDataset;
IRasterBand newRasterBand = newRasterBandCollection.Item(ti.BandID);
IRawPixels newRasterPixels = (IRawPixels)newRasterBand;
//set the size of the pixelBlock
int pixBlockSizeX = Math.Min(128, m_intCols);
int pixBlockSizeY = Math.Min(128, m_intRows);
//calculate the progressbar MaxValue
int hBlocks = Convert.ToInt32(m_intCols / pixBlockSizeX + 0.5);
int vBlocks = Convert.ToInt32(m_intRows / pixBlockSizeY + 0.5);
//set the maximum value of the progress controls of the progress dialog
if (ti.BandID == 0)
m_progressDlg.SetProgressbarMaximum(hBlocks * vBlocks);
//set the upper left pixel from which the subset starts
IPnt pnt = new PntClass();
pnt.SetCoords(Convert.ToDouble(pixBlockSizeX), Convert.ToDouble(pixBlockSizeY));
//instantiate the Top-Left-Corner to be used with the pixel-block for the copying process
IPnt tlc = new PntClass();
Monitor.Enter(m_lockObject);
//create the pixelBlock for the copying process
IPixelBlock pixelBlock = rawPixels.CreatePixelBlock(pnt);
Monitor.Exit(m_lockObject);
int p = 0;
int q = 0;
//read the raster band info from the input raster-band and write it to the new raster-band
for (int j = ti.J0; j < (ti.J0 + m_intRows); j += pixBlockSizeY)
{
for (int i = ti.I0; i < (ti.I0 + m_intCols); i += pixBlockSizeX)
{
//set the upper-left corner coordinate
tlc.SetCoords(Convert.ToDouble(i), Convert.ToDouble(j));
// block the other thread while reading from the raster
m_autoEvent.WaitOne();
//read the pixels from the input raster-band into the pixel-block
rawPixels.Read(tlc, (PixelBlock)pixelBlock);
m_autoEvent.Set();
//set the upper-left corner coordinate in order to write into the subset raster
tlc.SetCoords(Convert.ToDouble(p), Convert.ToDouble(q));
// block the other thread while writing to the output raster
m_autoEvent.WaitOne();
//write the pixels to the output rasterband
newRasterPixels.Write(tlc, (PixelBlock)pixelBlock);
m_autoEvent.Set();
//increment the horizontal coordinate of the output rasterband
p += pixBlockSizeX;
//increment the progressbars
if (ti.BandID < 3)
{
m_progressDlg.IncrementProgressBar(ti.BandID + 1);
}
}
//reset the horizontal coordinate of the output rasterband
p = 0;
//increment the vertical coordinate of the output rasterband
q += pixBlockSizeY;
}
//// wait untill all copy thraeds are done copying
////calculate statistics
////need to lock all other threads in order to write to the rasterband
//m_autoEvent.WaitOne();
////Monitor.Enter(m_lockObject);
//m_progressDlg.SetStatusBarMessage("compute statistics for band #" + (ti.BandID + 1).ToString());
//newRasterBand.ComputeStatsAndHist();
////set the message on the progress dialog statusbar
//m_progressDlg.SetStatusBarMessage("done subsetting band #" + (ti.BandID + 1).ToString());
//////signal the next available thread to write to the subset dataset
////Monitor.Exit(m_lockObject);
//m_autoEvent.Set();
//signal the main thread the that thread has finished its task
ti.DoneEvent.Set();
}
catch (Exception ex)
{
//m_autoEvent.Set();
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
In molti casi, vuoi sincronizzare l'esecuzione dei concorrenti thread che stanno lavorando. Normalmente, attendi che uno o più thread finiscano il loro task, segni in attesa un thread per proseguire poi il task quando certe condizioni vengono rispettate, verifichi se un dato thread sta ancora lavorando, cambi la priorità del thread o dai delle altre indicazioni.
In .NET ci sono diversi modi di gestire l'esecuzione di thread che stanno lavorando. Le principali classi disponibili per aiutare la gestione thread sono le seguenti:
-System.Threading.Thread: usata per creare e controllare i thread, cambiare la priorità e prendere lo status;
-System.Threading.WaitHandle: definisce un meccanismo per indicare la presa o il rilascio esclusivo a risorse condivise, consentendo di restringere l'accesso ad un blocco di codice. Se chiami il metodo WaitHandle.WaitAll() questo deve essere fatto da un multithreaded apartment (MTA) thread. Per lanciare multipli task sincronizzati, prima devi lanciare un worker thread che, a turno, eseguirà i multipli threads;
-System.Threading.Monitor: simile al System.Threading.WaitHandle provvede ad un meccanismo che sincronizza l'accesso agli oggetti;
-System.Threading.AutoResetEvent e System.Threading.ManualResetEvent: usato per notificare ai threads in attesa che un evento è occorso, consentento ai threads di comunicare l'un con l'altro tramite segnalazione.
Il seguente esempio estende quello coperto precedentemente. Esso usa le classi ManualResetEvent e WaitHandle per attendere che i multipli threads finiscano i loro tasks. In aggiunta dimostra, usando la classe AutoResetEvent, come bloccare i thread in esecuzione dall'accesso ad un blocco di codice e segnalare al prossimo disponibile thread quando il corrente thread ha completato il suo task.
public class TaskInfo
{
//defauld constructor
public TaskInfo()
{
}
public TaskInfo(int BandID, ManualResetEvent doneEvent)
{
m_bandID = BandID;
m_doneEvent = doneEvent;
}
public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
try
{
//use rubber-band in order to getthe subset area from the user
IRubberBand2 rubberBand = new RubberEnvelopeClass();
IEnvelope envelope = (IEnvelope)rubberBand.TrackNew(m_pHookHelper.ActiveView.ScreenDisplay, null);
if (null == envelope envelope.IsEmpty)
return;
// verify that there are layers in the map
if (m_pHookHelper.FocusMap.LayerCount == 0)
return;
// get the top raster layer
m_rasterLayer = null;
IEnumLayer layers = m_pHookHelper.FocusMap.get_Layers(null, false);
layers.Reset();
ILayer layer = layers.Next();
while (layer != null)
{
if (layer is IRasterLayer)
{
m_rasterLayer = (IRasterLayer)layer;
break;
}
layer = layers.Next();
}
if (m_rasterLayer == null)
{
MessageBox.Show("There is no raster layer in the map");
return;
}
// assert if there is no overlap between the user envelope and the top raster
envelope.Intersect(m_rasterLayer.AreaOfInterest);
if (envelope.IsEmpty)
{
MessageBox.Show("The delineated envelope does not intersect the topmost raster in the map");
return;
}
//query number of bands in the input raster
m_intBandCount = m_rasterLayer.BandCount;
IDataset dataset = (IDataset)m_rasterLayer;
//get the workspace connection props since we'll need to connect it from each of the worker threads
IPropertySet connectionProps = dataset.Workspace.ConnectionProperties;
//get the workspace type
m_workSpaceType = dataset.Workspace.Type;
//serialize the connection properties into a string in order to avoid cross apartment calls
IXMLSerializer xmlSerializer = new XMLSerializerClass();
m_strInputRasterWorkspaceConnectionProps = xmlSerializer.SaveToString(connectionProps, null, null);
//m_inputRasterName = dataset.Name;
m_inputRasterName = m_rasterLayer.FilePath;
//open a folder-browser in order to get the output location of the subset raster
FolderBrowserDialog folderDlg = new FolderBrowserDialog();
folderDlg.Description = "Select output folder for the subset raster";
folderDlg.ShowNewFolderButton = true;
if (DialogResult.OK != folderDlg.ShowDialog())
return;
//get the output folder that the user had chosen
m_outputRasterPath = folderDlg.SelectedPath;
//get the input raster layer name (will be used in order to create the output raster)
string inputRasterLayerName = m_rasterLayer.Name;
if (inputRasterLayerName.IndexOf(".") != -1)
inputRasterLayerName = inputRasterLayerName.Substring(0, inputRasterLayerName.IndexOf("."));
//set the output format
m_outputFormat = FormatType.IMG;
//create the output raster dataset
CreateRasterDataset(m_rasterLayer, m_outputRasterPath, inputRasterLayerName + "_Subset.img", envelope);
//show the output progress form
m_progressDlg = new frmProgress();
//force the control creation in order to make sure that the form has a handle
m_progressDlg.CreateControl();
//set the statusbar message of the progress dialog
m_progressDlg.SetStatusBarMessage("Subsetting raster " + m_rasterLayer.Name);
//show the progress dialog
m_progressDlg.Show();
//run the subset thread. The subset process uses WaitHandle in order to
//wait on the subsetting threads to finish their task. In order to use WaitHandle.WaitAll()
//the call must be made from a MTA. The current command is STA and therefore a thread must be created
//in order to allow this(.NET threads gets created as MTA by default).
Thread t = new Thread(new ThreadStart(SubsetProc));
t.Start();
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
/// <summary>
/// main subset method
/// </summary>
/// <remarks>Please note that this sample does not copy colormap
/// in case where it is present. The outcome will be black and white</remarks>
private void SubsetProc()
{
try
{
m_lockObject = new object();
//set the message on the progress dialog statusbar
m_progressDlg.SetStatusBarMessage("starting subset process");
//create ManualResetEvent in order to notify the main threads that all threads
//are done with their task
ManualResetEvent[] doneEvents = new ManualResetEvent[m_intBandCount];
//create the subset threads
Thread[] threadTask = new Thread[m_intBandCount];
//create a thread for each of the bands of the input raster
//each task will subset a different raster-band. The information required for the
//subsetting the raster-band will be passed to the task by the user-defined
//class TaskInfo
for (int i = 0; i < m_intBandCount; i++)
{
//create the ManualResetEvent flad for the task in order to signal the waiting thread
//that the task had been completed
doneEvents[i] = new ManualResetEvent(false);
//create the Task-Information for the thread and pass in the relevant information
//Pleas note that all the information is passed as simple types
TaskInfo ti = new TaskInfo(i, doneEvents[i]);
ti.WorkspaceType = m_workSpaceType;
ti.Rows = m_intRows;
ti.Columns = m_intCols;
ti.I0 = m_i0;
ti.J0 = m_j0;
ti.InputRasterName = m_inputRasterName;
ti.OutputRasterPath = m_outputRasterPath;
ti.OutputRasterName = m_outputRasterName;
ti.InputRasterWSConnectionProps = m_strInputRasterWorkspaceConnectionProps;
// assign the subsetting thread for the rasterband.
threadTask[i] = new Thread(new ParameterizedThreadStart(SubsetRasterBand));
// Note the STA apartment which is required to run ArcObjects
threadTask[i].SetApartmentState(ApartmentState.STA);
threadTask[i].Name = "Subset_" + (i + 1).ToString();
// start the task and pass the task information
threadTask[i].Start((object)ti);
}
//Sets the state of the event to signaled, thus allowing one or more of the waiting threads to proceed
m_autoEvent.Set();
// Wait for all threads to complete their task...
WaitHandle.WaitAll(doneEvents);
// verify that all tasks are done and that the memory is cleaned
for (int i = 0; i < m_intBandCount; i++)
{
threadTask[i].Join();
}
threadTask = null;
// spin a new thread in order to claculate statistics and pyramid layers
Thread additionalDataTask = new Thread(new ParameterizedThreadStart(CalculateStatsAndPyramids));
additionalDataTask.SetApartmentState(ApartmentState.STA);
additionalDataTask.Name = "calc_aux_data";
additionalDataTask.Start(System.IO.Path.Combine(m_outputRasterPath, m_outputRasterName));
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
/// <summary>
/// subset task method
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
try
{
System.Diagnostics.Trace.WriteLine(Thread.CurrentThread.GetApartmentState());
// The state object must be cast to the correct type, because the
// signature of the WaitOrTimerCallback delegate specifies type
// Object.
TaskInfo ti = (TaskInfo)state;
//set the message on the progress dialog statusbar
m_progressDlg.SetStatusBarMessage("Copy RasterBand #" + (ti.BandID + 1).ToString());
IRasterDataset rasterataset = OpenRasterDataset(ti);
//cast the input raster bandCollection
IRasterBandCollection rasterBaneCollection = (IRasterBandCollection)rasterataset;
//get the raster band from which to copy the data
IRasterBand rasterBand = rasterBaneCollection.Item(ti.BandID);
IRawPixels rawPixels = (IRawPixels)rasterBand;
//open the newly created raster dataset
IRasterDataset newRasterDataset = OpenOutputRasterDataset(ti.OutputRasterPath, ti.OutputRasterName);
//get the raster-band into which data will be copied
IRasterBandCollection newRasterBandCollection = (IRasterBandCollection)newRasterDataset;
IRasterBand newRasterBand = newRasterBandCollection.Item(ti.BandID);
IRawPixels newRasterPixels = (IRawPixels)newRasterBand;
//set the size of the pixelBlock
int pixBlockSizeX = Math.Min(128, m_intCols);
int pixBlockSizeY = Math.Min(128, m_intRows);
//calculate the progressbar MaxValue
int hBlocks = Convert.ToInt32(m_intCols / pixBlockSizeX + 0.5);
int vBlocks = Convert.ToInt32(m_intRows / pixBlockSizeY + 0.5);
//set the maximum value of the progress controls of the progress dialog
if (ti.BandID == 0)
m_progressDlg.SetProgressbarMaximum(hBlocks * vBlocks);
//set the upper left pixel from which the subset starts
IPnt pnt = new PntClass();
pnt.SetCoords(Convert.ToDouble(pixBlockSizeX), Convert.ToDouble(pixBlockSizeY));
//instantiate the Top-Left-Corner to be used with the pixel-block for the copying process
IPnt tlc = new PntClass();
Monitor.Enter(m_lockObject);
//create the pixelBlock for the copying process
IPixelBlock pixelBlock = rawPixels.CreatePixelBlock(pnt);
Monitor.Exit(m_lockObject);
int p = 0;
int q = 0;
//read the raster band info from the input raster-band and write it to the new raster-band
for (int j = ti.J0; j < (ti.J0 + m_intRows); j += pixBlockSizeY)
{
for (int i = ti.I0; i < (ti.I0 + m_intCols); i += pixBlockSizeX)
{
//set the upper-left corner coordinate
tlc.SetCoords(Convert.ToDouble(i), Convert.ToDouble(j));
// block the other thread while reading from the raster
m_autoEvent.WaitOne();
//read the pixels from the input raster-band into the pixel-block
rawPixels.Read(tlc, (PixelBlock)pixelBlock);
m_autoEvent.Set();
//set the upper-left corner coordinate in order to write into the subset raster
tlc.SetCoords(Convert.ToDouble(p), Convert.ToDouble(q));
// block the other thread while writing to the output raster
m_autoEvent.WaitOne();
//write the pixels to the output rasterband
newRasterPixels.Write(tlc, (PixelBlock)pixelBlock);
m_autoEvent.Set();
//increment the horizontal coordinate of the output rasterband
p += pixBlockSizeX;
//increment the progressbars
if (ti.BandID < 3)
{
m_progressDlg.IncrementProgressBar(ti.BandID + 1);
}
}
//reset the horizontal coordinate of the output rasterband
p = 0;
//increment the vertical coordinate of the output rasterband
q += pixBlockSizeY;
}
//// wait untill all copy thraeds are done copying
////calculate statistics
////need to lock all other threads in order to write to the rasterband
//m_autoEvent.WaitOne();
////Monitor.Enter(m_lockObject);
//m_progressDlg.SetStatusBarMessage("compute statistics for band #" + (ti.BandID + 1).ToString());
//newRasterBand.ComputeStatsAndHist();
////set the message on the progress dialog statusbar
//m_progressDlg.SetStatusBarMessage("done subsetting band #" + (ti.BandID + 1).ToString());
//////signal the next available thread to write to the subset dataset
////Monitor.Exit(m_lockObject);
//m_autoEvent.Set();
//signal the main thread the that thread has finished its task
ti.DoneEvent.Set();
}
catch (Exception ex)
{
//m_autoEvent.Set();
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
Talvolta la tua applicazione utilizza strutture dati come oggetti gestiti tipo
DataTable o
HashTable. Questi oggetti gestiti .NET ti consentono di condividerli attraverso multipli threads come un thread di lettura dati e un thread di rendering. Comunque, consulta il sito MSDN per verificare se un oggetto è thread safe. In molti casi, un oggetto è thread safe per la lettura ma non per la scrittura. Alcune collection implementano un metodo Syncronized, che provvede a sincronizzare la collection. Nei casi dove il tuo oggetto è accessibile da più di un thread, devi acquisire un esclusivo lock in accordo con la sezione Thread safety nell'MSDN, riguardo a questo particolare oggetto. L'acquisizione come esclusivo lock può essere fatta utilizzando uno dei metodi di sincronizzazione descritti nella precedente sezione od utilizzando un
lock statement, che marca un blocco come sezione critica per ottenere un esclusivo lock per un dato oggetto. Esso assicura che, qualora un altro thread cerchi di accedere all'oggetto, questo sarà bloccato fino a che l'oggetto verrà rilasciato (uscita dal lock).
La seguente figura mostra come un DataTable è condiviso da threads multipli. Prima però, controlla la DataTable class su MSDN per verificare se è thread safe.
In questa pagina, controlla la sezione Thread Safety, dove dice: "Questo tipo è safe per operazioni di lettura multithreaded. Devi sincronizzare per operazioni di scrittura".
Questo significa che per operazioni di lettura sul DataTable non ci sono problemi, ma devi prevenire che altri thread possano accedere alla tabella quando scrivono su di esso. Il seguente esempio mostra come bloccare questi altri threads:
r = m_locations.NewRow();
r[1] = zip;
r[2] = cityName;
lock(m_locations)
{
m_locations.Rows.Add(r);
}
In molti casi dove stai utilizzando un background thread per eseguire lunghe operazioni, vuoi riportare all'utente uno stato di avanzamento, errori o altre informazioni relative al task che stai facendo nel thread. Questo può essere fatto aggiornando un controllo sulla UI dell'applicazione. Comunque, in Windows, i controlli forms sono legati ad uno specifico thread (generalmente al thread main) e non sono thread safe. Come risultato, devi delegare, e poi fare un marshal, delle chiamate al controllo UI al thread al quale il controllo appartiene. La delega è fatta attraverso la chiamata al metodo Control.Invoke, che esegue il delegato sul thread che appartiene all'handle Window sottostante il controllo. Per verificare se un chiamante deve chiamare un metodo invoke, puoi utilizzare la proprietà Control.InvokeRequired. Devi assicurarti che l'handle del controllo sia creato prima che venga chiamato Control.Invoke o Control.InvokeRequired.
Il seguente esempio dimostra come visualizzare uno stato di avanzamento relativo ad un background task in una user form.
1) Nella user form, dichiara un delegato attraverso il quale passi le informazioni al controllo
public class WeatherItemSelectionDlg : System.Windows.Forms.Form
{
private System.Windows.Forms.GroupBox grpWeatherItems;
private System.Windows.Forms.ListBox lstWeatherItemNames;
private System.Windows.Forms.Button btnRefreshList;
private System.Windows.Forms.Label lblSelect;
private System.Windows.Forms.TextBox txtSelect;
private System.Windows.Forms.CheckBox chkNewSelection;
private System.Windows.Forms.Button btnSelect;
private System.Windows.Forms.Button btnDismiss;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.ContextMenu contextMenu1;
private System.Windows.Forms.MenuItem menuZoomTo;
//Class members
private IActiveView m_activeView = null;
private RSSWeatherLayerClass m_weatherLayer = null;
private string[] m_cityNames = null;
private DataTable m_weatherItemsTable = null;
// This delegate enables asynchronous calls for setting
// the text property on a TextBox control.
private delegate void IncrementProgressBarCallback();
private delegate void AddListItmCallback(string item);
private delegate void ShowProgressBarCallBack();
private delegate void HideProgressBarCallBack();
2) Nella user form, imposta il metodo per aggiornare la UI del controllo. Notare la chiamata ad Invoke. Il metodo deve avere la stessa firma come la precedente dichiarazione del delegato:
/// <summary>
/// Make Thread-Safe Calls to Windows Forms Controls
/// </summary>
/// <param name="item"></param>
private void AddListItemString(string item)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.lstWeatherItemNames.InvokeRequired)
{
//call itself on the main thread
AddListItmCallback d = new AddListItmCallback(AddListItemString);
this.Invoke(d, new object[] { item });
}
else
{
//guaranteed to run on the main UI thread
this.lstWeatherItemNames.Items.Add(item);
}
}
3) nel background thread, implementa il metodo che utilizza il delegato e passa il messaggio che sarà visualizzato sulla user form
/// <summary>
/// Populate the listbox according to a selection criteria
/// </summary>
private void PopulateSubListProc()
{
//get the selection criteria
string exp = txtSelect.Text;
//in case that the user did not specify a criteria, pupolate the entire citiname list
if(exp == "")
{
PopulateWeatherItemsTableProc();
return;
}
//set query
exp = "CITYNAME LIKE '" + exp + "%'";
//do the criteria selection against the table
DataRow[] rows = m_weatherItemsTable.Select(exp);
//iterate through the selectionset
foreach(DataRow r in rows)
{
//add the cityName to the listbox
AddListItemString(Convert.ToString(r[0]));
}
}
4) Scrivi la chiamata che lancia il background thread stesso, passando il metodo scritto nel punto 3
//spawn a thread to populate the list with items that match the selection criteria
Thread t = new Thread(new ThreadStart(PopulateSubListProc));
t.Start();
In alcune applicazioni multithreads, potresti aver bisogno di fare chiamate agli ArcObjects da differenti threads in esecuzione. Per esempio potresti avere un background thread che prende informazioni da un web service, il quale poi aggiunge un nuovo oggetto sulla mappa, cambia l'estensione della mappa, o esegue uno strumento di geoprocessiong (GP) per fare delle analisi.
Un caso molto comune è chiamare gli ArcObjects da un metodo di un evento Timer.
Comunque, questo può essere evitato trattando i componenti ArcObjects come se fossero un controllo UI e utilizzando Invoke a delegare la chiamata al thread main dove i componenti ArcObjects sono creati. Così facendo, non vengono fatte chiamate cross-apartment.
L'interfaccia ISynchronizeInvoke include i metodi Invoke, BeginInvoke, e EndInvoke. Implementare questi metodi può essere un compito arduo. Invece, dovresti ereditare la tua classe direttamente da System.Windows.Forms.Control od avere una classe di helper che eredita dal controllo. Entrambe le opzioni provvedono ad una semplice ed efficiente soluzione per invocare i metodi.
Il seguente codice impiega una user-defined classe InvokeHelper per invocare un event handler timer per ricentrare il riquadro visible della mappa ed impostare la rotazione della mappa. Nota che alcune delle logiche applicative devono essere fatte nella classe InvokeHelper in aggiunta alla struttura user-defined che è passata dal metodo delegato.
/// <summary>
/// A helper method used to delegate calls to the main thread.
/// </summary>
public sealed class InvokeHelper: Control
{
//Delegate used to pass the invoked method to the main thread.
public delegate void MessageHandler(NavigationData navigationData);
//Class members.
private IActiveView m_activeView;
private IPoint m_point = null;
/// <summary>
/// Class constructor.
/// </summary>
/// <param name="activeView"></param>
public InvokeHelper(IActiveView activeView)
{
//Make sure that the control was created and that it has a valid handle.
this.CreateHandle();
this.CreateControl();
//Get the active view.
m_activeView = activeView;
}
/// <summary>
/// Delegate the required method onto the main thread.
/// </summary>
/// <param name="navigationData"></param>
public void InvokeMethod(NavigationData navigationData)
{
// Invoke HandleMessage through its delegate.
if (!this.IsDisposed && this.IsHandleCreated)
Invoke(new MessageHandler(CenterMap), new object[]
{
navigationData
}
);
}
/// <summary>
/// The method that gets executed by the delegate.
/// </summary>
/// <param name="navigationData"></param>
public void CenterMap(NavigationData navigationData)
{
//Get the current map visible extent.
IEnvelope envelope =
m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds;
if (null == m_point)
{
m_point = new PointClass();
}
//Set the new map center coordinate.
m_point.PutCoords(navigationData.X, navigationData.Y);
//Center the map around the new coordinate.
envelope.CenterAt(m_point);
m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds = envelope;
//Rotate the map to the new rotation angle.
m_activeView.ScreenDisplay.DisplayTransformation.Rotation =
navigationData.Azimuth;
}
/// <summary>
/// Control initialization.
/// </summary>
private void InitializeComponent(){}
/// <summary>
/// A user defined data structure used to pass information to the invoke method.
/// </summary>
public struct NavigationData
{
public double X;
public double Y;
public double Azimuth;
/// <summary>
/// Struct constructor.
/// </summary>
/// <param name="x">map x coordinate</param>
/// <param name="y">map x coordinate</param>
/// <param name="azimuth">the new map azimuth</param>
public NavigationData(double x, double y, double azimuth)
{
X = x;
Y = y;
Azimuth = azimuth;
}
/// <summary>
/// This command triggers the tracking functionality.
/// </summary>
public sealed class TrackObject: BaseCommand
{
//Class members.
private IHookHelper m_hookHelper = null;
private InvokeHelper m_invokeHelper = null;
private System.Timers.Timer m_timer = null;
/// <summary>
/// Occurs when this command is created.
/// </summary>
/// <param name="hook">Instance of the application</param>
public override void OnCreate(object hook)
{
//Instantiate the timer.
m_timer = new System.Timers.Timer(60);
m_timer.Enabled = false;
//Set the timer's elapsed event handler.
m_timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
}
/// <summary>
/// Occurs when this command is clicked.
/// </summary>
public override void OnClick()
{
//Create the InvokeHelper class.
if (null == m_invokeHelper)
m_invokeHelper = new InvokeHelper(m_hookHelper.ActiveView);
//Start the timer.
if (!m_bIsRunning)
m_timer.Enabled = true;
else
m_timer.Enabled = false;
}
/// <summary>
/// Timer elapsed event handler.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
//Create the navigation data structure.
NavigationData navigationData = new NavigationData(currentPoint.X,
currentPoint.Y, azimuth);
//Update the map extent and rotation.
m_invokeHelper.InvokeMethod(navigationData);
}
Per utilizzare GP in una applicazione asincrona/multithreaded, usa il pattern di esecuzione asincrona esposto in ArcGIS Server 9.2 utilizzando GP Services. Questo pattern abilita il desktop a lavorare con GP in modalità di esecuzione asincrona.
Qui potete trovare gli esempio completi:
RSS weather layer
Multithreaded raster subset
RSS weather 3D layer