简体   繁体   中英

Will calling Task.Result cause a deadlock if ConfigureAwait(false) is used?

I found this method:

public override void OnActionExecuting(HttpActionContext actionContext)
{
    var body = Task.Run(async()
    => await actionContext.Request.Content.ReadAsStringAsync()
        .ConfigureAwait(false)).Result;
    //rest of code omitted for brevity.
}

I'm trying to work out two things:

1.Will this code cause a deadlock?

2.Since the method cannot be marked async Task , should it be written like this instead?

var body = actionContext.Request.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();

ConfigureAwait(false) is used to skip any synchronization context and go directly to the thread pool for the continuation work . So I am thinking you are on to something here for the cases where the synchronization context is a problem (single-threaded contexts) , such as Windows Forms.

The problem, in essence, is that if you lock the synchronization context's only thread with a .Result or .Wait() , then there is no thread for any continuation work to be completed, effectively freezing your application. By ensuring a thread pool thread will complete the job using ConfigureAwait(false) , then, at least in my head, all should be good as you are suspecting. I think this is a very nice find from you.

This however, as pointed out already in the given comments, is not your case for the particular code that you show. For starters, you are explicitly starting a new thread. That alone takes you away from the case you are fearing.

CLARIFICATION: A Task Is Not a Thread

As stated in the comment, I'll clarify. I simplified by saying "starting a new thread" in the last paragraph. I'll elaborate for the sake of the more enlightened.

Whenever you use Task.Run() , the newly created task will go sit in a thread pool task queue. There is the global queue, and there is one queue per thread pool thread. This is the fun part:

  • If the thread executing the Task.Run() statement is a thread pool thread, the new task will be placed last in that thread's task queue.
  • If the thread executing the Task.Run() statement is not a thread pool thread, the new task will be placed last in the global task queue.

At this point, since we are assuming we are not awaiting and instead we are putting the executing thread to sleep until the task is done, either using .Result or .Wait() , we enter the following situations:

  • A : The executing thread was a thread pool thread. This thread is now blocked and therefore 100% incapacitated to dequeue anything from its own task queue.
  • B : The executing thread was not a thread pool thread. The thread is now blocked, and the task went to the global queue.

If the hill-climbing algoritm is currently creating new threads, or the minimum number of threads has not yet been reached, new thread pool threads will be created. Each of the new threads will dequeue from the global task queue . If no tasks are there, they will proceed to steal work from individual threads' task queues.

This means that, by .Result 'ing or .Wait() 'ing from a thread has put us in the delicate position of having to wait for another thread pool thread to save us. This is... brittle? What if the global queue is receiving tasks at a pace faster than the rate new threads are added to the thread pool? Then we have a frozen thread as if it had deadlocked.

This is why using .Result or .Wait() is generally discouraged.

Anyway, I simplified the way I did because in either case, you have to wait for a different thread to come to your rescue. By writing code like that you have guaranteed that the current thread will never be able to save itself. Hence, "new thread".

Will this code cause a deadlock?

 string body = Task.Run(async() => { return await actionContext.Request.Content.ReadAsStringAsync().ConfigureAwait(false); }).Result;

No. The ReadAsStringAsync task is invoked on the ThreadPool , and the ThreadPool has no synchronization context . So the await will find no synchronization context to capture. For this reason the .ConfigureAwait(false) has no effect. You can omit it, or set the continueOnCapturedContext to true , and the result will be the same: no deadlock.

Should it be written like this instead?

 string body = actionContext.Request.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();

This code will cause no deadlock either. The reason is because the ReadAsStringAsync method does not capture the synchronization context internally. Any await in its internal implementation is configured with .ConfigureAwait(false) . This is something beyond your control. It's just how this method is implemented. The .ConfigureAwait(false) that you have inserted before the .GetAwaiter().GetResult() has no effect. The ConfigureAwait method affects the behavior of await , when a synchronization context is present. In your case there is no await , so whether a synchronization context is present or not is irrelevant. The .GetAwaiter().GetResult() is not await . So it doesn't matter if you set the continueOnCapturedContext to false or true , or omit the ConfigureAwait altogether. The result will be the same: no deadlock.

Since your code is part of an ASP.NET application, none of these two approaches is recommended. Both are blocking needlessly a ThreadPool thread while the ReadAsStringAsync operation is in-flight.


Update: As Alexei Levenkov pointed out in the comments below, blocking on async code with .Wait() or .Result or .GetAwaiter().GetResult() in ASP.NET applications carries the risk of a complete and total deadlock of the whole site. This can happen in case the ThreadPool availability has been exhausted. The ThreadPool can create quite a lot of threads, precisely 32,767 threads in my Windows 10 / 64-bit machine running .NET 6 (as I found out by calling the ThreadPool.GetMaxThreads method), so exhausting it is not an easy feat. But if your site is busy and you are blocking on async code a lot, it's not impossible. If this happens then the completing asynchronous operations will find no available threads to schedule their continuations. The .Wait() is blocking the current thread on a ManualResetEventSlim , that waits for a signal from another thread. The signal will never come, and so the .Wait() will block forever. Then the whole site will come to a complete halt. No more requests are going to be served, until the process is recycled. Here is a related article by Stephen Toub: Should I expose synchronous wrappers for asynchronous methods?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM