How to implement await without Async CTP

2019-03-27 21:05发布

问题:

How would you implement something that works similarly to the Async CTP await keyword? Is there a simple implementation that works like await in all cases, or does await require different implementations for different scenarios?

回答1:

await always involves the same kind of transformation - but it's a pretty painful one. The library side of await isn't too complicated, but the tricky bit is that the compiler builds a state machine for you, allowing the continuation to jump back to the right place.

It's possible that my some hacky use of iterator blocks (yield return) you could fake something similar... but it would be pretty ugly.

I gave a DevExpress webinar on what the compiler is doing behind the scenes a few weeks ago - that shows decompiled code from a couple of examples, as well as explaining how the compiler builds a task to return, and what the "awaiter" has to do. It may be useful to you.



回答2:

The new await keyword has similar semantics to the existing yield return keyword in that they both cause the compiler to generate the continuation style state machine for you. So it is possible to hack something together using iterators that has some of the same behaviors as the Async CTP.

Here is what it would look like.

public class Form1 : Form
{
    private void Button1_Click(object sender, EventArgs e)
    {
        AsyncHelper.Invoke<bool>(PerformOperation);
    }

    private IEnumerable<Task> PerformOperation(TaskCompletionSource<bool> tcs)
    {
        Button1.Enabled = false;
        for (int i = 0; i < 10; i++)
        {
            textBox1.Text = "Before await " + Thread.CurrentThread.ManagedThreadId.ToString();
            yield return SomeOperationAsync(); // Await
            textBox1.Text = "After await " + Thread.CurrentThread.ManagedThreadId.ToString();
        }
        Button2.Enabled = true;
        tcs.SetResult(true); // Return true
    }

    private Task SomeOperationAsync()
    {
        // Simulate an asynchronous operation.
        return Task.Factory.StartNew(() => Thread.Sleep(1000));
    }
}

Since yield return generates an IEnumerable our coroutine must return an IEnumerable. All of the magic happens inside the AsyncHelper.Invoke method. This is what gets our coroutine (masquerading as a hacked iterator) going. It takes special care to make sure the iterator is always executed on the current synchronization context if one exists which is important when trying to simulate how await works on a UI thread. It does this by executing the first MoveNext synchronously and then using SynchronizationContext.Send to do the rest from a worker thread which is also used to asynchronously wait on the individual steps.

public static class AsyncHelper
{
    public static Task<T> Invoke<T>(Func<TaskCompletionSource<T>, IEnumerable<Task>> method)
    {
        var context = SynchronizationContext.Current;
        var tcs = new TaskCompletionSource<T>();
        var steps = method(tcs);
        var enumerator = steps.GetEnumerator();
        bool more = enumerator.MoveNext();
        Task.Factory.StartNew(
            () =>
            {
                while (more)
                {
                    enumerator.Current.Wait();
                    if (context != null)
                    {
                        context.Send(
                            state =>
                            {
                                more = enumerator.MoveNext();
                            }
                            , null);
                    }
                    else
                    {
                        enumerator.MoveNext();
                    }
                }
            }).ContinueWith(
            (task) =>
            {
                if (!tcs.Task.IsCompleted)
                {
                    tcs.SetResult(default(T));
                }
            });
        return tcs.Task;
    }
}

The whole bit about the TaskCompletionSource was my attempt at replicating the way await can "return" a value. The problem is that the coroutine has to actually return an IEnumerable since it is nothing more than a hacked iterator. So I needed to come up with an alternate mechanism to capture a return value.

There are some glaring limitations with this, but I hope this gives you the general idea. It also demonstrates how the CLR could have one generalized mechanism for implementing coroutines for which await and yield return would use ubiquitously, but in different ways to provide their respective semantics.



回答3:

There are few implementations and examples of coroutines made out of iterators (yield).

One of the examples is Caliburn.Micro framework, that uses this patter for asynchronous GUI operations. But it can easily be generalised for general async code.



回答4:

The MindTouch DReAM framework implements Coroutines on top of the Iterator pattern which is functionally very similar to Async/Await:

async Task Foo() {
  await SomeAsyncCall();
}

vs.

IYield Result Foo() {
  yield return SomeAsyncCall();
}

Result is DReAM's version of Task. The framework dlls work with .NET 2.0+, but to build it you need 3.5, since we're using a lot of 3.5 syntax these days.



回答5:

Bill Wagner from Microsoft wrote an article in MSDN Magazine about how you can use the Task Parallel Library in Visual Studio 2010 to implement async like behavior without adding a dependency on the async ctp.

It uses Task and Task<T> extensively which also has the added benefit that once C# 5 is out, your code will be well prepared to start using async and await.



回答6:

From my reading, the major differences between yield return and await is that await can provide explicitly return a new value into the continuation.

SomeValue someValue = await GetMeSomeValue();

whereas with yield return, you'd have to accomplish the same thing by reference.

var asyncOperationHandle = GetMeSomeValueRequest();
yield return asyncOperationHandle;
var someValue = (SomeValue)asyncOperationHandle.Result;