4

Compare the following two methods:

static async Task<int> DownloadAsync(string url)
{
    var client = new WebClient();
    var awaitable = client.DownloadDataTaskAsync(url);
    byte[] data = await awaitable;
    return data.Length;
}

usage: Task<int> task = DownloadAsync("http://stackoverflow.com");

static Task<int> Download(string url)
{
    var client = new WebClient();
    var task = client.DownloadDataTaskAsync(url);
    byte[] data = task.Result;
    return Task.FromResult(data.Length);
}

usage:

Task task = new Task(() => Download("http://stackoverflow.com"));
task.Start();

As far as I can see both methods run asynchronously. My questions are:
Is there any difference in behavior between the two methods?
Why do we prefer async-await other then it being a nice pattern?

Gerard
  • 13,023
  • 14
  • 72
  • 125
  • 1
    Be sure to use `.ConfigureAwait(false)` in methods which do not require the `SynchronizationContext` to be captured, all of your awaits in `DownloadAsync` could use this. See [here](http://stackoverflow.com/a/13494570/921321) for an explanation why. – Lukazoid Nov 17 '13 at 01:17

3 Answers3

7

The two methods you post are completely different.

DownloadAsync is a truly asynchronous method. This means that while the data is downloading, there are no threads blocked on that asynchronous operation.

Download synchronously blocks the calling thread by calling Task.Result. I explain on my blog why Result should not be used with asynchronous Tasks: in the general case, it can cause deadlocks. But let's assume there's no deadlock. You then call it from a TPL task, so it blocks the task thread (most likely a thread pool thread). While the data is downloading, that task thread is blocked on that asynchronous operation.

So, DownloadAsync is more efficient.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • `byte[] data = task.Result;` is called on a worker thread, so only this worker thread will be blocked I assume, which corresponds to `byte[] data = await awaitable;` blocking that worker thread - isn't that correct? I believe in your article the Result is called in a UI thread and therefore blocking? – Gerard Nov 17 '13 at 11:41
  • There is no worker thread in the `DownloadAsync` case. The deadlock problem with `Result` is a *deadlock* problem, not a *blocking* problem. I recommend you read my [intro to `async`](http://blog.stephencleary.com/2012/02/async-and-await.html) post if you haven't already done so. – Stephen Cleary Nov 17 '13 at 12:17
  • Reading and understanding are two different things - helas ;) – Gerard Nov 17 '13 at 13:11
  • How can DownloadAsync be "a truly asynchronous method" and also "there is no worker thread"? I mean the await part clearly is executed on a worker thread and when finished returns in the method on that worker thread (in general). – Gerard Nov 17 '13 at 13:15
  • 1
    @Gerard: No, the "await part" is not executed on a worker thread. At a high level (slightly simplified), what happens is this: `DownloadDataTaskAsync` just registers a callback with the TCP/IP stack. So there's no *thread* just sitting around waiting for that packet. When the packet arrives (starting with an interrupt from the network card), `DownloadDataTaskAsync` completes its `Task`, which in turn schedules the rest of `DownloadData` to execute. The point is that in the time from sending the request to receiving the reply, there is no thread. No thread pool thread, no OS thread, *nothing*. – Stephen Cleary Nov 17 '13 at 19:50
  • Thanks Mr. Cleary, I believe that I understand what you mean. I also think I understand the deadlock problem you described. The worker thread I was thinking about really has nothing to do with the DownloadAsync method, it just enters there via the callback on completion. Deadlock occurs when registering the callback for oncompletion and waiting for the task-result are waiting for each other. – Gerard Nov 17 '13 at 23:02
  • Mr. Cleary did you already blog about this confusion regarding Taskscheduler.Default versus Taskscheduler.Current? Current is well-defined, what is the definiton of Default? – Gerard Nov 17 '13 at 23:04
  • I have a pair of blog posts where I talk about the `TaskScheduler` problems [in `StartNew`](http://blog.stephencleary.com/2013/08/startnew-is-dangerous.html) and [in `ContinueWith`](http://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html). It does apply to `Start` as well. The problem really is that `Current` makes sense as the default for dynamic task parallelism (the original intent behind `Task`), but `Default` makes sense as the... er... *default* for asynchronous tasks. `Default` is the thread pool task scheduler. – Stephen Cleary Nov 18 '13 at 03:17
3

new Task will execute the entire method using TaskScheduler.Current, usually this makes use of the ThreadPool.

By using async/await, the method is entered synchronously, and will only use an asynchronous continuation if it is required.

What I mean by this can be demonstrated with the following LINQPad program:

const int delay = 1;

public async Task DoSomethingAsync()
{
    Thread.CurrentThread.ManagedThreadId.Dump();
    await Task.Delay(delay).ConfigureAwait(false);
    Thread.CurrentThread.ManagedThreadId.Dump();
}

void Main()
{
    DoSomethingAsync().Wait();  
}

Try changing delay to 0, and you will see the the continuation resumes on the same thread, this is because Task.Delay just returns immediately if there is no delay, this avoids the overhead of arranging and executing continuations when they are not required.

By using new Task, you are losing this clever functionality and always using a ThreadPool thread, even when the implementor of an async method may not deem it necessary.

Lukazoid
  • 19,016
  • 3
  • 62
  • 85
  • Your answer is a bit tricky, but you pointed out a real difference in the two approaches. – Gerard Nov 17 '13 at 01:14
  • @Gerard Yeh, async/await is a bit tricky to explain, especially at 1.15am, I will maybe try and improve it in the morning. – Lukazoid Nov 17 '13 at 01:15
  • The method without async runs in a worker thread completely, the method with async-await uses the calling thread up to the point where the await is, then after the await it runs in a worker thread except as in your delay(0) example - when there is no real task - it runs again in the calling thread. – Gerard Nov 17 '13 at 01:16
  • 1
    @Gerard You've got it exactly. Imagine your `DownloadAsync` changed to start caching results, the second invocation would be able to return immediately. If you used `new Task`, you would be using a threadpool thread just to return a cached value. With async/await the invoker does not need to worry about those implementation details, it just says, give me the result when you are ready. – Lukazoid Nov 17 '13 at 01:22
  • Thanks for your effort so late, I see it now. – Gerard Nov 17 '13 at 01:26
  • 1
    `new Task()` doesn't execute anything. And `Task.Start()` executes it using `TaskScheduler.Current`. That's often the same as `TaskScheduler.Default`, but not always. – svick Nov 17 '13 at 02:01
  • @svick How can I avoid having to bother about TaskScheduler.Current versus TaskScheduler.Default? – Gerard Nov 17 '13 at 11:45
  • @Gerard Use `Task.Run()`. That always executes on `Default`. – svick Nov 17 '13 at 12:38
  • @svick Corrected my answer to use `TaskScheduler.Current` instead of `TaskScheduler.Default` and would the downvoter care to comment on what is wrong in my answer? – Lukazoid Nov 17 '13 at 13:22
0

Have a look at this post, Stephen Cleary explains exactly the why and the differences.

In short, it's quite the same. It's the new vs. old way of waiting. I find the async / await being nicer on the eyes as well, since you'll have the extra code in the other method, and don't need to put the task.Start().

Noctis
  • 11,507
  • 3
  • 43
  • 82