4

I'm trying this async code just for testing the async keyword:

public async Task<string> AsyncMethod()
{
    var link = "http://www.google.com";

    var webclient = new WebClient();
    var result = await webclient.DownloadStringTaskAsync(new Uri(link));

    return result;
}

public async Task<ActionResult> Index()
{
    var a = AsyncMethod();
    var b = AsyncMethod();

    Task.WaitAll(a, b);

    return View();
}

but when I debug it, the debugger hits the Task.WaitAll and just do nothing(the return keywork is never executed).. If I set await before the two 'AsyncMethod' and remove the Task.WaitAll it works.. So what am I doing wrong?

MuriloKunze
  • 15,195
  • 17
  • 54
  • 82
  • 1
    Does WaitAll start the tasks? If not, then you will wait a long time, since nobody started the Tasks... – flq Dec 26 '12 at 21:37
  • @flq What you mean by start the task? – MuriloKunze Dec 26 '12 at 21:40
  • 3
    @flq Tasks returned by methods marked with the `async` modifier are already running. – e_ne Dec 26 '12 at 21:41
  • Did you set a breakpoint at return result? maybe the taks is completing before it comes to Task.WaitAll. Try also to print the results after WaitAll, see if you get something. Another thing, you don't need to async keyword in Index(). – MBen Dec 26 '12 at 21:55
  • I think it`s not finishing before the Task.WaitAll, the download cannot be so fast and If I remove the async of Index I get a exception. – MuriloKunze Dec 26 '12 at 22:04
  • @MuriloKunze What kind of exception? – svick Dec 26 '12 at 22:36
  • @svick An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%@ Page Async="true" %> – MuriloKunze Dec 26 '12 at 23:08

3 Answers3

14

Because your method looks like ASP.NET MVC controller action, I'm assuming you're running on ASP.NET.

By default, an async method resumes on the same context where it was suspended (i.e. where you called await). In ASP.NET, this means the current request context. And only one thread can be in a specific context at a time. So, what happens is that the thread that executes Index() is in the request context, blocked in WaitAll(). On the other hand, both invocations of AsyncMethod() are trying to resume on the same context (after they finished downloading), but they are unable to do so, because Index() is still executing in that context. Because of this, the methods are in a deadlock and so nothing happens.

(The same deadlock would also happen in an GUI application, because GUI context behave similarly in this regard. Console applications don't have this issue, because they don't have any context.)

The fix for this is twofold:

  1. Never wait synchronously for an async method. (Probably the sole exception is if want to execute an async method from the Main() method of a console application.)

    Instead, wait for them asynchronously. In your case, that means using await Task.WhenAll(a, b).

  2. Use ConfigureAwait(false) in your "library" methods (i.e. those that don't actully need to execute on the request context).

Using 1 or 2 would fix your issue, but it's probably best if you do both.

For some more information about this issue, read Stephen Cleary's article Don't Block on Async Code.

Community
  • 1
  • 1
svick
  • 236,525
  • 50
  • 385
  • 514
  • Very useful, I've noticed that the problem didn't happen on Console applications but I didn't know why. +1 – e_ne Dec 26 '12 at 22:41
-1

Make sure that your async methods make use of the CancellationToken:

// the cancellation token from the request triggers 
// when the user cancels the HTTP request in the web browser
public async Task<IActionResult> Index(CancellationToken cancellationToken = default)
{
    // internal cancellation token for the timeout
    using var ctsTimeout = new CancellationTokenSource(TimeSpan.FromMilliSeconds(2000));

    // cancels when either the user cancels the request
    // or the timeout expires
    using var cts = CancellationToken.CreateLinkedTokenSource(cancellationToken, ctsTimeout.Token);

    // make sure your methods make use of the cancellation token internally
    // ie. check the token in loops and on I/O requests
    var a = AsyncMethod(cts.Token);
    var b = AsyncMethod(cts.Token);

    // optional: pass the token to the Task.WaitAll method
    // ensures the HTTP request completes
    // even when the internal tasks won't
    Task.WaitAll(new [] { a, b }, cts.Token);

    return View();
}

If one of the called methods don't return, then the timeout will ensure that the tasks get cancelled and the Task.WaitAll throws an Exception.

MovGP0
  • 7,267
  • 3
  • 49
  • 42
  • *"the timeout will ensure that the tasks get cancelled"* -- The tasks are not canceled, they are just ignored and become fire-and-forget tasks. The `CancellationToken` cancels the *awaiting* of the tasks, not the tasks themselves. – Theodor Zoulias Jan 07 '22 at 10:48
  • that's what the AsyncMethod(cts.Token) is for – MovGP0 Jan 07 '22 at 13:56
  • 1
    I am talking about the token passed in the `Task.WaitAll` method. I don't think it's needed there, unless you really want to fire-and-forget the waited tasks. – Theodor Zoulias Jan 07 '22 at 14:01
-2

It works this way:

public Task<string> FakeAsyncMethod()
{
    var link = "http://google.com";
    var webclient = new WebClient();
    var t = new Task<string>(() => webclient.DownloadString(new Uri(link)));
    return t;
}

public async Task Index()
{
    var a = FakeAsyncMethod();
    var b = FakeAsyncMethod();
    a.Start();
    b.Start();
    Task.WaitAll(a, b);
}

async void AsyncCall()
{
    await Index();
}

I don't know why it won't work with your method, but I suspect it's because tasks returned by methods marked with the async keyword are created in running state (more precisely, with Status equals to WaitingForActivation). I will look into it more.

EDIT: an alternative is to use Task.WhenAll paired with the await keyword.

public async Task Index()
{
    var a = AsyncMethod();
    var b = AsyncMethod();
    await Task.WhenAll(a, b);
}
e_ne
  • 8,340
  • 32
  • 43
  • I dont understand exactly why I need to start the async task because when I set a local url in my AsyncMethod I notice that the page is downloaded. – MuriloKunze Dec 26 '12 at 22:06
  • Great, the WhenAll is exactly what I need. Thank you. – MuriloKunze Dec 26 '12 at 22:08
  • 1
    In the first example I've posted, you're not using your methods, you are using mine (`FakeAsyncMethod`). That one returns a task with `Status` equals to `Created` but not running yet -- That's why you need to start it. In my edit, you're using your method, instead. – e_ne Dec 26 '12 at 22:08
  • This does not explain what the actualk problem is, or why the fixes work. And I don't see why having the `Task`s in running state would be an issue. – svick Dec 26 '12 at 22:14