【问题标题】:Task.Run with a WaitAll is blocking带有 WaitAll 的 Task.Run 被阻塞
【发布时间】:2021-12-27 14:45:56
【问题描述】:

我在这里是因为我在使用此代码时出现了一种奇怪的行为: 但在那之前,我知道这样做是一个非常糟糕的做法,所以它甚至没有在现实中使用我只是想了解幕后发生的事情,但我的知识真的很差。

这是有问题的代码:

int worker = 0;
int io = 0;
Console.WriteLine($"Worker thread {worker} Io thread {io}");
ThreadPool.GetAvailableThreads(out worker, out io);
ThreadPool.GetMaxThreads(out var workerThreadsMax, out var completionPortThreadsMax);

Console.WriteLine($"Worker thread {workerThreadsMax - worker} Io thread {completionPortThreadsMax - io}");
for (int i = 0; i < 100; i++)
{
                
    Task.Run(() =>
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " - Running thread");
        ThreadPool.GetAvailableThreads(out var worker2, out var io2);
        ThreadPool.GetMaxThreads(out var workerThreadsMax2, out var completionPortThreadsMax2);
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - Worker thread {workerThreadsMax2 - worker2} Io thread {completionPortThreadsMax2 - io2}");

        var t1 = Task.Delay(5000);
        var t2 = Task.Delay(5000);
        Task.WaitAll(t1, t2);

        Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " - End of thread");
        ThreadPool.GetAvailableThreads(out worker2, out io2);
        ThreadPool.GetMaxThreads(out workerThreadsMax2, out completionPortThreadsMax2);
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - Worker thread {workerThreadsMax2 - worker2} Io thread {completionPortThreadsMax2 - io2}");
    });
}

Console.ReadLine();

所以我在这段代码中试图做的是运行或至少排队 500 个任务(我知道很多,但很好奇),同时仍然显示来自 ThreadPool 的活动线程的数量。所以在第一行中,我在 ThreadPool 中有 0 个工作线程,这是有道理的,只要我还没有启动任何任务。但是当第一个任务运行时,有 8 个活动线程。这里发生了一件奇怪的事情: 每秒或更少产生一个新线程(但不是立即产生),这不是一个真正的问题,但我不明白为什么任务被阻止?即使完成了 250 毫秒的延迟,任务也不会自行结束,即使超过一分钟,它仍然会阻塞在 Task.WaitAll 线上:

Worker thread 0 Io thread 0
Worker thread 0 Io thread 0
8 - Running thread
8 - Worker thread 8 Io thread 0
6 - Running thread
6 - Worker thread 8 Io thread 0
10 - Running thread
10 - Worker thread 8 Io thread 0
7 - Running thread
7 - Worker thread 8 Io thread 0
11 - Running thread
11 - Worker thread 8 Io thread 0
5 - Running thread
9 - Running thread
9 - Worker thread 8 Io thread 0
12 - Running thread
12 - Worker thread 8 Io thread 0
5 - Worker thread 8 Io thread 0
13 - Running thread
13 - Worker thread 9 Io thread 0
14 - Running thread
14 - Worker thread 10 Io thread 0
15 - Running thread
15 - Worker thread 11 Io thread 0
16 - Running thread
16 - Worker thread 12 Io thread 0
17 - Running thread
17 - Worker thread 13 Io thread 0
18 - Running thread
18 - Worker thread 14 Io thread 0

这里是否发生了死锁?如果有人可以向我解释这一点,那就太好了。谢谢。

编辑:对于那些建议使用 async 和 await Task.WhenAll(..) 的人,你是完全正确的!但正如我所说,这是出于测试目的,我不会在现实中这样做,而是使用 async/await 语句。但是我们正在和朋友一起测试一些关于同步和异步任务的东西,在测试同步方式时,我们遇到了这个问题,但不知道发生了什么。感谢那些澄清这一点的人。很有启发性。

【问题讨论】:

  • 应该是await Task.WaitAll(....)。你需要让你的方法async释放线程
  • @Liam WaitAll 不是等待超载 AFAIK。
  • 好的,应该是WhenAll
  • 顺便说一句,你在谈论“500 个任务”的问题,但循环是for (int i = 0; i &lt; 100; i++)。您也在谈论“250ms 延迟”,但我能看到的唯一延迟是Task.Delay(5000)。
  • 这与死锁相反,您会看到线程池管理器试图解决这些未及时完成的​​线程并卡在 WaitAll() 调用上。它不知道为什么他们没有取得进展并假设它可能是由死锁引起的,允许额外的线程运行可以解决问题。对于耗时超过半秒的任务,您应该使用 TaskCreationOptions.LongRunning 选项。副作用是它现在使用常规线程而不是 tp 线程,并且不再受到池规则的限制。

标签: c# async-await task


【解决方案1】:

您的Task 应该是:

Task.Run(async () =>
    {


        var t1 = Task.Delay(5000);
        var t2 = Task.Delay(5000);
        await Task.WhenAll(t1, t2);


    });

没有async,该方法是阻塞的。 async/await 使任务非阻塞。正如Gurustron 所述,您需要使用非阻塞Task.WhenAll 而不是Task.WaitAll。见WaitAll vs WhenAll

【讨论】:

    【解决方案2】:

    让我们更详细地看一下这段代码

    var t1 = Task.Delay(5000);
    var t2 = Task.Delay(5000);
    Task.WaitAll(t1, t2);
    

    Task.Delay 本质上是一个定时器的封装,更具体地说是一个System.Threading.Timer。此计时器会将实际时间保持委托给操作系统。当计时器过去时,它将在线程池线程上引发偶数,该线程又将任务标记为已完成。这将触发对Task.WhenAll 任务的检查,以查看该任务是否可以完成,如果可以,则取消阻止。

    但是,您的测试本质上是为了耗尽线程池而设计的,从而导致典型的死锁。所有Task.WhenAll 任务都在等待一个或多个Task.Delay 完成,但这需要一个可用的线程池线程,但在等待Task.WhenAll 任务时所有线程都被阻塞。所以一切都在等待别的东西,没有什么可以运行。

    除了线程池的设计者预料到了这个问题并添加了增加线程池线程数量的机制,允许Task.Delay 完成并解决死锁。但这种机制缓慢。因此,任务完成存在巨大延迟也就不足为奇了。

    由于这是一个玩具示例,可能不需要解决方案,但可能值得重复。不要超额订阅线程池。使用异步、非阻塞代码,或注意使用多少线程的代码。

    【讨论】:

    • 完美答案!我以为幕后发生了这样的事情,但你添加了很多细节和信息。是的,我同意你的最后一句话,这只是为了测试目的,我们正在和朋友一起做一些测试(使用异步和非异步方式),在尝试同步方式时,我们遇到了那个“问题”,无法解释为你做过。谢谢。
    猜你喜欢
    • 2019-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多