I think you're getting a few things confused, here. What you're asking for is already possible using System.Threading.Tasks
, the async
and await
in C# 5 are just going to provide a little nicer syntactic sugar for the same feature.
Let's use a Winforms example - drop a button and a textbox on the form and use this code:
private void button1_Click(object sender, EventArgs e)
{
Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
.ContinueWith(t => DelayedAdd(t.Result, 20))
.ContinueWith(t => DelayedAdd(t.Result, 30))
.ContinueWith(t => DelayedAdd(t.Result, 50))
.ContinueWith(t => textBox1.Text = t.Result.ToString(),
TaskScheduler.FromCurrentSynchronizationContext());
}
private int DelayedAdd(int a, int b)
{
Thread.Sleep(500);
return a + b;
}
Run it and you'll see that (a) it doesn't block the UI thread and (b) you don't get the usual "cross-thread operation not valid" error - unless you remove the TaskScheduler
argument from the last ContinueWith
, in which case you will.
This is bog-standard continuation passing style. The magic happens in the TaskScheduler
class and specifically the instance retrieved by FromCurrentSynchronizationContext
. Pass this into any continuation and you tell it that the continuation must run on whichever thread called the FromCurrentSynchronizationContext
method - in this case, the UI thread.
Awaiters are slightly more sophisticated in the sense that they're aware of which thread they started on and which thread the continuation needs to happen on. So the above code can be written a little more naturally:
private async void button1_Click(object sender, EventArgs e)
{
int a = await DelayedAddAsync(5, 10);
int b = await DelayedAddAsync(a, 20);
int c = await DelayedAddAsync(b, 30);
int d = await DelayedAddAsync(c, 50);
textBox1.Text = d.ToString();
}
private async Task<int> DelayedAddAsync(int a, int b)
{
Thread.Sleep(500);
return a + b;
}
These two should look very similar, and in fact they are very similar. The DelayedAddAsync
method now returns a Task<int>
instead of an int
, and so the await
is just slapping continuations onto each one of those. The main difference is that it's passing along the synchronization context on each line, so you don't have to do it explicitly like we did in the last example.
In theory the differences are a lot more significant. In the second example, every single line in the button1_Click
method is actually executed in the UI thread, but the task itself (DelayedAddAsync
) runs in the background. In the first example, everything runs in the background, except for the assignment to textBox1.Text
which we've explicitly attached to the UI thread's synchronization context.
That's what's really interesting about await
- the fact that an awaiter is able to jump in and out of the same method without any blocking calls. You call await
, the current thread goes back to processing messages, and when it's done, the awaiter will pick up exactly where it left off, in the same thread it left off in. But in terms of your Invoke
/BeginInvoke
contrast in the question, I'm sorry to say that you should have stopped doing that a long time ago.
You will be able to accomplish your task using BackgroundWorker
. It is a well known class, and many people have used it.
The new C# 5 async
and await
keywords basically just make it easier to write readable asynchronous code. There may be fewer tutorials and examples of how to accomplish various tasks with these keywords rather than BackgroundWorker
.
Unless you need to use an older version of C#, I suggest learning how to use async
and await
.
Best Answer
Let's start by noting that this is already a problem, even without async support in the language. Lots of things can cause messages to be dequeued while you're handling an event. You already have to code defensively for this situation; the new language feature just makes that more obvious, which is goodness.
The most common source of a message loop running during an event causing unwanted re-entrancy is
DoEvents
. The nice thing aboutawait
as opposed toDoEvents
is thatawait
makes it more likely that new tasks are not going to "starve" currently-running tasks.DoEvents
pumps the message loop and then synchronously invokes the re-entrant event handler, whereasawait
typically enqueues the new task so that it will run at some point in the future.You can manage that complexity the same way you'd manage any other complexity in an OO language: you abstract the mechanisms away into a class, and make the class responsible for correctly implementing a policy. (Try to keep the mechanisms logically distinct from the policy; it should be possible to change up the policy without tinkering overmuch with the mechanisms.)
If you have a form with lots of controls on it which interact in interesting ways then you are living in a world of complexity of your own making. You've got to write code to manage that complexity; if you don't like that then simplify the form so that it isn't so complex.
Users will hate that.