web-dev-qa-db-ja.com

タイムアウトとキャンセルを使用して、C#で単一のイベントを待機する方法

したがって、私の要件は、別のクラスと別のスレッドからの最初のインスタンス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());

オプション1-ManualResetEventSlim

このオプションは悪くはありませんが、DisposeManualResetEventSlim処理は思ったよりも厄介です。それは私がクロージャー内の変更された/処分されたものにアクセスしているという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;
    }
}

オプション2 -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;
}

アップデート2:もう1つの優れたソリューション

BlockingCollectionConcurrentQueueと同じように機能しますが、タイムアウトとキャンセルトークンを受け入れるメソッドもあります。このソリューションの良い点の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();
    }
}
19
lobsterism

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; }
}
2
Dax Fohl

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)と連携させたい場合は、個別のオーバーロードを作成する必要があります。特にパラメータの数が常に同じであるとは限らないため、どの署名でも機能させる簡単な方法はないと思います。

5
Thomas Levesque

どうもありがとう!他の人が理解するのを助けるために...(たぶんヒットアクションハンドラーコードでシリアルデバイスコードを表示する)

次のようなものを追加して、ジェネリック型の制約を設定することもできます。

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

とにかく...どうもありがとう!

0
giacomo

ManualResetEventSlim.Wait (int millisecondsTimeout, CancellationToken cancellationToken)を使用しないのはなぜですか?

0
alexbk66