Jak czekać na pojedyncze zdarzenie w C #, z limitem czasu i anulowaniem
Zatem moim wymaganiem jest, aby moja funkcja czekała na pierwszą instancję ievent Action<T>
pochodzący z innej klasy i innego wątku i obsługujący go w moim wątku, pozwalając na przerwanie oczekiwania przez timeout lubCancellationToken
.
Chcę utworzyć ogólną funkcję, której mogę użyć ponownie. Udało mi się stworzyć kilka opcji, które robią (myślę) to, czego potrzebuję, ale obie wydają się bardziej skomplikowane niż sobie wyobrażam.
StosowanieDla jasności, przykładowe użycie tej funkcji wyglądałoby tak, gdzieserialDevice
wypluwa wydarzenia w osobnym wątku:
var eventOccurred = Helper.WaitForSingleEvent<StatusPacket>(
cancellationToken,
statusPacket => OnStatusPacketReceived(statusPacket),
a => serialDevice.StatusPacketReceived += a,
a => serialDevice.StatusPacketReceived -= a,
5000,
() => serialDevice.RequestStatusPacket());
Opcja 1 - ManualResetEventSlimTa opcja nie jest zła, aleDispose
obsługaManualResetEventSlim
jest bardziej niechlujny niż mogłoby się wydawać. Daje ReSharper pasuje, że mam dostęp do zmodyfikowanych / usuniętych rzeczy w zamknięciu i naprawdę trudno jest go śledzić, więc nie jestem nawet pewien, czy jest poprawny. Może brakuje mi czegoś, co mogłoby to oczyścić, co byłoby moją preferencją, ale nie widzę tego odręcznie. Oto kod.
public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> handler, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null)
{
var eventOccurred = false;
var eventResult = default(TEvent);
var o = new object();
var slim = new ManualResetEventSlim();
Action<TEvent> setResult = result =>
{
lock (o) // ensures we get the first event only
{
if (!eventOccurred)
{
eventResult = result;
eventOccurred = true;
// ReSharper disable AccessToModifiedClosure
// ReSharper disable AccessToDisposedClosure
if (slim != null)
{
slim.Set();
}
// ReSharper restore AccessToDisposedClosure
// ReSharper restore AccessToModifiedClosure
}
}
};
subscribe(setResult);
try
{
if (initializer != null)
{
initializer();
}
slim.Wait(msTimeout, token);
}
finally // ensures unsubscription in case of exception
{
unsubscribe(setResult);
lock(o) // ensure we don't access slim
{
slim.Dispose();
slim = null;
}
}
lock (o) // ensures our variables don't get changed in middle of things
{
if (eventOccurred)
{
handler(eventResult);
}
return eventOccurred;
}
}
Opcja 2 - odpytywanie bezWaitHandle
TheWaitForSingleEvent
funkcja tutaj jest znacznie czystsza. Jestem w stanie użyćConcurrentQueue
i dlatego nawet nie potrzebuję blokady. Ale nie podoba mi się funkcja odpytywaniaSleep
i nie widzę tego w ten sposób. Chciałbym przejśćWaitHandle
zamiastFunc<bool>
posprzątaćSleep
, ale za drugim razem mam całośćDispose
bałagan ponownie posprzątać.
public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> handler, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null)
{
var q = new ConcurrentQueue<TEvent>();
subscribe(q.Enqueue);
try
{
if (initializer != null)
{
initializer();
}
token.Sleep(msTimeout, () => !q.IsEmpty);
}
finally // ensures unsubscription in case of exception
{
unsubscribe(q.Enqueue);
}
TEvent eventResult;
var eventOccurred = q.TryDequeue(out eventResult);
if (eventOccurred)
{
handler(eventResult);
}
return eventOccurred;
}
public static void Sleep(this CancellationToken token, int ms, Func<bool> exitCondition)
{
var start = DateTime.Now;
while ((DateTime.Now - start).TotalMilliseconds < ms && !exitCondition())
{
token.ThrowIfCancellationRequested();
Thread.Sleep(1);
}
}
PytanieNie dbam o żadne z tych rozwiązań ani nie jestem w 100% pewien, że któreś z nich są w 100% poprawne. Czy jedno z tych rozwiązań jest lepsze od drugiego (idiomatyczność, wydajność itp.), Czy też istnieje łatwiejszy sposób lub wbudowana funkcja, aby sprostać temu, co muszę zrobić tutaj?
Aktualizacja: jak dotąd najlepsza odpowiedźModyfikacjaTaskCompletionSource
rozwiązanie poniżej. Brak długich zamknięć, zamków lub czegokolwiek innego. Wydaje się całkiem proste. Jakieś błędy tutaj?
public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> onEvent, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null)
{
var tcs = new TaskCompletionSource<TEvent>();
Action<TEvent> handler = result => tcs.TrySetResult(result);
var task = tcs.Task;
subscribe(handler);
try
{
if (initializer != null)
{
initializer();
}
task.Wait(msTimeout, token);
}
finally
{
unsubscribe(handler);
// Do not dispose task http://blogs.msdn.com/b/pfxteam/archive/2012/03/25/10287435.aspx
}
if (task.Status == TaskStatus.RanToCompletion)
{
onEvent(task.Result);
return true;
}
return false;
}
Aktualizacja 2: Kolejne świetne rozwiązanieOkazuje się, żeBlockingCollection
działa tak jakConcurrentQueue
ale ma także metody akceptujące limit czasu i żeton anulowania. Jedną z zalet tego rozwiązania jest możliwość aktualizacjiWaitForNEvents
dość łatwe:
public static bool WaitForSingleEvent<TEvent>(this CancellationToken token, Action<TEvent> handler, Action<Action<TEvent>> subscribe, Action<Action<TEvent>> unsubscribe, int msTimeout, Action initializer = null)
{
var q = new BlockingCollection<TEvent>();
Action<TEvent> add = item => q.TryAdd(item);
subscribe(add);
try
{
if (initializer != null)
{
initializer();
}
TEvent eventResult;
if (q.TryTake(out eventResult, msTimeout, token))
{
handler(eventResult);
return true;
}
return false;
}
finally
{
unsubscribe(add);
q.Dispose();
}
}