![](/img/trans.png)
[英]What resources do AutoResetEvent / ManualResetEvent consume?
[英]Is there a .Net class to do what ManualResetEvent.PulseAll() would do (if it existed)?
是否有.Net类来执行ManualResetEvent.PulseAll()
(如果存在)会做什么?
我需要以原子方式释放一组正在等待同一信号的线程。 (我不担心“线程踩踏”用于预期用途。)
您不能使用ManualResetEvent
来执行此操作。 例如,如果您这样做:
ManualResetEventSlim signal = new ManualResetEventSlim();
// ...
signal.Set();
signal.Reset();
然后,根本没有释放等待信号的线程。
如果在Set()
和Reset()
调用之间放置Thread.Sleep(5)
,则会释放部分等待线程,但不是全部。 将睡眠时间增加到10ms可以释放所有线程。 (已使用20个线程进行了测试。)
显然,添加Thread.Sleep()
来完成这项工作是不可接受的。
但是,这与Monitor.PulseAll()
容易,我已经编写了一个小类来实现。 (我之所以编写一个类来执行此操作,是因为我们发现使用Monitor的逻辑虽然相当简单,但不够明显,因此值得拥有一个此类以简化用法)。
我的问题很简单:.Net中已经有一个类可以做到这一点?
作为参考,这是我的“ ManualResetEvent.PulseAll()
”的基本版本:
public sealed class Signaller
{
public void PulseAll()
{
lock (_lock)
{
Monitor.PulseAll(_lock);
}
}
public void Wait()
{
Wait(Timeout.Infinite);
}
public bool Wait(int timeoutMilliseconds)
{
lock (_lock)
{
return Monitor.Wait(_lock, timeoutMilliseconds);
}
}
private readonly object _lock = new object();
}
这是一个示例程序,该示例程序演示了如果您不睡在Set()和Reset()之间,则不会释放等待线程:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Demo
{
public static class Program
{
private static void Main(string[] args)
{
_startCounter = new CountdownEvent(NUM_THREADS);
for (int i = 0; i < NUM_THREADS; ++i)
{
int id = i;
Task.Factory.StartNew(() => test(id));
}
Console.WriteLine("Waiting for " + NUM_THREADS + " threads to start");
_startCounter.Wait(); // Wait for all threads to have started.
Thread.Sleep(100);
Console.WriteLine("Threads all started. Setting signal now.");
_signal.Set();
// Thread.Sleep(5); // With no sleep at all, NO threads receive the signal. Try commenting this line out.
_signal.Reset();
Thread.Sleep(1000);
Console.WriteLine("\n{0}/{1} threads received the signal.\n\n", _signalledCount, NUM_THREADS);
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
private static void test(int id)
{
_startCounter.Signal(); // Used so main thread knows when all threads have started.
_signal.Wait();
Interlocked.Increment(ref _signalledCount);
Console.WriteLine("Task " + id + " received the signal.");
}
private const int NUM_THREADS = 20;
private static readonly ManualResetEventSlim _signal = new ManualResetEventSlim();
private static CountdownEvent _startCounter;
private static int _signalledCount;
}
}
您可以使用Barrier对象。 它允许运行不确定数量的任务,然后等待所有其他任务达到该点。
如果您不知道哪个代码块中的哪些任务将作为特定的工作单元开始工作,则可以以类似于Go中的WaitGroup的方式使用它。
版本1
最高清晰度:每个PulseAll
周期开始时都急切安装新的ManualResetEvent
。
public class PulseEvent
{
public PulseEvent()
{
mre = new ManualResetEvent(false);
}
ManualResetEvent mre;
public void PulseAll() => Interlocked.Exchange(ref mre, new ManualResetEvent(false)).Set();
public bool Wait(int ms) => Volatile.Read(ref mre).WaitOne(ms);
public void Wait() => Wait(Timeout.Infinite);
};
版本2
这个版本避免了为任何没有等待者而完成的PulseAll
周期创建内部事件。 每个周期的第一个服务员进入乐观的无锁竞争,以创建并自动安装单个共享事件。
public class PulseEvent
{
ManualResetEvent mre;
public void PulseAll() => Interlocked.Exchange(ref mre, null)?.Set();
public bool Wait(int ms)
{
ManualResetEvent tmp =
mre ??
Interlocked.CompareExchange(ref mre, tmp = new ManualResetEvent(false), null) ??
tmp;
return tmp.WaitOne(ms);
}
public void Wait() => Wait(Timeout.Infinite);
};
版本3
该版本通过分配两个持久性ManualResetEvent
对象并在它们之间进行翻转来消除按周期分配,这与上面的示例相比在语义上有一些改动,如下所示:
首先,回收相同的两个锁意味着您的PulseAll
周期必须足够长,以允许所有侍者清除先前的锁。 否则,当您快速连续两次调用PulseAll
时, 先前的 PulseAll
调用推定释放的任何等待线程(但OS尚无调度的机会)可能最终会为新线程重新阻塞循环也是如此。 我主要出于理论上的考虑而提及此问题,因为除非您在亚微秒的脉冲周期内阻塞大量线程,否则这是一个有争议的问题。 您可以决定这种情况是否与您的情况有关。 如果是这样,或者不确定或谨慎,则可以始终使用上述版本1或版本2 ,而没有此限制。
在此版本中,“可以说”也有所不同(但请参见下面的段落,以证明第二点可能是不相关的),将被视为基本同时的对PulseAll
调用合并,这意味着除多个“同时”调用者中的一个之外的所有调用者都将变为NOP 。 此类行为并非没有先例(请参见此处的“备注” ),并且可能会有所希望,具体取决于应用程序。
请注意,与错误,理论上的缺陷或并发错误相反,必须将后一点视为合理的设计选择。 这是因为在多个同时使用PulseAll
情况下, Pulse锁本来就是模棱两可的:具体来说,无法证明没有被单个指定的脉冲发生器释放的任何侍者一定会被其他合并/清除的脉冲之一释放要么。
PulseAll
,这种类型的锁并非旨在自动地对PulseAll
调用者进行序列化,实际上实际上并非如此,因为跳过的“同时”脉冲始终可以独立地来来去去,即使完全在合并的脉冲时间之后 ,但仍在服务员到达之前(不会被脉冲)“冲动”。
public class PulseEvent
{
public PulseEvent()
{
cur = new ManualResetEvent(false);
alt = new ManualResetEvent(true);
}
ManualResetEvent cur, alt;
public void PulseAll()
{
ManualResetEvent tmp;
if ((tmp = Interlocked.Exchange(ref alt, null)) != null) // try claiming 'pulser'
{
tmp.Reset(); // prepare for re-use, ending previous cycle
(tmp = Interlocked.Exchange(ref cur, tmp)).Set(); // atomic swap & pulse
Volatile.Write(ref alt, tmp); // release claim; re-allow 'pulser' claims
}
}
public bool Wait(int ms) => cur.WaitOne(ms); // 'cur' is never null (unlike 'alt')
public void Wait() => Wait(Timeout.Infinite);
};
最后,有一些一般性的看法。 在这里和这种类型的代码中,一个重要的重复主题通常是,在仍然公开可见的情况下,不得将ManualResetEvent
更改为信号状态(即,通过调用Set
)。 在上面的代码中,我们使用Interlocked.Exchange
原子地更改“ cur”中的活动锁的身份(在这种情况下,通过在备用服务器中立即交换),并在Set
之前执行此操作,以确保不存在任何错误。除了在交换时已经被阻塞的那些服务员之外,还有更多新的服务员添加到了ManualResetEvent
。
只有在此交换之后,才可以通过在我们的(现在)私有副本上调用Set
来释放那些等待线程。 如果我们在尚未发布的ManualResetEvent
上调用Set
,那么实际上错过了瞬时脉冲的迟到服务员可能仍会看到打开锁和驶过而无需等待下一个,例如根据定义是必需的。
有趣的是,这意味着,即使在直觉上感觉“脉冲”发生的确切时间应该与Set
的调用相一致,实际上更准确地说是在Interlocked.Exchange
。因为这是严格确定之前/之后截止时间并密封要释放的一组最终服务员(如果有)的操作。
因此,错过分界线并在到达后立即到达的服务员必须只能看到并阻止当前指定用于下一个周期的事件,即使尚未通知当前周期,也是如此。 ,也没有释放它的任何等待线程,全部都是为了确保“瞬时”脉冲的正确性。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.