Accessing UI Controls in Task.Run with Async/Await on Winforms

Using await Task.Run(() = someMethodAsync()) vs await someMethodAsync() in a UI based app

By default, when you await an incomplete operation, the sync-context is captured (if present). Then, when reactivated by the completion of the async work, that captured sync-context is used to marshal the work. Many scenarios don't have a sync-context, but UI applications such as winforms and WPF do, and the sync-context represents the UI thread (basically: this is so that the default behaviour becomes "my code works" rather than "I get cross-thread exceptions when talking to the UI in my async method"). Essentially, this means that when you await Task.Delay(1000) from a UI thread, it pauses for 1000ms (releasing the UI thread) but then resumes on the UI thread. This means that you "Tons of work to do in here" happens on the UI thread, blocking it.

Usually, the fix for this is simple: add .ConfigureAwait(false), which disables sync-context capture:

await Task.Delay(1000).ConfigureAwait(false);

(usually: add this after all await in that method)

Note that you should only do this when you know that you don't need what the sync-context offers. In particular: that you aren't going to try to touch the UI afterwards. If you do need to talk to the UI, you'll have to use the UI dispatcher explicitly.

When you used Task.Run(...), you escaped the sync-context via a different route - i.e. by not starting on the UI thread (the sync-context is specified via the active thread).

Using async with c# windows form

Don't block on async code. Instead, use await:

private async void searchButton_Click(object sender, EventArgs e)
{
await RunAsync();
...
}

Side note: this code is using async void because the method is an event handler. Normally, you would want to avoid async void.

How do I marshal an event from Task.Run back to the UI thread?

Since UpdateSomethingAsync needs to access the UI context, it shouldn't be wrapped in a Task.Run call. (You should very rarely, need to call an async method from Task.Run, usually only if the method is implemented incorrectly and you can't fix it.)

Instead DoSomethingSynchronous should be the thing you call from Task.Run. After all, the purpose of that method is to asynchronously run a synchronous method in a thread pool thread. So only use it for the synchronous method you want run in a thread pool thread, not the (supposedly) asynchronous method that needs to access the UI context.

Await after creating WinForm UI Control within a new Task causes deadlock?

First of all, you should be using Task.ConfigureAwait(false) in every place you don't want await to resume on the current synchronization context. If you did so for your Task.Delay call, you would not have that issue (but most probably a different one).

However if I create the UI Control using Invoke method as shown below it doesn't dead lock. I am curious why? Since the task is not running in UI context, what prevents Task.Delay from resuming in above code?

In fact the way you use it, Task.Delay will try to resume at the whatever the current synchronization context is when you invoke it. And here is the point. Normally the thread pool threads do not have a synchronization context (or use the default one which effective does not do any synchronization). But every System.Windows.Forms.Control by default installs (sets SynchronizationContext.Current to) a WindowsFormsSynchronizationContext in it's constructor. Hence after the call

Program.Container.GetInstance<FinishedRequestItem>()  

the current synchronization context is changed, and then Task.Delay tries to resume on that context, but of course it can't because there is no message pump running on that thread (which is required for WindowsFormsSynchronizationContext to function properly).

In general you should not create UI elements on non UI threads.

Updating GUI from async method

Neither Task nor await give you any guarantees in this respect. You need to consider the context where the task was created, and where the continuation was posted.

If you're using await in a winforms event handler, the synchronization context is captured, and the continuation returns back to the UI thread (in fact, it pretty much calls Invoke on the given block of code). However, if you just start a new task with Task.Run, or you await from another synchronization context, this no longer applies. The solution is to run the continuation on the proper task scheduler you can get from the winforms synchronization context.

However, it should be noted that it still doesn't necessarily mean that async events will work properly. For example, Winforms also uses events for things like CellPainting, where it actually depends on them running synchronously. If you use await in such an event, it's pretty much guaranteed not to work properly - the continuation will still be posted to the UI thread, but that doesn't necessarily make it safe. For example, suppose that the control has code like this:

using (var graphics = NewGraphics())
{
foreach (var cell in cells)
CellPainting(cell, graphics);
}

By the time your continuation runs, it's entirely possible the graphics instance has already been disposed of. It's even possible the cell is no longer part of the control, or that the control itself no longer exists.

Just as importantly, the code might depend on your code changing things - for example, there's events where you set some value in their EventArgs to indicate e.g. success, or to give some return value. Again, this means you can't use await inside - as far as the caller is aware, the function just returned the moment you do the await (unless it completes synchronously).

access all ListView Items from Task.Run() gives this error InvalidOperationException: 'Cross-thread operation not valid

The error is occurring because you are trying to read the items from the ListView from within the Task, which is running on another thread. Instead of reading the items within the Task.Run call, copy them into an array on the line above Task.Run and reference that array instead.

The CopyTo method provides an easy way to copy the ListViewItems to an array. It would look something like this:

// instantiate new array to hold copy of items
var copyOfItems = new ListViewItems[lvwFiles.Items.Count];
// copy items
lvwFiles.Items.CopyTo(copyOfItems, 0);

Now just reference copyOfItems within your Task!

C# Winforms - Why are some controls updated in async event handler, but not all of them?

Thanks to the insightful comments, a working fix to this issue is the following:

// add this to unbind data source before modifying data
myGrid.DataSource = null;

// async method call that modifies data
await MyTask(progress);

Note that just clearing the data bindings did not work in my case:

myGrid.DataBindings.Clear(); // not working

How to provide a feedback to UI in a async method?

First of all, say no to async void methods (Event handlers are exceptions) because exceptions thrown inside it will not be noticed. Instead use async Task and await it.

private async void button1_Click(object sender, EventArgs e)// <--Note the async modifier
{
// almost 15 process
foreach (var process in Processes)
{
// call a async method to process
await ProcessObject(process);
}
}

private async Task ProcessObject(ProcessViewModel process)// <--Note the return type
{
// my code is here with some loops
await Task.Run(()=>
{
//Will be run in ThreadPool thread
//Do whatever cpu bound work here
});

//At this point code runs in UI thread
process.Progress++;

// feedback to UI
UpdateRow(process);
}

However, this will start only one task at a time, once that is done it will start next. If you want to start all of them at once you can start them and use Task.WhenAll to await it.

private async void button1_Click(object sender, EventArgs e)// <--Note the async modifier
{
var tasks = Processes.Select(ProcessObject).ToList();
await Task.WhenAll(tasks);
}


Related Topics



Leave a reply



Submit