c# - 为什么 Monitor.PulseAll 在信号线程中导致 "stepping stair"延迟模式?

标签 c# .net multithreading latency

在使用 Monitor.PulseAll() 进行线程同步的库中,我注意到从调用 PulseAll(...) 到线程被唤醒的延迟似乎遵循“阶梯”分布 - - 步幅极大。被唤醒的线程几乎没有做任何工作;并几乎立即返回等待监视器。例如,在一个有 12 个内核和 24 个线程等待监视器的盒子上(2x Xeon5680/Gulftown;每个处理器 6 个物理内核;禁用 HT),脉冲和线程唤醒之间的延迟是这样的:

Latency using Monitor.PulseAll(); 3rd party library

前 12 个线程(注意我们有 12 个内核)需要 30 到 60 微秒来响应。然后我们开始有很大的跳跃;稳定期在 700、1300、1900 和 2600 微秒左右。

我能够使用下面的代码独立于第 3 方库成功地重新创建此行为。这段代码所做的是启动大量线程(更改 numThreads 参数),这些线程只是在监视器上等待,读取时间戳,将其记录到 ConcurrentSet,然后立即返回等待。每秒钟 PulseAll() 唤醒所有线程。它执行 20 次,并向控制台报告第 10 次迭代的延迟。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Diagnostics;

namespace PulseAllTest
{
    class Program
    {
        static long LastTimestamp;
        static long Iteration;
        static object SyncObj = new object();
        static Stopwatch s = new Stopwatch();
        static ConcurrentBag<Tuple<long, long>> IterationToTicks = new ConcurrentBag<Tuple<long, long>>();

        static void Main(string[] args)
        {
            long numThreads = 32;

            for (int i = 0; i < numThreads; ++i)
            {
                Task.Factory.StartNew(ReadLastTimestampAndPublish, TaskCreationOptions.LongRunning);
            }

            s.Start();
            for (int i = 0; i < 20; ++i)
            {
                lock (SyncObj)
                {
                    ++Iteration;
                    LastTimestamp = s.Elapsed.Ticks;
                    Monitor.PulseAll(SyncObj);
                }
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }

            Console.WriteLine(String.Join("\n",
                from n in IterationToTicks where n.Item1 == 10 orderby n.Item2 
                    select ((decimal)n.Item2)/TimeSpan.TicksPerMillisecond));
            Console.Read();
        }

        static void ReadLastTimestampAndPublish()
        {
            while(true)
            {
                lock(SyncObj)
                {
                    Monitor.Wait(SyncObj);
                }
                IterationToTicks.Add(Tuple.Create(Iteration, s.Elapsed.Ticks - LastTimestamp));
            }
        }
    }
}

使用上面的代码,这是一个启用了 8 个内核/w 超线程(即任务管理器中的 16 个内核)和 32 个线程(*2x Xeon5550/Gainestown;每个处理器 4 个物理内核;启用 HT)的机器上的延迟示例):

Latency using Monitor.PulseAll(), sample code

编辑:为了尝试将 NUMA 排除在等式之外,下面是在 Core i7-3770(Ivy Bridge)上运行具有 16 个线程的示例程序的图表; 4 个物理内核;超线程启用:

Latency using Monitor.PulseAll(), sample code, no NUMA

谁能解释为什么 Monitor.PulseAll() 会这样?

编辑2:

为了尝试证明这种行为并不是同时唤醒一堆线程所固有的,我使用事件复制了测试程序的行为;我没有测量 PulseAll() 的延迟,而是测量了 ManualResetEvent.Set() 的延迟。该代码正在创建多个工作线程,然后等待同一 ManualResetEvent 对象上的 ManualResetEvent.Set() 事件。当事件被触发时,他们会进行延迟测量,然后立即等待他们自己的每线程 AutoResetEvent。在下一次迭代之前(500 毫秒之前),ManualResetEvent 为 Reset(),然后每个 AutoResetEvent 为 Set(),因此线程可以返回等待共享的 ManualResetEvent。

我对发布这个犹豫不决,因为它可能是一个巨大的红色听证会(我没有声称事件和监视器的行为相似)加上它使用一些绝对糟糕的做法让事件表现得像监视器(我喜欢/讨厌如果我将其提交给代码审查,看看我的同事会怎么做);但我认为结果很有启发性。

本次测试与原测试在同一台机器上完成;一个 2xXeon5680/Gulftown;每个处理器 6 个内核(总共 12 个内核);禁用超线程。

ManualResetEventLatency

如果不是很明显这与 Monitor.PulseAll 有多么不同;这是叠加在最后一张图上的第一张图:

ManualResetEventLatency vs. Monitor Latency

用于生成这些测量值的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Diagnostics;

namespace MRETest
{
    class Program
    {
        static long LastTimestamp;
        static long Iteration;
        static ManualResetEventSlim MRES = new ManualResetEventSlim(false);
        static List<ReadLastTimestampAndPublish> Publishers = 
            new List<ReadLastTimestampAndPublish>();
        static Stopwatch s = new Stopwatch();
        static ConcurrentBag<Tuple<long, long>> IterationToTicks = 
            new ConcurrentBag<Tuple<long, long>>();

        static void Main(string[] args)
        {
            long numThreads = 24;
            s.Start();

            for (int i = 0; i < numThreads; ++i)
            {
                AutoResetEvent ares = new AutoResetEvent(false);
                ReadLastTimestampAndPublish spinner = new ReadLastTimestampAndPublish(
                    new AutoResetEvent(false));
                Task.Factory.StartNew(spinner.Spin, TaskCreationOptions.LongRunning);
                Publishers.Add(spinner);
            }

            for (int i = 0; i < 20; ++i)
            {
                ++Iteration;
                LastTimestamp = s.Elapsed.Ticks;
                MRES.Set();
                Thread.Sleep(500);
                MRES.Reset();
                foreach (ReadLastTimestampAndPublish publisher in Publishers)
                {
                    publisher.ARES.Set();
                }
                Thread.Sleep(500);
            }

            Console.WriteLine(String.Join("\n",
                from n in IterationToTicks where n.Item1 == 10 orderby n.Item2
                    select ((decimal)n.Item2) / TimeSpan.TicksPerMillisecond));
            Console.Read();
        }

        class ReadLastTimestampAndPublish
        {
            public AutoResetEvent ARES { get; private set; }

            public ReadLastTimestampAndPublish(AutoResetEvent ares)
            {
                this.ARES = ares;
            }

            public void Spin()
            {
                while (true)
                {
                    MRES.Wait();
                    IterationToTicks.Add(Tuple.Create(Iteration, s.Elapsed.Ticks - LastTimestamp));
                    ARES.WaitOne();
                }
            }
        }
    }
}

最佳答案

这些版本之间的一个区别是,在 PulseAll 的情况下 - 线程立即重复循环,再次锁定对象。

你有12个核心,所以有12个线程在运行,执行循环,再次进入循环,锁定对象(一个接一个),然后进入等待状态。其他线程一直在等待。在 ManualEvent 情况下,您有两个事件,因此线程不会立即重复循环,而是在 ARES 事件上被阻塞 - 这允许其他线程更快地获取锁所有权。

我通过在 ReadLastTimestampAndPublish 的循环末尾添加 sleep 来模拟 PulseAll 中的类似行为。这让其他线程可以更快地锁定 syncObj,并且似乎可以提高我从程序中获得的数字。

static void ReadLastTimestampAndPublish()
{
    while(true)
    {
        lock(SyncObj)
        {
            Monitor.Wait(SyncObj);
        }
        IterationToTicks.Add(Tuple.Create(Iteration, s.Elapsed.Ticks - LastTimestamp));
        Thread.Sleep(TimeSpan.FromMilliseconds(100));   // <===
    }
}

关于c# - 为什么 Monitor.PulseAll 在信号线程中导致 "stepping stair"延迟模式?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20860709/

相关文章:

C#:尝试一个简单的项目

c# - 从 C# 客户端向 python 服务器发送 http POST 请求时出错

c# - 为什么 IList<T> 不提供 List<T> 提供的所有方法?我应该使用哪个?

C: 如果文件描述符被删除,阻塞读取应该返回

multithreading - 使用 Hibernate SessionFactory 的多线程问题

c# - 将一个值与整个数组进行比较? (C#)

c# - CLR 在调用 C++ 函数时如何避免 thunking?

.net - 在 ReBus 中处理消息期间立即执行回复

.net - 数组与列表的性能

mysql - 行级锁,或者读时锁