Executing GP Tasks from different thread prevents events from being triggered

2929
8
Jump to solution
02-09-2012 10:10 AM
Labels (1)
BKuiper
Occasional Contributor III
Hi,

I have attached an example program that demonstrates that the GP Task events will only be called when the GP Task (SubmitJobAsync) is being called from the main UI Thread.

If you remove the dispatcher.Invoke(); around the GP SubmitJobAsync executing the events of the new threads will not be executed. I would say this is a bug.

Hope this will help you improve the code.
0 Kudos
1 Solution

Accepted Solutions
DanielO_Connor
New Contributor
We looked further into the code at Foliage and we see why the notifications do not occur when we SubmitJobAsync on a worker thread.
Here is synopsis of what was found and a possible solution:

??? ESRI code is notified async by the MS WebClient library code (for http traffic to server)
??? In this callback code, ESRI then notify clients (us) via WPF Dispatching (in fact DispatcherTimer)
??? ESRI cannot be sure which thread their code will be called back on ??? depends on Sync Context Rules
??? Current ESRI code defaults to Dispatch on the "current thread", i.e. whatever thread they are called ??? this is not sufficient:
      DispatcherTimer timer = new DispatcherTimer()
??? ESRI needs to use the override API (constructor) that allows passing in the correct Dispatcher:
      public DispatcherTimer(DispatcherPriority priority, System.Windows.Threading.Dispatcher dispatcher)
??? ESRI also needs to grab and cache away the Dispatcher during Submit()  so they can notify on the correct thread

View solution in original post

0 Kudos
8 Replies
BKuiper
Occasional Contributor III
The problem is related to the DispatcherTimer in submitJob_Completed() in Geoprocessor. The Tick event is never triggered by the DispatcherTimer when called from the "background" thread.
0 Kudos
MichaelBranscomb
Esri Frequent Contributor
Hi,

Thanks for the post, and apologies for the delay in replying. We're still looking into this - there may be a bug - but in the meantime there are a couple of alternative approaches you can take:

#1. Call SubmitJobAsync on the UI thread - the overhead is minimal and should still provide a good user experience.

#2. Keep the separate thread approach and disable the automatic JobStatus checks (which normally result in JobCompleted) by calling Geoprocessor.CancelJobStatusUpdates() and then perform the checks manually using Geoprocessor.CheckJobStatusAsync() which will result in the Geoprocessor.StatusUpdated event being raised. The default interval for the automatic checks is 5 seconds (changed via the Geoprocessor.UpdateDelay) but if you're doing this manually the interval is up to you. I've modified your code to show a quick example which will give feedback whether it's running on a background thread or on the UI thread.

public MainWindow()
{
    InitializeComponent();

    this.Completed += (sender, result) =>
    {
        Application.Current.Dispatcher.Invoke(new Action(() =>
        {
            MessageBox.Show("Result: " + result.Token);
        }), null);
    };

    LocalGeoprocessingService.GetServiceAsync(gpkLocation, GPServiceType.SubmitJob, localGpService => 
    {
        if (localGpService.Error != null)
            return;
        lgs = localGpService;
        Thread thread = new Thread(new ParameterizedThreadStart(this.ExecuteGPTask));
        thread.Start("Background Thread");
        this.ExecuteGPTask("UI Thread"); 
    });
}

public void ExecuteGPTask(object token)
{
    //Set up parameters
    List<GPParameter> parameters = new List<GPParameter>();
    string input = "5+5";
    GPString inputParam = new GPString("input", input);
    parameters.Add(inputParam);

    // Check local GP service is running and there are >=1 tasks
    if (this.lgs.Status != LocalServiceStatus.Running || this.lgs.Tasks.Count == 0)
        return;

    // Assumes only one task in GP Package (GPK)
    string taskUri = this.lgs.Tasks[0].Url;

    // Create a new Geoprocessor task
    Geoprocessor gpContour = new Geoprocessor(taskUri);
    gpContour.Token = token.ToString();

    // Create a timer to schedule CheckJobStatusAsync calls
    System.Timers.Timer timer = null;

    // Handler for StatusUpdated event (in response to CheckJobStatusAsync)
    gpContour.StatusUpdated += (s, e) =>
    {
        Geoprocessor geoprocessingTask = s as Geoprocessor;
        switch (e.JobInfo.JobStatus)
        {
            case esriJobStatus.esriJobSubmitted:
                // Disable automatic status checking.
                geoprocessingTask.CancelJobStatusUpdates(e.JobInfo.JobId);
                break;
            case esriJobStatus.esriJobSucceeded:
                if (this.Completed != null)
                {
                    this.Completed(this, new MyEventArgs() { Token = gpContour.Token });
                    if (timer != null)
                    {
                        timer.Stop();
                        timer.Dispose();
                    }
                }
                // Get the results.
                // geoprocessingTask.GetResultDataAsync(e.JobInfo.JobId, "<parameter name>");
                break;
            case esriJobStatus.esriJobFailed:
            case esriJobStatus.esriJobTimedOut:
                MessageBox.Show("operation failed");
                break;
        }
    };
    JobInfo jobInfo = gpContour.SubmitJob(parameters);
    timer = new System.Timers.Timer(3000);
    timer.Elapsed += (s, e) =>
    {
        gpContour.CheckJobStatusAsync(jobInfo.JobId);
    };
    timer.Start();
}



Cheers

Mike
0 Kudos
BKuiper
Occasional Contributor III
Thanks for your great feedback. We already (temporarily) switch over to executing it on the (main) UI thread.
0 Kudos
DanielO_Connor
New Contributor
We looked further into the code at Foliage and we see why the notifications do not occur when we SubmitJobAsync on a worker thread.
Here is synopsis of what was found and a possible solution:

??? ESRI code is notified async by the MS WebClient library code (for http traffic to server)
??? In this callback code, ESRI then notify clients (us) via WPF Dispatching (in fact DispatcherTimer)
??? ESRI cannot be sure which thread their code will be called back on ??? depends on Sync Context Rules
??? Current ESRI code defaults to Dispatch on the "current thread", i.e. whatever thread they are called ??? this is not sufficient:
      DispatcherTimer timer = new DispatcherTimer()
??? ESRI needs to use the override API (constructor) that allows passing in the correct Dispatcher:
      public DispatcherTimer(DispatcherPriority priority, System.Windows.Threading.Dispatcher dispatcher)
??? ESRI also needs to grab and cache away the Dispatcher during Submit()  so they can notify on the correct thread
0 Kudos
SteveVidal
New Contributor II
Very good thread, thanks a lot for that. Saved me a lot of headaches.

On a slightly different topic, if\when Esri decides to fix that bug, the same behaviour (UI thread affinity) is also apparent with the latest versions of ESRI.ArcGIS.Client.Tasks.EditTask() which used to work fine inside a WCF service...
0 Kudos
BKuiper
Occasional Contributor III
i noticed this hasn't been resolved in 10.1.1, furthermore, the use of DispatcherTimer in the Esri code prevents it from working in a console application, as there is no Application Dispatcher created. So highly inconvenient.

I was trying to make a proof of concept for an existing console application before migrating it to WPF and stumbled on this bug again.
0 Kudos
MichaelBranscomb
Esri Frequent Contributor
Hi,

For your console app you should just need to create and set a new SynchronizationContext e.g.

using System;
using System.Threading;
using System.Windows.Threading;
using ESRI.ArcGIS.Client;

namespace UpdateGraphicsConsole
{
    class Program
    {
        static string _url = "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Wildfire/FeatureServer/0";

        // Make sure the main thread is an STA thread
        [STAThread]
        static void Main(string[] args)
        {
            // The SynchronizationContext of the main thread is null.
            // Create and set a new DispatcherSynchronizationContext.
            var syncCtx = new DispatcherSynchronizationContext();
            SynchronizationContext.SetSynchronizationContext(syncCtx); 

            // Get the current dispatcher to check thread access.
            Dispatcher d = Dispatcher.CurrentDispatcher;
            
            // Create a FeatureLayer
            FeatureLayer fl1 = new FeatureLayer
            {
                Url = _url,
                ID = "23",
                Mode = FeatureLayer.QueryMode.Snapshot,
                AutoSave = false,
            };

            // Report which thread the FeatureLayer was created on
            Console.WriteLine("FeatureLayer created on Thread " + d.Thread.ManagedThreadId.ToString());

            // Register a handler for the Initialized event
            fl1.Initialized += (sender, eventArgs) =>
            {
                // Report which thread the event fired on (this was always a different thread until the DispatcherSynchronizationContext was set
                Console.WriteLine("Initialized event handled on Thread " + Dispatcher.CurrentDispatcher.Thread.ManagedThreadId.ToString());

                // Get the FeatureLayer
                FeatureLayer fl2 = sender as FeatureLayer;

                // Get the FeatureLayer dispatcher
                Dispatcher d1 = fl2.Dispatcher;

                // Report the thread we're invoking the Update call on (this was always the original thread)
                Console.WriteLine("Invoking Update on Thread " + d1.Thread.ManagedThreadId.ToString());

                // CheckAccess always returned false because this code was running on a different thread until the DispatcherSynchronizationContext was set
                if (fl2.Dispatcher.CheckAccess())
                {
                    fl2.Update();
                }
                else
                    d1.BeginInvoke(
                        DispatcherPriority.Normal,
                        new Action(delegate()
                            {
                                fl2.Update();
                            }));
            };

            // Register a handler for the UpdateCompleted event and call SaveEdits()
            fl1.UpdateCompleted += (sender, e) =>
            {
                // Get the FeatureLayer
                FeatureLayer fl3 = sender as FeatureLayer;

                // Get the FeatureLayer Dispatcher
                Dispatcher d2 = fl3.Dispatcher;

                // Update the graphic attribute (or perform any other edits)
                fl3.Graphics[0].Attributes["description"] = Guid.NewGuid().ToString();

                // Write out the new value 
                Console.WriteLine(
                    "Updated Graphic ObjectID " 
                    + fl3.Graphics[0].Attributes["objectid"] 
                    + " with Description: " 
                    + fl3.Graphics[0].Attributes["description"]); 
                
                // Call CheckAccess to confirm whether we need to post back to the original thread
                // Then call SaveEdits
                if (fl3.Dispatcher.CheckAccess())
                {
                    fl3.SaveEdits();
                }
                else
                    d2.BeginInvoke(
                    DispatcherPriority.Normal,
                    new Action(() => fl3.SaveEdits()));
            };

            // Register a handler for the EndSaveEdits event and write out the status
            fl1.EndSaveEdits += (sender, e) =>
            {
                Console.WriteLine(e.Success ? "Success" : "Fail");
                // Can check results online at query endpoint (by description value, objectid, etc)
                // http://sampleserver6.arcgisonline.com/arcgis/rest/services/Wildfire/FeatureServer/0/query
            };

            // Register a handler for the InitializationFailed event
            fl1.InitializationFailed += (sender, e) =>
            {
                // Report error
                FeatureLayer fl4 = sender as FeatureLayer;
                Console.WriteLine(fl4.InitializationFailure.Message);
            };

            // Register a handler for the UpdateFailed event
            fl1.UpdateFailed += (sender, e) =>
            {
                // Report failure to update layer
                Console.WriteLine(e.Error);
            };

            // Register a handler for the SaveEditsFailed event
            fl1.SaveEditsFailed += (sender, e) =>
            {
                // Report failure
                Console.WriteLine(e.Error);
            };


            Console.WriteLine("Calling Initialize on Thread " + d.Thread.ManagedThreadId.ToString());

            // Call Initialize method (in a map/UI scenario this call would be handled by the Map).
            fl1.Initialize();

            // Need to call Dispatcher.Run 
            Dispatcher.Run();
            
        }
    }
}


Cheers
0 Kudos
BKuiper
Occasional Contributor III
Hi,

For your console app you should just need to create and set a new SynchronizationContext e.g.



Thanks Mike, I also implemented this workaround for now. Any plans for changing the code so you can call it from any thread instead of the UI thread ?
0 Kudos