Task Continuation on UI Thread

Task continuation on UI thread

Call the continuation with TaskScheduler.FromCurrentSynchronizationContext():

    Task UITask= task.ContinueWith(() =>
{
this.TextBlock1.Text = "Complete";
}, TaskScheduler.FromCurrentSynchronizationContext());

This is suitable only if the current execution context is on the UI thread.

Task continuation on UI thread, when started from background thread

You need to get a reference to TaskScheduler.FromCurrentSynchronizationContext() from the UI thread and pass it to the continuation.

Similar to this.
http://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/

private void Form1_Load(object sender, EventArgs e)
{
// This requires a label titled "label1" on the form...
// Get the UI thread's context
var context = TaskScheduler.FromCurrentSynchronizationContext();

this.label1.Text = "Starting task...";

// Start a task - this runs on the background thread...
Task task = Task.Factory.StartNew( () =>
{
// Do some fake work...
double j = 100;
Random rand = new Random();
for (int i = 0; i < 10000000; ++i)
{
j *= rand.NextDouble();
}

// It's possible to start a task directly on
// the UI thread, but not common...
var token = Task.Factory.CancellationToken;
Task.Factory.StartNew(() =>
{
this.label1.Text = "Task past first work section...";
}, token, TaskCreationOptions.None, context);

// Do a bit more work
Thread.Sleep(1000);
})
// More commonly, we'll continue a task with a new task on
// the UI thread, since this lets us update when our
// "work" completes.
.ContinueWith(_ => this.label1.Text = "Task Complete!", context);
}

TaskContinuationOptions for completing on the UI Thread

Answering my own question, after comments from @PeterBons.

Additonal code to add to answer at https://stackoverflow.com/a/15120092/858282

/// <summary>
/// as per http://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/
/// from UI, store and pass 'TaskScheduler.FromCurrentSynchronizationContext()' into this method to avoid the
/// need for 'Invoke' to avoid cross threading UI exceptions
/// </summary>
public Task<T> QueueTask<T>(Func<T> work, TaskScheduler tScheduler, CancellationToken cancelToken = default(CancellationToken))
{
lock (key)
{
var task = previousTask.ContinueWith(t => work()
, cancelToken == default(CancellationToken) ? CancellationToken.None : cancelToken
, TaskContinuationOptions.None
, tScheduler);
previousTask = task;
return task;
}
}

How to get a Task to return to the UI thread in WPF

You can associate a TaskScheduler with the continuation task to force the delegate that sets the Content and Visibility properties to be set on the UI thread:

var overlayTask = Task.Delay(250, token).ContinueWith(_ => {
Overlay.Content = overlayMessage;
Overlay.Visibility = Visibility.Visible;
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());

Task.ContinueWith callback thread

This depends on the scheduler that is associated with the continuation. By default, task continuations are scheduled through the Current scheduler, being the TaskScheduler associated with the currently executing task. When ContinueWith is not called from within a task, Current will return the Default scheduler, which is the default TaskScheduler instance provided by the .NET Framework, and which will schedule your tasks on the thread pool.

If you want to influence this behaviour, you can call one of the ContinueWith overloads that takes a TaskScheduler parameter. A common pattern is to pass TaskScheduler.FromCurrentSynchronizationContext() when creating continuations on the UI thread, as this would cause the continuation to be dispatched back onto the UI thread when executed.

Edit: In reply to your comment: The deadlock may arise if you spawn a child task (intended to run on the thread pool) from a continuation running on the UI thread. In such cases, the child task will inherit the task scheduler from the parent task, which would be bound to the UI thread, causing the child task to run on the UI thread too.

Task.Factory.StartNew(() =>
{
// Do background work.
}).ContinueWith(_ =>
{
// Update UI, then spawn child task to do more background work...
Task.Factory.StartNew(() =>
{
// ...but child task runs on UI thread!
});
},
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());

To resolve this, you can use the StartNew overload that accepts a TaskScheduler parameter for the child task, and pass TaskScheduler.Default to it:

    // Update UI, then spawn child task to do more background work...
Task.Factory.StartNew(() =>
{
// ...and child task now runs on the thread pool.
},
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default);

Task continuation blocking UI thread

You're explicitly telling the continuation to run in the current synchronization context by specifying

TaskScheduler.FromCurrentSynchronizationContext()

So yes, that will block the UI thread because it's running in the UI thread assuming this overall method is called in a UI thread. The original task will run in the background, but your continuations are both scheduled to run in the UI thread. If you don't want to do that, don't use that task scheduler.

ContinueWith a Task on the Main thread

Will it wait until the main thread is currently finished doing whatever it is doing, or will it get called immediately after the asynchronous call is complete?

When the asynchronous call completes, the continuation will be scheduled. The effect of that scheduling depends on the scheduler (of course) but for a "normal" WPF or Windows Forms message loop, I'd expect it to be scheduled in a similar way to a call to Control.BeginInvoke or Dispatcher.BeginInvoke - in other words, when the "main" thread has finished the rest of the tasks which had been scheduled before this one.

I wouldn't expect the main thread to magically stop what it's doing and execute the continuation. It could make the continuation a high-priority task in its queue, however.

Task ContinueWith() Not Updating Cursor on UI Thread

The cursor change needs to be done on the UI thread. You can either use the the overload of ContinueWith that takes a task scheduler:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); 

Task.Factory
.StartNew(() => PerformMigration(legacyTrackerIds))
.ContinueWith(_ => this.Cursor = Cursors.Arrow, uiScheduler);

Or use the Dispatcher.Invoke method:

Task.Factory
.StartNew(() => PerformMigration(legacyTrackerIds))
.ContinueWith(_ => { Dispatcher.Invoke(() => { this.Cursor = Cursors.Arrow; }); });

Task continuation to maintain UI thread responsiveness

I would use the Event Based Asynchronous-TYPE Pattern for this. A simplified version of the code I use to spin-off method onto a background thread using TPL is below

private void TaskSpin(TaskScheduler uiScheduler, 
Func<TaskScheduler, object[], bool> asyncMethod,
object[] methodParameters)
{
try
{
Task asyncTask = Task.Factory.StartNew<bool>(() =>
asyncMethod(uiScheduler, methodParameters));

// Callback for finish/cancellation.
asyncTask.ContinueWith(task =>
{
// Check task status.
switch (task.Status)
{
// Handle any exceptions to prevent UnobservedTaskException.
case TaskStatus.RanToCompletion:
if (asyncTask.Result)
UpdateUI(uiScheduler, "OK");
else
{
string strErrComplete = "Process failed.";
UpdateUI(uiScheduler, strErrComplete);
}
break;
case TaskStatus.Faulted:
string strFatalErr = String.Empty;
UpdateUI(uiScheduler, "Fatal Error);
if (task.Exception != null)
strFatalErr = task.Exception.InnerException.Message;
else
strFatalErr = "Operation failed";
MessageBox.Show(strFatalErr);
break;
}
asyncTask.Dispose();
return;
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception eX)
{
Utils.ErrMsg(eX.Message);
}
}

I hope this helps.

Edit. Note, in the above uiScheduler is the TaskScheduler for the UI Thread. That is

TaskSheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

Task.ContinueWith from calling thread

You can use TaskContinuationOptions.ExecuteSynchronously:

ContinueWith(() => { ... }, TaskContinuationOptions.ExecuteSynchronously);

ExecuteSynchronously tells it to try and run the continuation on whatever thread was last executing the antecedent task. And in the case of a continuation executing after a Task.Factory.StartNew call, that thread would be a thread pool thread.

But you have to note that this is merely a hint and the framework won't always honor your request. So you shouldn't structure your code so that running on the same thread becomes a necessity.



Related Topics



Leave a reply



Submit