为啥级联 ManualResetEvents 的多次等待会使执行时间增加三倍?

Posted

技术标签:

【中文标题】为啥级联 ManualResetEvents 的多次等待会使执行时间增加三倍?【英文标题】:Why do multiple waits on cascaded ManualResetEvents triple the execution time?为什么级联 ManualResetEvents 的多次等待会使执行时间增加三倍? 【发布时间】:2021-09-21 21:41:35 【问题描述】:

我已经尽可能简化了场景。在下面,您可以找到完整的代码,您可以使用它自己测试程序。在我的用例中,我必须等待来自另一个线程的重置事件 - 让我们称之为 subEventsubEvent 事件再次发出信号,另一个线程已完成。让我们称之为 resetEvent。在这个简化版本中,我在 200 毫秒后发出 resetEvent 信号,并立即发出 subEvent 信号。这是在SingleCascaded 方法中实现的。

此示例按预期工作。我的输出是

Sub Thread: 229 ms
Main 229 ms
Thread: 229 ms

嗯,30 毫秒仍然是一个沉重的代价,但是当我增加两个重置事件的等待次数时,它变得更糟 - 这更接近我在程序中的场景。在 MultipleCascaded 方法中,我创建了 5 个等待 resetEvent 的线程(在 200 毫秒后设置)和 15 个等待 subEvent 的线程(在之后设置) resetEvent

Set reset event 641
Main 641 ms
Thread 4: 660 ms
Thread 1: 661 ms
Thread 3: 662 ms
Sub Thread 4.0.: 662 ms
Sub Thread 4.1.: 663 ms
Sub Thread 0.0.: 661 ms
Sub Thread 0.1.: 661 ms
Sub Thread 1.1.: 664 ms
Sub Thread 1.2.: 664 ms
Sub Thread 3.0.: 665 ms
Sub Thread 3.1.: 665 ms
Sub Thread 3.2.: 666 ms
Thread 0: 661 ms
Sub Thread 4.2.: 663 ms

这里有趣的部分是,reset 事件的Set\Wait 方法不会产生时间间隔。在这种情况下,定时器似乎无法在指定时间之后执行。

当然,在我的应用程序中,这些都不是计时器,例如 I/O 访问、计算等。但是效果是一样的。执行过程中有一个明显的时间间隔,似乎什么也没发生。我在其他情况下也遇到了问题(例如过度使用 async/await)。但是我无法在一个小例子中重现它。

我现在的问题是。怎么了?更重要的是,我该如何解决这个问题?这是我发现的问题所在:

线程池中的所有线程都被阻塞 - 有很多线程可以同时处于活动状态 计时器 - 正如我在实际应用程序中提到的,我负责 I/O 访问和计算工作。没有计时器。 这个特定重置事件的实现 - 当我使用 ManualResetEventAutoResetEvent 并且在我的旧应用程序中使用 async/await 时也会发生这种情况;在这种情况下,它是TaskCompletionSource

这里是代码供你测试。

class Program

    static void Main(string[] args)
    
        SingleCascaded();
        MultipleCascaded();
    

    private static void MultipleCascaded()
    
        Console.WriteLine("Test multiple waits cascaded");
        ManualResetEventSlim resetEvent = new ManualResetEventSlim(false);
        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int j = 0; j < 5; j++)
        
            ThreadPool.QueueUserWorkItem(state =>
            
                ManualResetEventSlim subEvent = new ManualResetEventSlim(false);
                for (int k = 0; k < 3; k++)
                
                    ThreadPool.QueueUserWorkItem(state =>
                    
                        subEvent.Wait();
                        Console.WriteLine(
                            $"Sub Thread ((Tuple<int, int>) state).Item1.((Tuple<int, int>) state).Item2.: watch.ElapsedMilliseconds ms");
                    , new Tuple<int, int>((int) state, k));
                

                resetEvent.Wait();
                subEvent.Set();
                Console.WriteLine($"Thread state: watch.ElapsedMilliseconds ms");
            , j);
        

        using Timer timer = new Timer(state =>
                                      
                                          Console.WriteLine($"Set reset event watch.ElapsedMilliseconds");
                                          resetEvent.Set();
                                      , null, 200,
                                      Timeout.Infinite);
    

    private static void SingleCascaded()
    
        Console.WriteLine("Test single waits cascaded");
        
        ManualResetEventSlim resetEvent = new ManualResetEventSlim(false);
        Stopwatch watch = new Stopwatch();
        watch.Start();
        
        ThreadPool.QueueUserWorkItem(state =>
        
            ManualResetEventSlim subEvent = new ManualResetEventSlim(false);
            ThreadPool.QueueUserWorkItem(state =>
            
                subEvent.Wait();
                Console.WriteLine(
                    $"Sub Thread: watch.ElapsedMilliseconds ms");
            );

            resetEvent.Wait();
            subEvent.Set();
            Console.WriteLine($"Thread: watch.ElapsedMilliseconds ms");
        );

        using Timer timer = new Timer(state =>  resetEvent.Set(); , null, 200,
                                      Timeout.Infinite);
        resetEvent.Wait();
        Console.WriteLine($"Main watch.ElapsedMilliseconds ms");
    

【问题讨论】:

似乎不需要级联调用。如果我只有一个 ResetEvent 和许多线程在等待它,它也可以工作。 我认为这既与ManualResetEventSlim 等待的方式(“忙于旋转一小段时间”)有关,也与 Timer 回调在 ThreadPool 上运行并排队等待执行的事实有关当线程可用时,这不是立即可用,但在您的情况下是 600 毫秒,而不是预期的 200 毫秒。即使您的应用程序中没有 Timer,如果您在 ThreadPool 上运行 I/O,也会出现同样的问题。不确定这是否仍然相关:docs.microsoft.com/en-us/archive/msdn-magazine/2010/september/… 【参考方案1】:

根据我的评论,这是因为循环使用 ThreadPool 中的所有可用线程,然后需要开始使用算法添加更多线程,并且计时器回调 (resetevent.set) 运行在一个线程池线程。此外,即使应用程序中没有计时器,如果您的 I/O 在线程池线程上运行,问题也是一样的。

对此的一种解决方案是在循环之前使用 this 将立即可用的按需线程的数量设置为更高的数量:

// Get number of immediately available threads
ThreadPool.GetMinThreads(out var a, out var b);
Console.WriteLine($"worker         : " + a); // on my machine: 12
Console.WriteLine($"completion port: " + b); // on my machine: 12

// Set minimum number of immediately available threads.
// First parameter (worked threads) is relevant here:
ThreadPool.SetMinThreads(30, 12);

您可以通过改变这些数字或改变循环中的上限(= 所需线程数)来验证这一点。它与Reset 事件的类型无关,因为在这两种情况下,工作线程都会阻塞Wait

将计时器替换为Thread.Sleep() 也可以验证这一点。这不在ThreadPool 上运行;相反,它阻塞了主线程:

Thread.Sleep(10000); // Block main thread for 10secs for demo purposes.
resetEvent.Set();

然后您可以看到 12 个线程立即执行,然后在我的机器上每 500-1000 秒一个接一个地添加下一个。 10 秒后,设置事件并解除阻塞:

START
    Thread wait: 13 ms
    Thread wait: 13 ms
    Thread wait: 13 ms
    Thread wait: 13 ms
    Thread wait: 13 ms
        Sub Thread wait: 16 ms
        Sub Thread wait: 16 ms
        Sub Thread wait: 17 ms
        Sub Thread wait: 19 ms
        Sub Thread wait: 21 ms
        Sub Thread wait: 23 ms
        Sub Thread wait: 25 ms    // 12th thread
        Sub Thread wait: 1014 ms  // no more threads available, start adding them one by one
        Sub Thread wait: 2003 ms
        Sub Thread wait: 2517 ms
        Sub Thread wait: 3511 ms
        Sub Thread wait: 4518 ms
        Sub Thread wait: 5517 ms
        Sub Thread wait: 6512 ms
        Sub Thread wait: 7507 ms
Set event: 10013 ms               // set: all threads are released
        Sub Thread go  : 10013 ms
    Thread go  : 10013 ms
        Sub Thread go  : 10013 ms
        Sub Thread go  : 10013 ms

您还可以将 Sleep() 设置为 200 毫秒而不是 10 秒。您会看到,在这种情况下,线程将在大约 200 毫秒后运行,因为不需要像 Timer 那样在 ThreadPool 上等待。

MS 文档:SetMinThreadsTimer

【讨论】:

以上是关于为啥级联 ManualResetEvents 的多次等待会使执行时间增加三倍?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Django 对外键进行级联删除?

为啥我会通过此表关系获得“多个级联路径”?

触发器级联为啥会失败

为啥 Django 模型级联删除在现实世界中会失败?

为啥手动定义的 Spring Data JPA 删除查询不会触发级联?

为啥 enumerable: false 不会级联到 TypeScript 中的继承类?