2

The thing i am interested in is why do we need to call InvokeOnMainThread while this would be the main intent and responsibility of TaskScheduler.FromCurrentSynchronizationContext()?.

I am using the TPL in Monotouch for an iPhone app to do some background tasks and update the UI via a reporter class. But it seems that TaskScheduler.FromCurrentSynchronizationContext() is not synchronizing to the UI thread as what you would expect. At this time I managed to get it working (but still feels wrong) by using InvokeOnMainThread as described by the Threading topic at Xamarin's site.

I also found a reported (similar) bug at BugZilla that seems to be resolved.. and another threading question about the preferred way of using background threads in MonoTouch.

Below is the code snippet to illustrate my question and to show the behaviour.

    private CancellationTokenSource cancellationTokenSource;

    private void StartBackgroundTask ()
    {
        this.cancellationTokenSource = new CancellationTokenSource ();
        var cancellationToken = this.cancellationTokenSource.Token;
        var progressReporter = new ProgressReporter ();

        int n = 100;
        var uiThreadId = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine ("Start in thread " + uiThreadId);

        var task = Task.Factory.StartNew (() =>
        {
            for (int i = 0; i != n; ++i) {

                Console.WriteLine ("Work in thread " + Thread.CurrentThread.ManagedThreadId);

                Thread.Sleep (30); 

                progressReporter.ReportProgress (() =>
                {
                    Console.WriteLine ("Reporting in thread {0} (should be {1})",
                        Thread.CurrentThread.ManagedThreadId,
                        uiThreadId);

                    this.progressBar.Progress = (float)(i + 1) / n;
                    this.progressLabel.Text = this.progressBar.Progress.ToString();

                });
            }

            return 42; // Just a mock result
        }, cancellationToken);

        progressReporter.RegisterContinuation (task, () =>
        {
            Console.WriteLine ("Result in thread {0} (should be {1})",
                Thread.CurrentThread.ManagedThreadId,
                uiThreadId);

            this.progressBar.Progress = (float)1;
            this.progressLabel.Text = string.Empty;

            Util.DisplayMessage ("Result","Background task result: " + task.Result);

        });
    }

And the reporter class has these methods

    public void ReportProgress(Action action)
    {
        this.ReportProgressAsync(action).Wait();
    }
    public Task ReportProgressAsync(Action action)
    {
        return Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
    }
    public Task RegisterContinuation(Task task, Action action)
    {
        return task.ContinueWith(() => action(), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
    }
    public Task RegisterContinuation<TResult>(Task<TResult> task, Action action)
    {
        return task.ContinueWith(() => action(), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
    }

The results in the Application output window will be:

Start in thread 1
Work in thread 6
Reporting in thread 6 (should be 1)
Work in thread 6
Reporting in thread 6 (should be 1)
...
Result in thread 1 (should be 1)

As you can see 'Work in thread 6' is fine. Reporting is also on thread 6, this is wrong. The funny part is that the RegisterContinuation does its reporting in thread 1!!!


PROGRESS: I still haven't figured this one out.. Anyone?

Community
  • 1
  • 1
Remco Koedoot
  • 239
  • 4
  • 13

2 Answers2

1

I think the problem is that you're retrieving the task scheduler from within the ProgressReporter class by doing TaskScheduler.FromCurrentSynchronizationContext().

You should pass a task scheduler into the ProgressReporter and use that one instead:

public class ProgressReporter
{
    private readonly TaskScheduler taskScheduler;

    public ProgressReporter(TaskScheduler taskScheduler)
    {
        this.taskScheduler = taskScheduler;
    }

    public Task RegisterContinuation(Task task, Action action)
    {
        return task.ContinueWith(n => action(), CancellationToken.None,
            TaskContinuationOptions.None, taskScheduler);
    }

    // Remaining members...
}

By passing the task scheduler taken from the UI thread into the progress reporter, you're sure that any reporting is done on the UI thread:

TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
ProgressReporter progressReporter = new ProgressReporter(uiScheduler);
Sandor Drieënhuizen
  • 6,310
  • 5
  • 37
  • 80
  • Thanks Sandor but I tried this and it didn't work. It gives the same result in the console output and no progress is shown in the UI. As mentioned in my question, the 'ContinueWith()' seems to work without having to call 'InvokeOnMainThread()'. – Remco Koedoot Mar 21 '12 at 00:50
  • I have to admit that I tried this on Windows only, using a WinForms project. On Windows, a console project doesn't even allow `TaskScheduler.FromCurrentSynchronizationContext()`, it causes an exception to be thrown: 'The current SynchronizationContext may not be used as a TaskScheduler.' – Sandor Drieënhuizen Mar 21 '12 at 08:51
  • @RemcoKoedoot, could you show your updated code (as an edit to the question) which uses this technique? – Matt Smith May 10 '13 at 21:27
0

What version of MonoTouch are you using and what is the output of: TaskScheduler.FromCurrentSynchronizationContext ().GetType ().ToString (). It should be a class of type UIKitSynchronizationContext if the context has been correctly registered. If this is a context of the correct type, could you do a quick test by directly calling the Post and Send methods on the context to see if they end up executing on the correct thread. You'll need to spin up a few threadpool threads to test that it works correctly, but it should be reasonably simple.

Alan
  • 409
  • 3
  • 3
  • Hi Alan, I am using MonoDevelop 2.8.6.5 and MonoTouch 5.2.10.1332177242. I didn't find the time yet to check what you suggested but I will certainly do. I will post the results. – Remco Koedoot Mar 22 '12 at 09:42
  • That call returns the type System.Threading.Tasks.SynchronizationContextScheduler. But when I run SynchronizationContext.Current then it is of type UIKitSynchronizationContext. – Remco Koedoot Mar 25 '12 at 00:34
  • I did the test with the Post and Send on SynchronizationContext.Current from the queued threads but the result was the same as before. When I took the SynchronizationContext.Current out of the For loop and called Post from within the queued threads on that Context, it worked as expected... So I am still puzzled why TaskScheduler is not posting on the MainThread, even if I give it as an parameter along the stack. – Remco Koedoot Mar 25 '12 at 00:45