During creating simple sample using async/await
, I discovered, that some examples just illustrate the pattern on Button1_Click
like methods and freely update GUI controls directly from async
methods. So one could consider this as the safe mechanism. But my test code was constantly crashing on TargetInvocationException
exceptions in mscorlib.dll
with inner exceptions like: NullReference
, ArgumentOutOfRange
etc. Regarding the stack trace, everything seemed to point to the WinForms.StatusStrip
labels displaying results (and being driven directly from the async
methods bound to the button event handlers). The crashing seems to be fixed when using the old school Control.Invoke
when accessing the GUI controls.
The questions are: Have I missed something important? Are the async methods usafe the same way as the threads/background workers formerly used for long term operations and thus the Invoke
is the recommended solution? Are the code snippets driving GUI directly from async
methods wrong?
EDIT: For the missing-source downvoters: Create a Simple Form containing three buttons and one StatusStrip containing two labels...
//#define OLDSCHOOL_INVOKE
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AsyncTests
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void LongTermOp()
{
int delay;
int thisId;
lock (mtx1)
{
delay = rnd.Next(2000, 10000);
thisId = firstCount++;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label1Gen.Text = $"Generating first run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
))
#endif
;
++firstPending;
}
await Task.Delay(delay);
lock (mtx1)
{
--firstPending;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label1Gen.Text = $"First run #{thisId} completed, {firstPending} pending..."
#if OLDSCHOOL_INVOKE
))
#endif
;
}
}
private async Task LongTermOpAsync()
{
await Task.Run((Action)LongTermOp);
}
private readonly Random rnd = new Random();
private readonly object mtx1 = new object();
private readonly object mtx2 = new object();
private int firstCount;
private int firstPending;
private int secondCount;
private int secondPending;
private async void buttonRound1_Click(object sender, EventArgs e)
{
await LongTermOpAsync();
}
private async void buttonRound2_Click(object sender, EventArgs e)
{
await Task.Run(async () =>
{
int delay;
int thisId;
lock (mtx2)
{
delay = rnd.Next(2000, 10000);
thisId = secondCount++;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label2Gen.Text = $"Generating second run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
))
#endif
;
++secondPending;
}
await Task.Delay(delay);
lock (mtx2)
{
--secondPending;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label2Gen.Text = $"Second run #{thisId} completed, {secondPending} pending..."
#if OLDSCHOOL_INVOKE
))
#endif
;
}
});
}
private void buttonRound12_Click(object sender, EventArgs e)
{
buttonRound1_Click(sender, e);
buttonRound2_Click(sender, e);
}
private bool isRunning = false;
private async void buttonCycle_Click(object sender, EventArgs e)
{
isRunning = !isRunning;
await Task.Run(() =>
{
while (isRunning)
{
buttonRound12_Click(sender, e);
Application.DoEvents();
}
});
}
}
}
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).