有人可以解释这种僵局行为吗?

时间:2019-09-23 12:39:06

标签: c# asp.net asynchronous concurrency

TLDR:在下面的示例(ASP.NET)中,为什么task1.Result和task2.Result导致死锁,而task3.Result没有死锁?

我们的代码中有一个类,在这里我无法轻松地将该方法标记为异步,并且在等待异步任务完成时不能阻止代码阻塞。原因是我们的框架不支持这一点。因此,我试图找到带有task.Result的解决方案并遇到一些僵局。幸运的是,我找到了一个解决方案(请参阅task3)。现在,我试图找出task1,task2和task3之间的区别,以了解为什么前两个会导致死锁,而第三个不会导致死锁。

使用调试器,我看不到有任何区别,例如在调用task3.Result之前正在运行task3。像其他两个一样,它仍处于WaitingForActivation状态。谁能向我解释Task3如何工作?

    public class HomeController : Controller
    {
        public ActionResult GetSomething()
        {
            var task1 = GetSomethingAsync();
            var task2 = Task.Run(async () => await task1);
            var task3 = Task.Run(async () => await GetSomethingAsync());
            return Content(task1.Result);
//            return Content(task2.Result);
//            return Content(task3.Result);
        }

        private static async Task<string> GetSomethingAsync()
        {
            return await Task.Run(() => "something");
        }
    }

2 个答案:

答案 0 :(得分:2)

问题的根源是这里的await

private static async Task<string> GetSomethingAsync()
{
    return await Task.Run(() => "something");
}

上下文在进入等待之前被捕获,并且上下文是主线程。因此,在等待之后必须恢复上下文,以便将继续安排在主线程中运行。由于此时主线程已被阻塞,因此它无法处理继续操作,从而导致死锁。

要防止捕获上下文,您可以将此awaitConfigureAwait(false)进行配置。


更新:通过 continuation 表示等待之后出现在GetSomethingAsync中的代码。尽管此后没有代码,但似乎编译器会费心为该方法的此no-op部分创建实际的延续(否则在您的示例中不应出现死锁)。

应注意,编译器将async方法转换为包含多个小型任务的Task方法。执行路径中遇到的每个await都会导致创建一个微型任务,该微型任务是等待任务的延续。所有这些微型任务都一个接一个地完成,最后一个微型任务的完成标志着async方法返回的“主任务”的完成。

答案 1 :(得分:1)

GetSomethingAsync()所做的一切由调用线程完成,直到某些操作(OP)无法立即完成(例如io),然后将控制流提供给调用函数。 如果然后访问返回的Result对象上的Task属性,线程将被阻塞。

这导致了一个问题,即使OP已经完成,线程也不会知道它,因为他正忙于等待Task

的完成。

但是,如果让GetSomethingAsync()被某个线程池线程(Task.Run(...)这样做)执行,则线程池线程可以完成OP,并且可以通知调用线程{{1 }}已完成。

更新:

您的第二个方法不起作用,因为该任务仍在您的主线程上启动。如果你有这种方法

Task

并在带有public static async Task DoStuffAsync() { Console.WriteLine($"Doing some stuff1 on thread {Thread.CurrentThread.ManagedThreadId}"); await Task.Delay(50); Console.WriteLine($"Doing some stuff2 on thread {Thread.CurrentThread.ManagedThreadId}"); }

的应用中运行此代码
SynchronizationContext

它将输出如下内容:

var task = DoStuffAsync();
Console.WriteLine($"Doing main stuff on thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Run(async () => await task);

因此,通过代码行Doing some stuff1 on thread 1 Doing main stuff on thread 1 Doing some stuff2 on thread 1 ,您仅实现了一个线程池线程等待原始Task.Run(async () => await task)的完成,但这反过来又创建了一个新的Task,如果没有,等待操作会导致死锁。