Thursday, October 16, 2014

Multi-Threading and ArcGIS

As our processors get stronger and stronger with multi cores, software steps up to fully utilize these cores in order to be more effective at processing. Thus born the multi threading concept where a software can spawn threads do extra work independently from the parent process.

Although ArcGIS for Desktop up until now, (10.2.2) is not fully Multi-threaded, i.e. not all its functionalities are delegated to threads for execution, you still can write your own full Multi-threaded customizations on top of it.

There is a catch though.

For instance, below is an ArcMap customization code with two command buttons, both draw  marker element on the map. However, one does it with threading, the other without threading.



 
       
    '--------------------- Threading Example ---------------------
    Public Overrides Sub OnClick()
        'TODO: Add CreateCircleThreading.OnClick implementation
        Dim pThread As New Threading.Thread(Sub() DrawCircle(RGB(255, 0, 0)))
        pThread.Start()
    End Sub



 '--------------------- No Threading Example ---------------------
    Public Overrides Sub OnClick()
        'TODO: Add CreateCircleNoThreading.OnClick implementation
        'draw a red circle in the center
        DrawCircle(RGB(0, 0, 255))

    End Sub
 


    Public Sub DrawCircle(lRGBColor As Long)
        Dim pMxdoc As IMxDocument = m_application.Document

        Dim pMarkerElement As IMarkerElement = New MarkerElement
        Dim pMarkerSymbol As IMarkerSymbol = New SimpleMarkerSymbol
        Dim pColor As IColor = New RgbColor()
        pColor.RGB = lRGBColor

        pMarkerSymbol.Color = pColor
        pMarkerSymbol.Size = 50

        pMarkerElement.Symbol = pMarkerSymbol

        Dim pElement As IElement = pMarkerElement

        pElement.Geometry = pMxdoc.ActiveView.Extent.UpperRight

        pMxdoc.ActiveView.GraphicsContainer.AddElement(pElement, 0)

        pMxdoc.ActiveView.Extent.Expand(100, 100, False)
        pMxdoc.ActiveView.Refresh()

    End Sub

 
 

Obviously both will give you the same result, however, the threading option is better if you want to free up your cpu to do more important things, and of course if your operation is much more heavier than drawing a simple element. You want to through the bulk work on a thread to do that.

There is however, something interesting. The threading code I just gave you? it doesn't work. Yes its broken. The reason is that ArcGIS and ArcObjects (especfiically ICommand interface the command button for ArcMap) do not support invoking threads that owns its controls. So you will call a new thread (which is isolated from ArcMap) and then that thread will tries to draws an element on ArcMap, which she (i don't know why I think its a woman) doesn't have access to throwing an error in the process. So this is a classic problem called cross-thread violation.

Solving this problem is easy, you need to force the class that implements ICommand to have an invoke capability, so you have to make your ArcMap button into a Windows Form which allows invoking into the same thread. At least this is how I solved it, if you guys have another method please do share. Below is the code


 

Public NotInheritable Class CreateCircleThreading
    Inherits Form
    Implements ICommand
...
...

    Public Sub OnCommandClick() Implements ICommand.OnClick
        'TODO: Add CreateCircleThreading.OnClick implementation

        Me.Invoke(Sub() DrawCircle(RGB(255, 0, 0)))

        'Dim pThread As New Threading.Thread(Sub() DrawCircle(RGB(255, 0, 0)))
        'pThread.Start()
    End Sub


    Public Sub DrawCircle(lRGBColor As Long)
        Dim pMxdoc As IMxDocument = m_application.Document

        Dim pMarkerElement As IMarkerElement = New MarkerElement
        Dim pMarkerSymbol As IMarkerSymbol = New SimpleMarkerSymbol
        Dim pColor As IColor = New RgbColor()
        pColor.RGB = lRGBColor

        pMarkerSymbol.Color = pColor
        pMarkerSymbol.Size = 50

        pMarkerElement.Symbol = pMarkerSymbol

        Dim pElement As IElement = pMarkerElement

        pElement.Geometry = pMxdoc.ActiveView.Extent.UpperRight

        pMxdoc.ActiveView.GraphicsContainer.AddElement(pElement, 0)

        pMxdoc.ActiveView.Extent.Expand(100, 100, True)
        pMxdoc.ActiveView.Refresh()

    End Sub




One more thing, our command is not exactly a form until you create a handle for it. So we have to add this line in the new constructor. Showing and hiding the form will take care of this.


            'force to create handle (VERY IMPORTANT)
            Me.Show()
            Me.Hide()


You can download source working code here