7

Given the following code, why does ask.WhenAny never return when provided with a Task.Delay of 1 second? Technically I'm not sure if it does return after a extended amount of time, but it doesn't after 15 seconds or so after which I manually kill the process. According to the documentation I shouldn't be required to manually start the delayTask, and in fact I receive a exception if I try to do so manually.

The code is being called from the UI thread when a user selects a context menu item in a WPF application, although it works fine if I have the click method specified for the context menu item run this code in a new thread.

public void ContextMenuItem_Click(object sender, RoutedEventArgs e)
{
    ...
    SomeMethod();
    ...
}

public void SomeMethod()
{
    ...
    SomeOtherMethod();
    ....
}

public void SomeOtherMethod()
{
    ...
    TcpClient client = Connect().Result;
    ...
}

//In case you're wondering about the override below, these methods are in
//different classes i've just simplified things here a bit so I'm not posting
//pages worth of code.
public override async Task<TcpClient> Connect()
{
    ...
    Task connectTask = tcpClient.ConnectAsync(URI.Host, URI.Port);
    Task delayTask = Task.Delay(1000);
    if (await Task.WhenAny(connectTask, delayTask) == connectTask)
    {
        Console.Write("Connected\n");
        ...
        return tcpClient;
    }
    Console.Write("Timed out\n");
    ...
    return null;
}

If I change ContextMenuItem_Click to the following it works fine

public void ContextMenuItem_Click(object sender, RoutedEventArgs e)
{
    ...
    new Thread(() => SomeMethod()).Start();
    ...
}
Noctis
  • 11,507
  • 3
  • 43
  • 82
Matt
  • 774
  • 1
  • 10
  • 28
  • 4
    Can you provide a short but complete program demonstrating the problem? (For example, try using `Task.WhenAny` with two delaying tasks...) – Jon Skeet Jun 01 '14 at 21:28
  • @Jon - Even with "if (await Task.WhenAny(delayTask) == delayTask)" isn't returning. Although if I put delayTask.Wait() before the if(Task.WhenAny) statement, the if(Task.WhenAny) will return. – Matt Jun 01 '14 at 21:30
  • 1
    So show that within a short but complete program which we can try to reproduce it for ourselves... – Jon Skeet Jun 01 '14 at 21:31
  • Well I suppose that is another problem. It is in a WPF application, and if I put that code in main window's constructor it works fine. The code above is being called from the UI thread when a context menu item is selected. Any ideas as to why this would be happening? – Matt Jun 01 '14 at 21:35
  • 1
    No, but I'd find it easier to help you if I could easily reproduce it myself - it's really not *that* hard to put together a completely-minimal WPF app to demonstrate the problem. I'm off to bed now, but maybe someone else will be able to help... – Jon Skeet Jun 01 '14 at 21:36
  • I'll see what I can do because you do have a good point. I was hoping I could get away without having to do, and it would be obvious to someone with more .NET experience than me, because it is a bit of a hassle to format the whitespace for SO code blocks. – Matt Jun 01 '14 at 21:40
  • If jon skeet is having a problem with it you will probably need to give us the whole thing... – Gilad Jun 01 '14 at 22:08
  • I've updated the code a bit to give a better idea of whats going. Yeah it isnt a complete program, but I guessing Stephen's answer below is probably identifies the cause of this issue. Just trying to avoid posting pages worth of code, and the required formatting. – Matt Jun 02 '14 at 03:09

1 Answers1

14

I predict that further up your call stack, you're calling Task.Wait or Task<T>.Result. This will cause a deadlock that I explain in full on my blog.

In short, what happens is that await will (by default) capture the current "context" and use that to resume its async method. In this example, the "context" is the WPF UI context.

So, when your code does its await on the task returned by WhenAll, it captures the WPF UI context. Later, when that task completes, it will attempt to resume on the UI thread. However, if the UI thread is blocked (i.e., in a call to Wait or Result), then the async method cannot continue running and will never complete the task it returned.

The proper solution is to use await instead of Wait or Result. This means your calling code will need to be async, and it will propagate through your code base. Eventually, you'll need to decide how to make your UI asynchronous, which is an art in itself. At least to start with, you'll need an async void event handler or some kind of an asynchronous MVVM command (I explore async MVVM commands in an MSDN article). From there you'll need to design a proper asynchronous UI; i.e., how your UI looks and what actions it permits when asynchronous operations are in progress.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Would this deadlock occur if it was running in a thread other than the UI thread? If not, then you'd be correct, the method one level up in the call stack is calling .Result on it. I'll update the code in just a second to reflect this. – Matt Jun 02 '14 at 03:01
  • I explain all the details on my blog: this deadlock happens with any context that only allows one thread at a time (i.e., UI context or ASP.NET request context). If there is no "context", then the "thread pool context" is used, which will schedule the remainder of the `async` method to the thread pool (which *can* run on another thread). This is why the deadlock does not happen on a separate thread (or in Console applications). – Stephen Cleary Jun 02 '14 at 11:58
  • Stephen Cleary could you write a small code example for a solution for the above code in a IIS assuming that he needs the Task`.Result` – Rui Caramalho Dec 11 '19 at 17:14
  • 1
    @RuiCaramalho: There are no simple solutions for blocking on asynchronous code. If blocking absolutely cannot be avoided, you can use one of the hacks in my [brownfield async article](https://learn.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development). – Stephen Cleary Dec 11 '19 at 17:47
  • Stephen Cleary. I did read your article, nice article :). But II was using the TASK with `ConnectAsync` for a single Computer (in IIS). So created a new method using `Connect` with `tcpClient.ReceiveTimeout = sec * 1000;`. So if `System.Web.Hosting.HostingEnvironment.IsHosted` I use this method – Rui Caramalho Dec 11 '19 at 18:35