在使用Monitor.PulseAll()进行线程同步的库中,我注意到从PulseAll(...)被调用到线程被唤醒的时间的延迟似乎遵循“踩台阶”分布 - - 步数极大。被唤醒的线程几乎没有工作;并几乎立即回到监视器上等待。例如,在一个包含12个内核,24个线程等待监视器的盒子上(2x Xeon5680 / Gulftown;每个处理器6个物理内核; HT禁用),Pulse和线程唤醒之间的延迟是这样的:
前12个线程(注意我们有12个核心)需要30到60微秒才能响应。然后我们开始获得非常大的跳跃;高原在700,1300,1900和2600微秒左右。
使用下面的代码,我能够独立于第三方库成功重新创建此行为。这段代码的作用是启动大量线程(更改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)的盒子上的延迟示例):
编辑:为了尝试将NUMA排除在等式之外,下面是运行示例程序的图表,其中Core i7-3770(Ivy Bridge)上有16个线程; 4个物理核心; HT启用:
有人可以解释为什么Monitor.PulseAll()会以这种方式运作吗?
EDIT2:
为了尝试表明这种行为不是同时唤醒一堆线程所固有的,我已经使用Events复制了测试程序的行为;而不是测量PulseAll()的延迟,我正在测量ManualResetEvent.Set()的延迟。代码创建了许多工作线程,然后在同一个ManualResetEvent对象上等待ManualResetEvent.Set()事件。当事件被触发时,他们会进行延迟测量,然后立即等待他们自己的每个线程AutoResetEvent。在下一次迭代(之前500ms)之前,ManualResetEvent是Reset(),然后每个AutoResetEvent都是Set(),因此线程可以返回等待共享的ManualResetEvent。
我犹豫是否发布了这个因为它可能是一个巨大的红色听证会(我没有声称事件和监视器的行为类似)加上它使用一些绝对可怕的做法让事件表现得像一个监视器(我喜欢/讨厌看看我的同事如果将其提交给代码审查会做什么);但我认为结果很有启发性。
该测试在与原始测试相同的机器上进行; 2xXeon5680 / Gulftown;每个处理器6个内核(总共12个内核);超线程已禁用。
如果不明显,这与Monitor.PulseAll有多么不同;这是覆盖在最后一个图上的第一个图:
用于生成这些测量的代码如下:
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();
}
}
}
}
}
答案 0 :(得分:1)
这些版本之间的一个区别是,在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)); // <===
}
}
答案 1 :(得分:1)
首先,这不是一个答案,只是通过查看SSCLI来了解到底发生了什么。其中大部分都远远超出我的想象,但仍然很有趣。
兔子洞的行程始于对Monitor.PulseAll
的呼叫,该呼叫在C#中实现:
clr\src\bcl\system\threading\monitor.cs
:
namespace System.Threading
{
public static class Monitor
{
// other methods omitted
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern void ObjPulseAll(Object obj);
public static void PulseAll(Object obj)
{
if (obj==null) {
throw new ArgumentNullException("obj");
}
ObjPulseAll(obj);
}
}
}
InternalCall方法在clr\src\vm\ecall.cpp
中路由:
FCFuncStart(gMonitorFuncs)
FCFuncElement("Enter", JIT_MonEnter)
FCFuncElement("Exit", JIT_MonExit)
FCFuncElement("TryEnterTimeout", JIT_MonTryEnter)
FCFuncElement("ObjWait", ObjectNative::WaitTimeout)
FCFuncElement("ObjPulse", ObjectNative::Pulse)
FCFuncElement("ObjPulseAll", ObjectNative::PulseAll)
FCFuncElement("ReliableEnter", JIT_MonReliableEnter)
FCFuncEnd()
ObjectNative
住在clr\src\vm\comobject.cpp
:
FCIMPL1(void, ObjectNative::PulseAll, Object* pThisUNSAFE)
{
CONTRACTL
{
MODE_COOPERATIVE;
DISABLED(GC_TRIGGERS); // can't use this in an FCALL because we're in forbid gc mode until we setup a H_M_F.
THROWS;
SO_TOLERANT;
}
CONTRACTL_END;
OBJECTREF pThis = (OBJECTREF) pThisUNSAFE;
HELPER_METHOD_FRAME_BEGIN_1(pThis);
//-[autocvtpro]-------------------------------------------------------
if (pThis == NULL)
COMPlusThrow(kNullReferenceException, L"NullReference_This");
pThis->PulseAll();
//-[autocvtepi]-------------------------------------------------------
HELPER_METHOD_FRAME_END();
}
FCIMPLEND
OBJECTREF
在Object
之上散布了一些魔力(->
运算符已超载),因此OBJECTREF->PulseAll()
实际上是Object->PulseAll()
,它在{ {1}}并将呼叫转发至clr\src\vm\object.h
:
ObjHeader->PulseAll
class Object
{
// snip
public:
// snip
ObjHeader *GetHeader()
{
LEAF_CONTRACT;
return PTR_ObjHeader(PTR_HOST_TO_TADDR(this) - sizeof(ObjHeader));
}
// snip
void PulseAll()
{
WRAPPER_CONTRACT;
GetHeader()->PulseAll();
}
// snip
}
检索ObjHeader::PulseAll
,SyncBlock
使用AwareLock
进行Enter
和Exit
锁定对象。 AwareLock
(clr\src\vm\syncblk.cpp
)使用CLREvent
(clr\src\vm\synch.cpp
)作为MonitorEvent
(CLREvent::CreateMonitorEvent(SIZE_T)
)创建,调用UnsafeCreateEvent
( clr\src\inc\unsafe.h
)或托管环境的同步方法。
clr\src\vm\syncblk.cpp
:
void ObjHeader::PulseAll()
{
CONTRACTL
{
INSTANCE_CHECK;
THROWS;
GC_TRIGGERS;
MODE_ANY;
INJECT_FAULT(COMPlusThrowOM(););
}
CONTRACTL_END;
// The following code may cause GC, so we must fetch the sync block from
// the object now in case it moves.
SyncBlock *pSB = GetBaseObject()->GetSyncBlock();
// GetSyncBlock throws on failure
_ASSERTE(pSB != NULL);
// make sure we own the crst
if (!pSB->DoesCurrentThreadOwnMonitor())
COMPlusThrow(kSynchronizationLockException);
pSB->PulseAll();
}
void SyncBlock::PulseAll()
{
CONTRACTL
{
INSTANCE_CHECK;
NOTHROW;
GC_NOTRIGGER;
MODE_ANY;
}
CONTRACTL_END;
WaitEventLink *pWaitEventLink;
while ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL)
pWaitEventLink->m_EventWait->Set();
}
DequeueThread
使用crst
(clr\src\vm\crst.cpp
),这是关键部分的包装。 m_EventWait
是手册CLREvent
。
所以,所有这些都是使用操作系统原语,除非默认的主机提供程序是覆盖的东西。