![](/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.