开发者

How to make Task awaitable

Yesterday I started playing with Microsoft CTP async library, and nowhere I could not find the proper implementation of the awaitable Task. I know that it must have implementation like this?:

public struct SampleAwaiter<T>
{
    private readonly Task<T> task;
    public SampleAwaiter(Task<T> task) { this.task = task; }
    public bool IsCompleted { get { return task.IsCompleted; } }
    public void OnCompleted(Action continuation) { TaskEx.Run(continuation); }
    public T GetResult() { return task.Result; }
}

But how would I now implement a task that would, le开发者_StackOverflow中文版t's say, wait 5 seconds, and the return some string, for example "Hello World"?

One way is to use Task directly like so:

Task<string> task = TaskEx.Run(
            () =>
                {
                    Thread.Sleep(5000);
                    return "Hello World";
                });

        string str = await task;

But how would I do that with the awaitable implementation? Or did I just misunderstood everything?

Thanks for any information/help :)


The key here is AsyncCtpThreadingExtensions.GetAwaiter which provides those methods via an extension method. Since the async implementation is pattern based (like LINQ), rather than tied to a specific interface it can come from everywhere (it is TaskAwaiter in this case).

Your code as written is awaitable. For example:

static void Main()
{
    Test();
    Console.ReadLine(); // so the exe doesn't burninate
}
static async void Test() {
    Task<string> task = TaskEx.Run(
           () =>
           {
               Thread.Sleep(5000);
               return "Hello World";
           });
    string str = await task;
    Console.WriteLine(str);
}

This prints Hello World after 5 seconds.


Addition one year later

After using async-await for over a year now, I know that some things about async I wrote in my original answer are not correct, although the code in the answer is still correct. Hera are two links that helped me enormously to understand how async-await works.

This interview Eric Lippert shows an excellent analogy for async-await. Search somewhere in the middle for async-await.

In this article, the ever so helpful Eric Lippert shows some good practices for async-await

Original answer

OK, here is a full example that helped me during the learning process.

Suppose you have a slow calculator, and you want to use it when pressing a button. Meanwhile you want your UI to stay responsive, and maybe even do other things. When the calculator is finished you want to display the result.

And of course: use async / await for this, and none of the old methods like setting event flags and waiting for these events to be set.

Here is the slow calculator:

private int SlowAdd(int a, int b)
{
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));
    return a+b;
}

If you want to use this asynchronously while using async-await you have to use Task.Run(...) to start it asynchronously. The return value of Task.Run is an awaitable Task:

  • Task if the return value of the function you Run is void
  • Task<TResult> if the return value of the function you run is TResult

You can just start the Task, do something else, and whenever you need the result of the Task you type await. There is one drawback:

If you want to 'await' your function needs to be async and return Task instead of void or Task<TResult> instead of TResult.

Here's the code that runs the slow calculator. It's common practice to terminate the identifier of an async function with async.

private async Task<int> SlowAddAsync(int a, int b)
{
    var myTask = Task.Run ( () => SlowAdd(a, b));
    // if desired do other things while the slow calculator is working
    // whenever you have nothing to do anymore and need the answer use await
    int result = await myTask;
    return result;
}

Side remark: Some people prefer Task.Factory.StartNew above Start.Run. See what MSDN tells about this:

MSDN: Task.Run versus Task.Factory.StartNew

The SlowAdd is started as an async function, and your thread continues. Once it needs the answer it awaits for the Task. The return value is the TResult, which in this case is an int.

If you have nothing meaningful to do the code would look like this:

private async Task`<int`> SlowAddAsync(int a, int b)
{
    return await Task.Run ( () => SlowAdd(a, b));
}

Note that SlowAddAsync is declared an async function, so everyone who uses this async function should also be async and return Task or Task<TResult>:

private async Task UpdateForm()
{
     int x = this.textBox1.Text;
     int y = this.textBox2.Text;
     int sum = await this.SlowAddAsync(x, y);
     this.label1.Text = sum.ToString();
}

The nice thing about async / await is that you don't have to fiddle with ContinueWith to wait until the previous task is finished. Just use await, and you know the task is finished and you have the return value. The statement after the await is what you'd normally do in the ContinueWith.

By the way, your Task.Run doesn't have to call a function, you can also put a statement block in it:

int sum = await Task.Run( () => {
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));
    return a+b});

However the nice thing about a separate function is that you give those who don't need / want / understand async, the possibility to use the function without async / await.

Remember:

Every function that uses await should be async

Every async function should return Task or Task<Tresult>

"But my event handler can't return a Task!"

private void OnButton1_Clicked(object sender, ...){...}

You're right, therefore that is the only exception:

async event handlers may return void

So when clicking the button, the async event handler would keep the UI responsive:

private async void OnButton1_Clicked(object sender, ...)
{
    await this.UpdateForm();
}

However you still have to declare the event handler async

A lot of .NET functions have async versions that return a Task or Task<TResult>.

There are async functions for - Internet access - Stream Read and Write - Database access - etc.

To use them you don't have to call Task.Run, they already return Task and Task<TResult> just call them, continue doing your own stuff and when you need the answer await for the Task and use the TResult.

Start several tasks and wait for them to finish If you start several tasks and you want to wait for all of them to finish, use Task.WhenAll(...) NOT Task.Wait

Task.Wait returns a void. Task.WhenAll returns a Task, so you can await for it.

Once a task is finished, the return value is already the return of the await, but if you await Task.WhenAll ( new Task[]{TaskA, TaskB, TaskC}); you have to use the Task<TResult>.Result property to know the result of a task:

int a = TaskA.Result;

If one of the tasks throws an exception, it is wrapped as InnerExceptions in an AggregateException. So if you await Task.WhenAll, be prepared to catch the AggregateException and check the innerExceptions to see all exceptions thrown by the tasks you started. Use the function AggregateException.Flatten to access the exceptions more easily.

Interesting to read about cancellation:

MSDN about Cancellation in managed threads

Finally: you use Thread.Sleep(...). The async version is Task.Delay(TimeSpan):

private async Task`<int`> MySlowAdd(int a, int b)
{
    await Task.Delay(TimeSpan.FromSeconds(5));
    return a+b;
}

If you use this function your program keeps responsive.


I ended up with this sample code ... is this the proper implementation of the awaitable pattern?

namespace CTP_Testing
{
using System;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

public class CustomAsync
{
    public static CustomAwaitable GetSiteHeadersAsync(string url)
    {
        return new CustomAwaitable(url);
    }
}

public class CustomAwaitable
{
    private readonly Task<string> task;
    private readonly SynchronizationContext ctx;

    public CustomAwaitable(string url)
    {
        ctx = SynchronizationContext.Current;
        this.task = Task.Factory.StartNew(
            () =>
                {
                    var req = (HttpWebRequest)WebRequest.Create(url);
                    req.Method = "HEAD";
                    var resp = (HttpWebResponse)req.GetResponse();
                    return this.FormatHeaders(resp.Headers);
                });
    }
    public CustomAwaitable GetAwaiter() { return this; }
    public bool IsCompleted { get { return task.IsCompleted; } }
    public void OnCompleted(Action continuation)
    {
        task.ContinueWith(_ => ctx.Post(delegate { continuation(); }, null));
    }
    public string GetResult() { return task.Result; }

    private string FormatHeaders(WebHeaderCollection headers)
    {
        var headerString = headers.Keys.Cast<string>().Select(
            item => string.Format("{0}: {1}", item, headers[item]));

        return string.Join(Environment.NewLine, headerString.ToArray());
    }
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜