Writing multithreaded ArcObjects code


SummaryMultithreading allows an application to do more than one thing at a time within a single process. This topic details what multithreading means in the context of the ArcObjects .NET Software Developer Kit (SDK) as well as the rules that must be followed to properly integrate threading into ArcObjects applications.

This topic does not attempt to give an introduction to multithreading or to teach multithreading concepts, but rather to give practical solutions for daily ArcObjects programming problems that involve multithreading.

In this topic


Introduction to multithreading

Multithreading is generally used to improve the responsiveness of applications. This responsiveness can be the result of real performance improvements or the perception of improved performance. By using multiple threads of execution in your code, you can separate data processing and input/output (I/O) operations from the management of your program's user interface (UI). This will prevent any long data processing operations from reducing the responsiveness of your UI.
 
The performance advantages of multithreading come at the cost of increased complexity in the design and maintenance of your code. Threads of an application share the same memory space, so you must make sure that accessing shared data structures is synchronized to prevent the application from entering into an invalid state or crashing. This synchronization is often called concurrency control.
 
Concurrency control can be achieved at two levels: the object level and the application level. It can be achieved at the object level when the shared object is thread safe, meaning that the object forces all threads trying to access it to wait until the current thread accessing the object is finished modifying the object’s state. Concurrency control can be done at the application level by acquiring an exclusive lock on the shared object, allowing one thread at a time to modify the object’s state. Be careful when using locks, as overuse of them will protect your data but can also lead to decreased performance. Finding a balance between performance and protection requires careful consideration of your data structures and the intended usage pattern for your extra threads.

When to use multithreading

There are two items to consider when building multithreaded applications: thread safety and scalability. It is important for all objects to be thread safe, but simply having thread-safe objects does not automatically mean that creating multithreaded applications is straightforward or that the resulting application will provide improved performance.
 
The .NET Framework allows you to easily generate threads in your application; however, writing multithreaded ArcObjects code should be done carefully. The underlying architecture of ArcObjects is Component Object Model (COM). For that reason, writing multithreading ArcObjects applications requires an understanding of both .NET multithreading and the threading model for COM.
 
Multithreading will not always make your code run faster; in many cases it will add extra overhead and complexity that would eventually reduce the execution speed of your code. Multithreading should only be used when the added complexity is worth the cost. A general rule of thumb is that a task is suited to multithreading if it can be broken into different independent tasks.

ArcObjects threading model

All ArcObjects components are marked as single threaded apartment (STA). STAs are limited to one thread each, but COM places no limit on the number of STAs per process. When a method call enters an STA, it is transferred to the STA's one and only thread. Consequently, an object in an STA will only receive and process one method call at a time, and every method call that it receives will arrive on the same thread.
 
ArcObjects components are thread safe, and developers can use them in multithreaded environments. For ArcObjects applications to run efficiently in a multithreaded environment, the apartment threading model used by ArcObjects, Threads in Isolation, must be considered. This model works by eliminating cross-thread communication. All ArcObjects references within a thread should only communicate with objects in the same thread.
 
For this model to work, the singleton objects at ArcGIS 9.x were designed to be singletons per thread and not singletons per process. The resource overhead of hosting multiple singletons in a process is outweighed by the performance gain of stopping the cross-thread communication that would occur if the singleton were created in one thread, then accessed from the other threads.
 
As a developer of the extensible ArcGIS system, all objects, including those you write, must adhere to this rule for the Threads in Isolation model to work. If you are creating singleton objects as part of your development, you must ensure that these objects are singletons per thread, not per process.
 
To be successful using ArcObjects in a multithreaded environment, programmers must follow the Threads in Isolation model while writing their multithreaded code in such a way as to avoid application failures such as deadlock situations, negative performance due to marshalling, and other unexpected behavior.

Multithreading scenarios

Although there are a number of ways to implement multithreading applications, the following are a few of the more common scenarios that developers encounter.
 
The ArcObjects .NET SDK includes several threading samples, referenced in the following code example, that cover the scenarios described in this topic. The samples demonstrate solutions for real-life problems while showing the best programming practices. While these samples use multithreading as part of a solution for a given problem, in some you will find that the multithreading is just an architectural aspect of a broader picture.
 
Running lengthy operations on a background thread
When you are required to run a lengthy operation, it is convenient to allow the operation to run on a background thread while leaving the application free to handle other tasks and keep the UI responsive. Some examples of such operations include iterating through a FeatureCursor to load information into a DataTable and performing a complex topological operation while writing the result into a new FeatureClass.
 
To accomplish this task, keep in mind the following points:
 
The following code example, an excerpt from the RSS Weather Layer sample, demonstrates a background thread that is used to iterate through a FeatureCursor and populate a DataTable that will is used later in the application. This keeps the application free to run without waiting for the table to be populated.
 

[C#]
// 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);
}
}
Implementing stand-alone ArcObjects applications
As stated on the Microsoft Developer Network (MSDN) Web site, "In the .NET Framework version 2.0, new threads are initialized as ApartmentState.MTA if their apartment state has not been set before they are started. The main application thread is initialized to ApartmentState.MTA by default. You can no longer set the main application thread to ApartmentState.STA by setting the Thread.ApartmentState property on the first line of code. Use the STAThreadAttribute instead."
 
As an ArcObjects developer, this means that if your application is not initialized as a single threaded application, the .NET framework will create a special single threaded apartment (STA) thread for all ArcObjects since they are marked as STA. This will cause a thread switch to this thread on each call from the application to ArcObjects. In turn, this forces the ArcObjects components to marshall each call, and eventually it may be about 50 times slower for a call to the COM component. Fortunately, this can be avoided by simply marking the main function as [STAThread].
 
The following code example marks a console application as STA:
 

[C#]
namespace ConsoleApplication1
{
  class Program
  {
    [STAThread]
    static void Main(string[] args)
    {
       // ...
    }
  }
}
If you create a Windows Application using the VS2005 project wizard, it automatically puts the [STAThread] on the main function for you.
Using managed ThreadPool and BackGroundWorker threads
Thread pool threads are background threads. Thread pooling enables you to use threads more efficiently by providing your application with a pool of worker threads that are managed by the system. The advantage of using a thread pool over creating a new thread for each task is that thread creation and destruction overhead is negated, which can result in better performance and better system stability.
 
However, by design all ThreadPool threads are in the multithreaded apartment (MTA) and therefore should not be used to run ArcObjects which are single-threaded apartment.
To work around this problem you have a few options. One is to implement a dedicated ArcObjects thread that is marked as STAThread and to delegate each of the calls from the MTA threads to this dedicated ArcObjects thread. Another solution is to use an implementation of a custom STA thread pool, such as an array of threads marked as STAThread to run ArcObjects.
 
The following code example—an excerpt from the Multithreaded raster subset sample—demonstrates using an array of STAThread threads to subset a RasterDataset, using a different thread to subset each raster band:
 

[C#]
/// <summary>
/// Class used to pass the task information to the working thread.
/// </summary>
public class 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)
{ 
    ...
// Run the subset thread that will spin off separate subset tasks. By default, this thread will run as MTA.
// This is needed to use WaitHandle.WaitAll(), the call must be made
// from MTA.
Thread t = new Thread(new ThreadStart(SubsetProc));
t.Start();
}

/// <summary>
/// Main subset method.
/// </summary>
private void SubsetProc()
{
...
//Create a dedicated thread for each band of the input raster.
//Create the subset threads.
Thread[] threadTask = new Thread[m_intBandCount];

//Each thread will subset a different raster band. 
    //All information required for subsetting the raster band will be passed to the task by the user-defined class TaskInfo.
for (int i = 0; i < m_intBandCount; i++)
{
   TaskInfo ti = new TaskInfo(i, doneEvents[i]);
   ...
   // Assign the subsetting thread for the rasterband.
   threadTask[i] = new Thread(new ParameterizedThreadStart(SubsetRasterBand));
   // Note the STA apartment that 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);
}
...
// Wait for all threads to complete their task…
WaitHandle.WaitAll(doneEvents);
...
}

/// <summary>
/// Subset task method.
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
// The state object must be cast to the correct type, because the
// signature of the WaitForTimerCallback delegate specifies type
// Object.
TaskInfo ti = (TaskInfo)state;

//Deserialize the workspace connection properties.
IXMLSerializer xmlSerializer = new XMLSerializerClass();
object obj = xmlSerializer.LoadFromString(ti.InputRasterWSConnectionProps, null, null);

IPropertySet workspaceProperties = (IPropertySet)obj;
...
}
Synchronizing execution of concurrent running threads
In many cases, you have to synchronize the execution of concurrently running threads. Normally, you have to wait for one or more threads to finish their tasks, signal a waiting thread to resume its task when a certain condition is met, test whether a given thread is alive and running, change a thread priority, or give some other indication.
 
In .NET, there are several ways to manage the execution of running threads. The main classes available to help thread management are as follows:
 
The following code example, also in the Multithreaded raster subset sample, extends what was covered in the previous section. It uses the ManualResetEvent and the WaitHandle classes to wait for multiple threads to finish their tasks. In addition, it demonstrates using the AutoResetEvent class to block running threads from accessing a block of code and to signal the next available thread when the current thread had completed its task.
 

[C#]
/// <summary>
/// Class used to pass the task information to the working thread.
/// </summary>
public class TaskInfo
{
//Signal the main thread that the thread has finished its task.
private ManualResetEvent m_doneEvent;
...
public TaskInfo(int BandID, ManualResetEvent doneEvent)
{
   m_bandID = BandID;
   m_doneEvent = doneEvent;
}
...
public ManualResetEvent DoneEvent
{
   get { return m_doneEvent;  }
   set { m_doneEvent = value; }
}
}



//Block access to the shared resource (raster dataset).
private static AutoResetEvent m_autoEvent = new AutoResetEvent(false);

...
public override void OnMouseDown(int Button, int Shift, int X, int Y)
{ 
    ...
// Run the subset thread that will spin off separate subset tasks. By default, this thread will run as MTA.
// This is needed since to use WaitHandle.WaitAll(), the call must be made
// from MTA.
}

/// <summary>
/// Main subset method.
/// </summary>
private void SubsetProc()
{
...
//Create ManualResetEvent to notify the main threads that
//all ThreadPool threads are done with their tasks.
ManualResetEvent[] doneEvents = new ManualResetEvent[m_intBandCount];

//Create a ThreadPool task for each band of the input raster.
//Each task will subset a different raster band. 
    //All information required for subsetting the raster band will be passed to the ThreadPool 
task by the user-defined class TaskInfo.
for (int i = 0; i < m_intBandCount; i++)
{
   //Create the ManualResetEvent field for the task to 
       // signal the waiting thread that the task had been completed.
   doneEvents[i] = new ManualResetEvent(false);

   TaskInfo ti = new TaskInfo(i, doneEvents[i]);
   ...
   // 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);
}

//Set the state of the event to signaled to allow one or more of the waiting threads to proceed.
m_autoEvent.Set();

// Wait for all threads to complete their task…
WaitHandle.WaitAll(doneEvents);
...         
}

/// <summary>
/// Subset task method.
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
// The state object must be cast to the correct type, because the
// signature of the WaitOrTimerCallback delegate specifies type
// Object.
TaskInfo ti = (TaskInfo)state;
...

//Lock all other threads to get exclusive access.
m_autoEvent.WaitOne();

//Insert code containing your threading logic here.

//Signal the next available thread to get write access.
m_autoEvent.Set();

//Signal the main thread that the thread has finished its task.
ti.DoneEvent.Set();
}
Sharing a managed type across multiple threads
Sometimes your .NET application’s underlying data structure will be a managed object such as a DataTable or HashTable. These .NET managed objects allow you to share them across multiple threads such as a data fetching thread and a main rendering thread. However, you should consult the MSDN Web site to verify whether an item is thread safe. In many cases, an object is thread safe for reading but not for writing. Some collections implement a Synchronized method, which provides a synchronized wrapper around the underlying collection.
 
In cases where your object is being accessed from more than one thread, you should acquire an exclusive lock according to the Thread Safety section in MSDN regarding this particular object. Acquiring such an exclusive lock can be done using one of the synchronization methods described in the previous section or by using a lock statement, which marks a block as a critical section by obtaining a mutual-exclusive lock for a given object. It ensures that if another thread attempts to access the object, it will be blocked until the object is released (exits the lock).
 
The following screen shot demonstrates sharing a DataTable by multiple threads. First, check the DataTable Class on the MSDN Web site to verify if it is thread safe.
 
 
 
On that page, check the section on Thread Safety, where it says, "This type is safe for multithreaded read operations. You must synchronize any write operations."
 
This means that reading information out of the DataTable is not a problem, but you must prevent other threads access to the table when you are about to write to it. The following code example shows how to block those other threads:
 

[C#]
private DataTable m_locations = null;
...

DataRow rec = m_locations.NewRow();
rec["ZIPCODE"] = zipCode;  //ZIP Code 
rec["CITYNAME"] = cityName; //City name

//Lock the table and add the new record.
lock(m_locations)
{
  m_locations.Rows.Add(rec);
}
Updating the UI from a background thread
In most cases where you are using a background thread to perform lengthy operations, you want to report to the user the progress, status, errors, or other information related to the task performed by the thread. This can be done by updating a control on the application's UI. However, in Windows, forms controls are bound to a specific thread (generally the main thread) and are not thread safe. As a result, you must delegate, and thus marshal, any call to a UI control to the thread to which the control belongs. The delegation is done through calling the Control.Invoke method, which executes the delegate on the thread that owns the control's underlying window handle. To verify whether a caller must call an invoke method, you can use the property Control.InvokeRequired. You must make sure that the control's handle was created before attempting to call Control.Invoke or Control.InvokeRequired.
 
The following code example, an excerpt from the RSS Weather Layer sample, demonstrates reporting a background task's progress into a user form.
 
  1. In the user form, declare a delegate through which you will pass the information to the control.

[C#]
public class WeatherItemSelectionDlg : System.Windows.Forms.Form
{
  private delegate void AddListItmCallback(string item);
  …       
  1. In the user form, set the method to update the UI control. Notice the call to Invoke. The method must have the same signature as the previously declared delegate:

[C#]
//Make thread-safe calls to Windows Forms Controls.
private void AddListItemString(string item)
{
// InvokeRequired 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);
}
}
  1. On the background thread, implement the method that will use the delegate and pass over the message to be displayed on the user form.

[C#]
private void PopulateSubListProc()
{
//Insert code containing your threading logic here.

//Add the item to the list box.
frm.AddListItemString(data needed to update the UI control, string in this case);
}
  1. Write the call that launches the background thread itself, passing in the method written in Step 3.

[C#]
//Generate a thread to populate the list with items that match the selection criteria.
Thread t = new Thread(new ThreadStart(PopulateSubListProc));
t.Start();
Calling ArcObjects from a thread other than the main thread
In many multithreading applications, you will need to make calls to ArcObjects from different running threads. For example, you might have a background thread that gets a response from a Web service, which, in turn, should add a new item to the map display, change the map extent, or run a geoprocessing (GP) tool to perform some type of analysis.
 
A very common case is calling ArcObjects from a timer event handler method. Timer's Elapsed event is raised on a ThreadPool task, a thread that is not the main thread. Yet it needs to use ArcObjects, which looks like it would require cross-apartment calls. However, this can be avoided by treating the ArcObjects component as if it were a UI control and using Invoke to delegate the call to the main thread where the ArcObjects component is created. Thus, no cross-apartment calls are made.
 
The ISynchronizeInvoke interface includes methods Invoke, BeginInvoke, and EndInvoke. Implementing these methods yourself can be a daunting task. Instead, you should either have your class directly inherit from System.Windows.Forms.Control or you should have a helper class that inherits Control. Either option will provide a simple and efficient solution for invoking methods.
 
The following code example employs a user-defined InvokeHelper class to invoke a timer’s elapsed event handler to recenter the map's visible bounds and set the map's rotation. Note that some of the application logic must be done on the InvokeHelper class in addition to the user-defined structure that is being passed by the method delegate.
 

[C#]
/// <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);     
   ...
}
Multithreading with geoprocessing
To use GP in an asynchronous/multithreaded application, use the asynchronous execution pattern exposed in ArcGIS Server 9.2 using GP Services. This pattern enables the desktop working with GP in an asynchronous execution mode.


See Also:

Sample: Multithreaded raster subset
Sample: RSS Weather Layer
Sample: RSS Weather Layer 3D