是否有.Net类来执行ManualResetEvent.PulseAll()会做什么(如果它存在)?
是否有.Net类来执行ManualResetEvent.PulseAll()
会做什么(如果它存在)?
我需要primefaces地释放一组等待相同信号的线程。 (我并不担心我的预期用法会出现“线程踩踏事件”。)
您不能使用ManualResetEvent
来执行此操作。 例如,如果你这样做:
ManualResetEventSlim signal = new ManualResetEventSlim(); // ... signal.Set(); signal.Reset();
然后根本没有释放等待信号的线程。
如果在Set()
和Reset()
调用之间放置一个Thread.Sleep(5)
,则会释放一些但不是所有等待的线程。 将睡眠时间增加到10毫秒可以释放所有线程。 (这是用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()之间hibernate,则不会释放等待线程:
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 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
周期创建内部事件。 每个周期的第一个服务员进入乐观的无锁竞赛,以创建和primefaces安装单个共享事件。
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
调用假定释放的所有等待线程 – 但操作系统尚未有机会安排 – 可能最终会被重新阻止循环也是如此。 我提到这主要是作为理论上的考虑因素,因为除非你在亚微秒脉冲周期内阻塞极端数量的线程,否则这是一个没有实际意义的问题。 您可以决定此条件是否与您的情况相关。 如果是这样,或者如果您不确定或谨慎,您可以始终使用上面的版本1或版本2 ,但没有此限制。 -
同样“可以说”不同(但请参见以下段落,为什么第二点可能certificate是无关紧要的)在这个版本中,对被认为基本上同时发生的
PulseAll
调用被合并,这意味着除了一个多个“同时”呼叫者之外的所有呼叫者都成为NOP 。 这种行为并非没有先例(参见此处的“备注” )并且可能是可取的,具体取决于应用。
请注意,后一点必须被视为合法的设计选择,而不是错误,理论缺陷或并发错误。 这是因为脉冲锁在多个同时PulseAll
情况下本质上是模糊的:具体而言,没有办法certificate任何未被单个指定脉冲发生器释放的服务员必然会被其他合并/消除脉冲之一释放无论是。
用不同的方式说,这种类型的锁不是为了primefaces序列化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
以primefaces方式更改’cur’中的活动锁的标识(在这种情况下,通过在备用中即时交换)并在Set
之前执行此操作,这对于保证不存在更多新的服务员添加到该ManualResetEvent
,超出了在交换时已被阻止的那些。
只有在这个交换之后,通过在我们的(现在 – )私有副本上调用Set
来释放那些等待的线程是安全的。 如果我们在仍然发布的时候调用ManualResetEvent
上的Set
,那么实际上错过了瞬间脉冲的迟到的服务员仍然可以看到打开的锁定并且没有等待下一个,而不是等待下一个。根据定义要求。
有趣的是,这意味着即使它可能直观地感觉到“脉冲”发生的确切时刻应该与Set
被调用一致,事实上在Interlocked.Exchange
时刻之前更正确地说它是正确的。因为这是严格确定前/后截止时间的行动,并密封了将被释放的最终服务员(如果有的话)。
因此,错过了截止并立即到达的服务员必须只能看到 – 并将阻止 – 现在指定用于下一个周期的事件,即使当前周期尚未发出信号,这也是如此,也没有任何等待线程被释放,所有这些都是“瞬时”脉冲正确性所要求的。