したがって、私の要件は、別のクラスと別のスレッドからの最初のインスタンスevent Action<T>
を関数で待機させ、それをスレッドで処理して、タイムアウトまたはCancellationToken
のいずれかによって待機が中断されるようにすることです。 。
再利用できる汎用関数を作成したい。私は(私が思うに)必要なことを実行するいくつかのオプションを作成することができましたが、どちらも私が想像するよりも複雑に見えます。
明確にするために、この関数の使用例は次のようになります。ここで、serialDevice
は別のスレッドでイベントを吐き出します。
var eventOccurred = Helper.WaitForSingleEvent<StatusPacket>(
cancellationToken,
statusPacket => OnStatusPacketReceived(statusPacket),
a => serialDevice.StatusPacketReceived += a,
a => serialDevice.StatusPacketReceived -= a,
5000,
() => serialDevice.RequestStatusPacket());
このオプションは悪くはありませんが、Dispose
のManualResetEventSlim
処理は思ったよりも厄介です。それは私がクロージャー内の変更された/処分されたものにアクセスしているというReSharperの適合を与えます、そしてそれが正しいかどうかさえわかりません。たぶん、これをクリーンアップできる何かが足りないので、それが私の好みですが、私はそれを手に負えないと思います。これがコードです。
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;
}
}
WaitHandle
なしでポーリングここでのWaitForSingleEvent
関数ははるかにクリーンです。 ConcurrentQueue
を使用できるので、ロックすら必要ありません。しかし、私はポーリング関数Sleep
が好きではなく、このアプローチではそれを回避する方法がわかりません。 Func<bool>
の代わりにWaitHandle
を渡して、Sleep
をクリーンアップしたいのですが、2回目は、Dispose
全体を取得しました。再びクリーンアップするために混乱。
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);
}
}
私はこれらの解決策のどちらも特に気にしませんし、どちらかが100%正しいと100%確信していません。これらのソリューションのいずれかが他のソリューションよりも優れていますか(イディオマティック、効率など)、またはここで行う必要があることを満たすためのより簡単な方法または組み込み関数がありますか?
以下のTaskCompletionSource
ソリューションの変更。長い閉鎖、ロック、または何も必要ありません。かなり簡単なようです。ここにエラーはありますか?
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;
}
BlockingCollection
はConcurrentQueue
と同じように機能しますが、タイムアウトとキャンセルトークンを受け入れるメソッドもあります。このソリューションの良い点の1つは、WaitForNEvents
をかなり簡単に作成できるように更新できることです。
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();
}
}
Rxを使用して、イベントを監視可能に変換し、次にタスクに変換し、最後にトークン/タイムアウトでそのタスクを待機できます。
これが既存のソリューションのいずれよりも優れている点の1つは、イベントのスレッドでunsubscribe
を呼び出し、ハンドラーが確実にすることです。 2回呼び出されます。 (最初のソリューションでは、これをtcs.TrySetResult
ではなくtcs.SetResult
で回避しますが、「TryDoSomething」を取り除き、DoSomethingが常に機能することを確認するのは常に良いことです)。
もう1つの利点は、コードが単純なことです。それは本質的に1行です。したがって、特に独立した関数は必要ありません。コードが正確に何をするのかをより明確にするためにインライン化でき、オプションのパラメーター(オプションのinitializer
など)を大量に必要とせずにテーマにバリエーションを作成したり、N個のイベントを待機したりすることができます。必要のない場合のタイムアウト/キャンセル)。そして、それが終了した場合、スコープ内のbool
return valと実際のresult
の両方があります。まったく便利です。
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; }
}
TaskCompletetionSource
を使用して、完了またはキャンセルとしてマークできるTask
を作成できます。特定のイベントの可能な実装は次のとおりです。
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では、次のように使用できます。
private async Task MyMethod()
{
...
await WaitFirstMyEvent(foo, cancellationToken);
...
}
イベントを同期的に待機する場合は、Wait
メソッドを使用することもできます。
private void MyMethod()
{
...
WaitFirstMyEvent(foo, cancellationToken).Wait();
...
}
これはより一般的なバージョンですが、それでもAction
署名のあるイベントに対してのみ機能します。
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;
}
次のように使用できます。
await WaitFirstEvent(
handler => foo.MyEvent += handler,
handler => foo.MyEvent -= handler,
cancellationToken);
他のイベントシグネチャ(例:EventHandler
)と連携させたい場合は、個別のオーバーロードを作成する必要があります。特にパラメータの数が常に同じであるとは限らないため、どの署名でも機能させる簡単な方法はないと思います。
どうもありがとう!他の人が理解するのを助けるために...(たぶんヒットアクションハンドラーコードでシリアルデバイスコードを表示する)
次のようなものを追加して、ジェネリック型の制約を設定することもできます。
where TEvent : EventArgs
私の場合、「ウェイター」のイベントからの結果も必要です
だから私は次のように署名を変更しました
(ジェネリックオブジェクトでは高速で醜い...)
public static bool WaitForSingleEventWithResult<TEvent, TObjRes>(
this CancellationToken token,
Func<TEvent, TObjRes> onEvent,
...
このように呼ぶ
var ct = new CancellationToken();
object result;
bool eventOccurred = ct.WaitForSingleEventWithResult<MyEventArgs, object>(
onEvent: statusPacket => result = this.OnStatusPacketReceived(statusPacket),
subscribe: sub => cp.StatusPacketReceived_Action += sub,
unsubscribe: unsub => cp.StatusPacketReceived_Action -= unsub,
msTimeout: 5 * 1000,
initializer: /*() => serialDevice.RequestStatusPacket()*/null);
とにかく...どうもありがとう!
ManualResetEventSlim.Wait (int millisecondsTimeout, CancellationToken cancellationToken)
を使用しないのはなぜですか?