2013-07-14 26 views
17

Yani benim gereksinim, başka bir sınıf ve başka bir iş parçacığından gelen bir event Action<T> ilk örnek için beklemek ve beklemeyi sağlayan iş parçacığım üzerinde işlemek için sahip olmaktır zaman aşımı veya CancellationToken tarafından kesintiye uğratılır.C#, tek bir olay için zaman aşımı ve iptal ile nasıl beklenir

Yeniden kullanabileceğim genel bir işlev oluşturmak istiyorum. İhtiyacım olan şeyi (sanırım) yapan bir çift seçenek oluşturmayı başardım, ama her ikisinin de olması gerektiğini hayal ettiğimden daha karmaşık görünüyor.

Daha açıkçası, bu işlevin bir örnek kullanımı şu şekilde görünecektir Kullanımı, serialDevice ayrı iş parçacığı üzerinde olayları saçıyor:

var eventOccurred = Helper.WaitForSingleEvent<StatusPacket>(
    cancellationToken, 
    statusPacket => OnStatusPacketReceived(statusPacket), 
    a => serialDevice.StatusPacketReceived += a, 
    a => serialDevice.StatusPacketReceived -= a, 
    5000, 
    () => serialDevice.RequestStatusPacket()); 

Seçenek 1-ManualResetEventSlim

Bu seçenek kötü değil, ancak ManualResetEventSlim'un Dispose kullanımı, olması gerektiği gibi göründüğünden daha karışıktır. ReSharper, kapağın içinde değiştirilmiş/bertaraf edilen şeylere erişebileceğimi uyarıyor ve gerçekten de takip edilmesi zor, bu yüzden doğru olduğundan emin değilim. Belki de bunu temizleyebilecek bir şeyim var, bu benim tercihim olabilir, ama ben bunu uygunsuz görmüyorum. İşte kod. Bir WaitHandle

burada WaitForSingleEvent fonksiyonu olmadan

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; 
    } 
} 

Seçenek 2-yoklama daha temiz. ConcurrentQueue kullanabiliyorum ve bu yüzden bir kilitlemeye bile gerek yok. Ancak, yoklama işlevi Sleep'u beğenmiyorum ve bu yaklaşımla herhangi bir yol göremiyorum. Sleep'u temizlemek için Func<bool> yerine bir WaitHandle geçmek istiyorum, ancak ikincisi tekrar temizlemek için tüm Dispose karışıklık var.

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); 
    } 
} 

soru

Özellikle bu çözümlerden birini bakımı, ne de ikisinden% 100 doğru% 100 eminim yoktur. Bu çözümlerden biri diğerinden (idiyomite, verimlilik, vb.) Daha mı iyi, yoksa burada yapmam gerekenleri karşılamak için daha kolay bir yol mu var?

Güncelleme: En iyi yanıt şu ana kadar

aşağıda TaskCompletionSource çözüm için bir varyasyon. Uzun kapaklar, kilitler veya gerekli herhangi bir şey yok. Oldukça basit görünüyor. Burada herhangi bir hata var mı?

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; 
} 

Güncelleme 2: Başka harika bir çözüm

BlockingCollection eserler sadece ConcurrentQueue gibi çıkıyor ama aynı zamanda bir zaman aşımı ve iptal jetonu kabul yöntemleri vardır. Bu çözüm hakkında güzel bir şey WaitForNEvents oldukça kolay hale getirmek için güncellenebilir olmasıdır:

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(); 
    } 
} 
+0

"AutoResetEvent" gibi bir şey istediğiniz gibi geliyor. Kullanabilme ihtimaline baktın mı? –

+0

@KendallFrey Evet, bu beni "ManualResetEventSlim" in yaptığı "Dispose" karmaşasına sokacak gibi görünüyor mu, yoksa etrafta bir yol var mı? – lobsterism

+0

Resharper'ın şikayet etmesinin nedeni, abonelik ve abonelikten çıkma eylemlerinin geçirilmesinden bu yana akışı analiz edememesidir. Olayı (yansımayla) geçirdiyseniz veya bir şekilde akışı daha doğrulanabilir hale getirdiyse şikayeti durdurabilir. Her halükarda, kişisel olarak Resharper uyarılarını yüksek saygı göstermiyorum. –

cevap

2

Olayı bir gözlemlenebilir, ardından bir göreve dönüştürmek için Rx'i kullanabilir ve sonunda bu görevi belirteç/zaman aşımı ile bekleyebilirsiniz. Bu, mevcut çözümlerden herhangi birinin üzerine sahip

avantajlarından biri işleyicinizin iki kez denilen olmayacak sağlanması etkinliğin parçacığı üzerinde unsubscribe çağırır olmasıdır. (İlk çözümünüzde tcs.SetResult yerine tcs.TrySetResult ile çalışabilirsiniz, ancak bir "TryDoSomething" den kurtulmak her zaman güzeldir ve sadece DoSomething'in her zaman çalıştığından emin olun).

Kodun sadeliği bir başka avantajdır. Aslında bir satır. Yani özellikle bağımsız bir işleve ihtiyacınız yok. Tam olarak kodunuzun ne yaptığını daha net bir şekilde gösterebilmeniz için satır içi ve isteğe bağlı initializer gibi isteğe bağlı parametreler gerekmeden tema üzerinde varyasyonlar yapabilir veya N olaylarında beklemenize izin verebilir veya örneklerde yukarıdaki zaman aşımları/iptalleri yapabilirsiniz. Gerekli olmayan yerlerde). Ve tamamlandığındadönüş değeri ve gerçek result kapsamına sahip olacaksınız.

using System.Reactive.Linq; 
using System.Reactive.Threading.Tasks; 
... 
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 task = Observable.FromEvent(subscribe, unsubscribe).FirstAsync().ToTask(); 
    if (initializer != null) { 
     initializer(); 
    } 
    try { 
     var finished = task.Wait(msTimeout, token); 
     if (finished) onEvent(task.Result); 
     return finished; 
    } catch (OperationCanceledException) { return false; } 
} 
4

Sen tamamlanmış veya iptal gibi işaretleyebilirsiniz bir Task oluşturmak için TaskCompletetionSource kullanabilirsiniz.

public Task WaitFirstMyEvent(Foo target, CancellationToken cancellationToken) 
{ 
    var tcs = new TaskCompletionSource<object>(); 
    Action handler = null; 
    var registration = cancellationToken.Register(() => 
    { 
     target.MyEvent -= handler; 
     tcs.TrySetCanceled(); 
    }); 
    handler =() => 
    { 
     target.MyEvent -= handler; 
     registration.Dispose(); 
     tcs.TrySetResult(null); 
    }; 
    target.MyEvent += handler; 
    return tcs.Task; 
} 

C# 5'te bu gibi kullanabilirsiniz:

private async Task MyMethod() 
{ 
    ... 
    await WaitFirstMyEvent(foo, cancellationToken); 
    ... 
} 

zaman uyumlu olay için beklemek istiyorum, ayrıca Wait yöntemi kullanabilirsiniz Burada belirli bir olay için olası bir uygulama var :

İşte
private void MyMethod() 
{ 
    ... 
    WaitFirstMyEvent(foo, cancellationToken).Wait(); 
    ... 
} 

daha genel bir versiyonu, ama yine de sadece Action imzasıyla olaylar için çalışır:

public Task WaitFirstEvent(
    Action<Action> subscribe, 
    Action<Action> unsubscribe, 
    CancellationToken cancellationToken) 
{ 
    var tcs = new TaskCompletionSource<object>(); 
    Action handler = null; 
    var registration = cancellationToken.Register(() => 
    { 
     unsubscribe(handler); 
     tcs.TrySetCanceled(); 
    }); 
    handler =() => 
    { 
     unsubscribe(handler); 
     registration.Dispose(); 
     tcs.TrySetResult(null); 
    }; 
    subscribe(handler); 
    return tcs.Task; 
} 

Böyle kullanabilirsiniz:

await WaitFirstEvent(
     handler => foo.MyEvent += handler, 
     handler => foo.MyEvent -= handler, 
     cancellationToken); 

Bunu diğer olay imzalarla çalışmak istiyorsanız (örn EventHandler), ayrı aşırı yüklemeler oluşturmanız gerekecektir. Herhangi bir imza için çalışmanın kolay bir yolu olduğunu sanmıyorum, özellikle de parametre sayısı her zaman aynı değil.

+0

Sorunuza bir güncelleme ekledim — örneklerinizi başlangıç ​​noktası olarak kullanarak olası bir çözüm. Genel olarak TaskCompletionSource veya gerçekten "Task" ile herhangi bir deneyimim yok. Çözümde göze çarpan bir hata görüyor musunuz? (Bu .Net 4.0 ve bir masaüstü uygulaması bu yüzden "task.Wait" ile bir iş parçacığı tutarak bir sorun değil.) – lobsterism

+0

@lob, kodunuzun iptalini desteklemiyor. Ayrıca, görev durumunu sınamak mantıklı gelmez: eğer bu noktaya ulaşırsa, durum bir şey olamaz ancak RanToCompletion, aksi halde istisna –

+0

kadar köpürtüldü Not "task.Wait kullanıyorum bir zaman aşımı ve CancellationToken alır aşırı yükleme. İşi yapıyor gibi görünüyor. “Wait” çağrıldığında simge iptal edilirse, o zaman OperationCancellationException öğesini atar ve eğer zaman aşımına uğrarsa, o zaman Status [Durum] TaskStatus.WaitingForActivation (Kalıcı Bağlantı). – lobsterism