web-dev-qa-db-ja.com

TaskCompletionSourceが「タスクがすでに完了しているときに、タスクを最終状態に移行しようとしました」をスロー

TaskCompletionSourceを使用してMyServiceをラップしたいのですが、これは単純なサービスです。

public static Task<string> ProcessAsync(MyService service, int parameter)
{
    var tcs = new TaskCompletionSource<string>();
    //Every time ProccessAsync is called this assigns to Completed!
    service.Completed += (sender, e)=>{ tcs.SetResult(e.Result); };   
    service.RunAsync(parameter);
    return tcs.Task;
}

このコードは初めてうまくいきます。しかしsecondを呼び出す時間ProcessAsyncを呼び出すだけで、Completedのイベントハンドラーが再度割り当てられます(同じservice変数が毎回使用されます)。したがって、2回実行されます。 2回目はこの例外をスローします。

既に完了している場合、タスクの最終状態の遷移を試みる

tcsを次のようにクラスレベルの変数として宣言する必要があるかどうかはわかりません。

TaskCompletionSource<string> tcs;

public static Task<string> ProccessAsync(MyService service, int parameter)
{
    tcs = new TaskCompletionSource<string>();
    service.Completed -= completedHandler; 
    service.Completed += completedHandler;
    return tcs.Task;    
}

private void completedHandler(object sender, CustomEventArg e)
{
    tcs.SetResult(e.Result); 
}

さまざまな戻り値の型で多くのメソッドをラップする必要があるため、失われたコード、変数、イベントハンドラーを記述する必要があるため、これがこのシナリオでのベストプラクティスであるかどうかはわかりません。それで、この仕事をするより良い方法はありますか?

16

ここでの問題は、各アクションでCompletedイベントが発生しますが、TaskCompletionSourceは1回しか完了できないことです。

ローカルTaskCompletionSourceを引き続き使用できます(必要があります)。 TaskCompletionSourceを完了する前に、コールバックの登録を解除する必要があります。そうすれば、この特定のTaskCompletionSourceを使用したこの特定のコールバックが再び呼び出されることはありません。

public static Task<string> ProcessAsync(MyService service, int parameter)
{
    var tcs = new TaskCompletionSource<string>();
    EventHandler<CustomEventArg> callback = null;
    callback = (sender, e) => 
    {
        service.Completed -= callback;
        tcs.SetResult(e.Result); 
    };
    service.Completed += callback;
    service.RunAsync(parameter);
    return tcs.Task;
}

これにより、サービスがこれらすべてのデリゲートへの参照を保持しているときに発生する可能性のあるメモリリークも解決されます。

ただし、これらの操作を複数同時に実行することはできません。少なくとも、リクエストとレスポンスを照合する方法がない限りはそうではありません。

25
i3arnon

i3arnonanswer の代替ソリューションは次のようになります。

public async static Task<string> ProcessAsync(MyService service, int parameter)
{
    var tcs = new TaskCompletionSource<string>();

    EventHandler<CustomEventArg> callback = 
        (s, e) => tcs.SetResult(e.Result);

    try
    {
        contacts.Completed  += callback;

        contacts.RunAsync(parameter);

        return await tcs.Task;
    }
    finally
    {
        contacts.Completed  -= callback;
    }
}

ただし、このソリューションには、コンパイラによって生成されたステートマシンがあります。より多くのメモリとCPUを使用します。

3
Paulo Morgado

MyServiceCompletedイベントを複数回発生させるようです。これにより、SetResultが複数回呼び出され、エラーが発生します。

私には3つの選択肢があります。 Completedイベントを1回だけ発生するように変更します(複数回完了できるのは奇妙に思えます)。SetResultTrySetResult に変更して、例外がスローされないようにします2回目に設定しようとすると(イベントが引き続き呼び出され、完了ソースが設定されようとするため、小さなメモリリークが発生します)、またはイベントのサブスクライブを解除します( i3arnonの回答

2