2

I was having a problem with a hanging await (described here). During research I found out that calling SetResult on my TaskCompletionSource actually invokes awaiting continuation in the context of the thread that called SetResult (this is also spelled out in this answer to a somewhat related question). In my case this is a different thread (a thread-pool worker thread) from the one that started the await (an ASP.NET request thread).

While I'm still not sure why this would cause a hang, I decided to try forcing the SetResult into the original context. I stored the value of SynchronizationContext.Current before entering await on the request thread and manually applied it in the worker thread via SynchronizationContext.SetSynchronizationContext just before calling SetResult. This solved the hang and I can now await all my async methods without having to specify ConfigureAwait(false).

My question is: is this a reasonable and correct approach to manually capturing and applying the SynchronizationContext? FWIW, I tried doing a simple Post() with the SetResult delegate first, but that still caused a hang. I'm obviously a bit out of my comfort zone here... Please help me understand what's going on!

Community
  • 1
  • 1
aoven
  • 2,248
  • 2
  • 25
  • 36

2 Answers2

3

SetResult is not guaranteed to call anything. Therefore, this is not reliable.

You need to switch the sync context at the point where it is captured. A common pain point here is WebClient which captures the context when starting a web request. So your code would look like this:

SetContext(newContext);
new WebClient().DownloadAsync(...);
SetContext(oldContext);

Restore the old context to not disturb anything.

In other words the problem is in the continuation code, not in the code calling SetResult.

usr
  • 168,620
  • 35
  • 240
  • 369
  • Good point about restoring the context! Regarding guarantees of SetResult (or lack thereof)... Could you expand a bit, please? The *observed* effect is that the manually applied context is flowed from the worker thread back to the continuation just fine, so it must be done by the SetResult, AFAIU. If this is merely a coincidence, as you implied, I'd LOVE to know more about the gritty details! – aoven Dec 07 '16 at 09:46
  • The TPL has a few points where code is inlined for efficiency. This is terrible. See https://github.com/dotnet/corefx/issues/2454. If you modify thread local state and have this call chain: A=>SetResult=>B then B will see that state. If the chain is A=>SetResult=>QueueToThreadPool(B) then B will see clean state. Does this explain what you are seeing?= – usr Dec 07 '16 at 09:57
  • I'm unable to say either way for sure... My chain is actually A=>QueueToThreadPool=>SetResult=>B and I've now determined that SetResult doesn't call the continuation directly because the call succeeds and the worker thread doesn't block. The only thing (visible to me) that blocks is the await A. Passing RunContinuationsAsynchronously to TaskCompletionSource doesn't seem to have any effect, same with calling Post/Send. You previously mentioned the problem is in the continuation... What kind of problem could there possibly be, if it's never even executed (the block occurs on the await line)? – aoven Dec 07 '16 at 10:46
  • I'm starting to become confused. Can you post executable code that demonstrates the issue? Or that at least comes close? – usr Dec 07 '16 at 11:43
  • Yes, another respondent to my original question suggested this already... I'll try to isolate the relevant parts as soon as I find the time. Please, stay tuned! – aoven Dec 07 '16 at 14:32
0

To my embarrassment, I had completely overlooked that my HTTP handler was derived from a small base class, which implemented IAsyncHttpHandler in a very questionable way in order to add support for async handlers:

public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
    ...
    var task = HandleRequestAsync(...);

    Task.Run(async () => { await task; }).GetAwaiter().GetResult();
    ...
}

I can't even remember why I did this in the first place (it was over a year ago), but it definitely was THE stupid part I was looking for for the last couple of days!

Changing the handler base class to .NET 4.6's HttpTaskAsyncHandler got rid of the hangs. Sorry for wasting everyone's time! :(

aoven
  • 2,248
  • 2
  • 25
  • 36