gpt4 book ai didi

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

转载 作者:IT王子 更新时间:2023-10-29 03:49:06 26 4
gpt4 key购买 nike

在使用 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/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com