【问题标题】:Task.Run continues on the same thread causing deadlockTask.Run 在同一个线程上继续导致死锁
【发布时间】:2018-03-11 14:16:45
【问题描述】:

考虑以下我将同步等待的异步方法。等一下,我知道。我知道这被认为是不好的做法和causes deadlocks,但我完全同意conscious,并采取措施通过使用Task.Run 包装代码来防止死锁。

    private async Task<string> BadAssAsync()
    {
        HttpClient client = new HttpClient();

        WriteInfo("BEFORE AWAIT");

        var response = await client.GetAsync("http://google.com");

        WriteInfo("AFTER AWAIT");

        string content = await response.Content.ReadAsStringAsync();

        WriteInfo("AFTER SECOND AWAIT");

        return content;
    }

如果这样调用:BadAssAsync().Result,则此代码肯定会死锁(在具有 SyncronizationContext 的环境中,它在 ASP.NET 等单线程上安排任务)。

我面临的问题是,即使使用这个“安全”包装器,它仍然偶尔会死锁。

    private T Wait1<T>(Func<Task<T>> taskGen)
    {
        return Task.Run(() =>
        {
            WriteInfo("RUN");

            var task = taskGen();

            return task.Result;
        }).Result;
    }

这些“WriteInfo”行是有目的的。这些调试行让我看到它偶尔发生的原因是Task.Run 中的代码,有点神秘,是由开始服务请求的同一个线程执行的。这意味着它的 AspNetSynchronizationContext 为SyncronizationContext 并且肯定会死锁。

这里是调试输出:

***(工作正常)
开始:TID:17; SCTX:System.Web.AspNetSynchronizationContext;调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler
运行:TID:45; SCTX:&ltnull> 调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler
等待之前:TID:45; SCTX:&ltnull> 调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler
等待后:TID:37; SCTX:&ltnull> 调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler
第二次等待后:TID:37; SCTX:&ltnull> 调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler

***(死锁)
开始:TID:48; SCTX:System.Web.AspNetSynchronizationContext;调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler
运行:TID:48; SCTX:System.Web.AspNetSynchronizationContext;调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler
等待之前:TID:48; SCTX:System.Web.AspNetSynchronizationContext;调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler

注意Task.Run() 中的代码在 TID=48 的同一线程上继续。

问题是为什么会这样?为什么 Task.Run 在同一个线程上运行代码允许 SyncronizationContext 仍然有效?

这里是WebAPI控制器的完整示例代码:https://pastebin.com/44RP34Ye和完整示例代码here。

更新。这是重现问题根本原因的较短控制台应用程序代码示例 - 在等待的调用线程上调度 Task.Run 委托。这怎么可能?

static void Main(string[] args)
{
    WriteInfo("\n***\nBASE");

    var t1 = Task.Run(() =>
    {
        WriteInfo("T1");

        Task t2 = Task.Run(() =>
        {
            WriteInfo("T2");
        });

        t2.Wait();
    });

    t1.Wait();
}
基础:TID:1; SCTX:&ltnull> 调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler
T1:TID:3; SCTX:&ltnull> 调度器:System.Threading.Tasks.ThreadPoolTask​​Scheduler
T2:TID:3; SCTX: &ltnull> 调度器: System.Threading.Tasks.ThreadPoolTask​​Scheduler

【问题讨论】:

  • taking measures to prevent deadlocks via wrapping code with Task.Run. 如您所见,这并不能解决您的问题。您不应该同步等待异步操作。它本质上是有问题的。没有任何“简单的技巧”可以让它变得好起来。如果您不想不断处理此类问题,则需要消除该潜在问题,并使您的代码完全异步或完全同步。
  • @NeilBostian:“任务”本身不会“运行”,Task.Run 除外,它确实在后台线程上运行。您正在考虑调用异步方法。
  • @NeilBostian Task 是异步工作的代表。根据您使用它们的方式以及它们的实现方式,它们最终可能会或可能不会并行完成这项工作。 “任务将始终在产生它的同一线程上运行”这句话甚至没有意义。许多任务根本不涉及在线程中运行代码,而那些通常不涉及在调用线程中运行代码(因为它们应该是异步的)。您的链接答案没有正确使用其术语,因为它声称在并行完成工作时没有并行完成工作。
  • @SLaks,“你的方法根本没有帮助” - 感谢您注意到这一点!问题是为什么?
  • 好问题! @All:问题不在于“良好实践”,而在于 TPL 的内部运作。

标签: c# .net multithreading asynchronous deadlock


【解决方案1】:

在 IdentityServer 的内部我 found 另一种有效的技术。它非常接近问题中规定的不可靠技术。你会在最后或这个答案中找到源代码。

这项技术的魔力应该归功于Unwrap() 方法。在内部调用 Task&lt;Task&lt;T&gt;&gt; 时,它会创建一个新的“promise task”,只要两个(我们执行的任务和嵌套的任务)都完成。

这成功且不会产生死锁概率的原因很简单——承诺任务是no subjects for inlining,这是有道理的,因为没有“工作”可以内联。反过来,这意味着我们阻塞当前线程并让默认调度程序(ThreadPoolTaskScheduler)在没有SynchronizationContext 的情况下在新线程中完成工作。

internal static class AsyncHelper
{
    private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

    public static void RunSync(Func<Task> func)
    {
        _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
    }

    public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
    }
}

此外,Task.Run 的签名会隐式执行 Unwrap,这会导致以下最短的安全实现。

T SyncWait<T>(Func<Task<T>> f) => Task.Run(f).Result;

【讨论】:

    【解决方案2】:

    我们和我的一个好朋友能够通过inspecting stack traces 和阅读.net reference source 解决这个问题。很明显,问题的根本原因是Task.Run 的有效负载正在任务上调用Wait 的线程上执行。事实证明,这是 TPL 进行的性能优化,目的是不启动额外线程并防止宝贵线程无所事事。

    这是 Stephen Toub 的一篇文章,描述了这种行为:https://blogs.msdn.microsoft.com/pfxteam/2009/10/15/task-wait-and-inlining/。

    Wait 可以简单地阻塞一些同步原语,直到 目标任务完成,在某些情况下,这正是它所做的。 但是阻塞线程是一项昂贵的冒险,因为线程会被捆绑 大量的系统资源,而阻塞的线程是死重的 直到它能够继续执行有用的工作。相反,等待 更喜欢执行有用的工作而不是阻塞,它有有用的 触手可及:正在等待的任务。如果任务是 Wait'd on 已经开始执行,Wait 必须阻塞。然而, 如果还没有开始执行,Wait 或许可以拉取目标 任务从它排队的调度程序中出来并内联执行 在当前线程上。

    教训:如果你真的需要同步等待异步工作,Task.Run 的技巧是不可靠的。你必须将SyncronizationContext清零,等待,然后返回SyncronizationContext。

    【讨论】:

    • IMO,教训是技巧永远不可靠。如果您想要手动线程管理,请明确执行,不要(尝试)依赖其他方法的副作用。
    猜你喜欢
    • 2021-08-02
    • 1970-01-01
    • 1970-01-01
    • 2023-01-08
    • 2020-08-29
    • 2021-03-14
    • 1970-01-01
    • 1970-01-01
    • 2011-12-22
    相关资源
    最近更新 更多