6

すべて、C# で Task:s をキャンセルする複雑なケースの設計/ベスト プラクティスに関する質問です。共有タスクのキャンセルをどのように実装しますか?

最小限の例として、次のことを想定してみましょう。長時間実行され、協調的にキャンセル可能な操作「Work」があります。キャンセル トークンを引数として受け取り、キャンセルされた場合はスローします。一部のアプリケーションの状態で動作し、値を返します。その結果は、2 つの UI コンポーネントによって個別に必要とされます。

アプリケーションの状態が変更されていない間は、Work 関数の値をキャッシュする必要があります。1 つの計算が進行中の場合、新しい要求は 2 番目の計算を開始するのではなく、結果の待機を開始する必要があります。

どちらの UI コンポーネントも、他の UI コンポーネント タスクに影響を与えることなく、そのタスクをキャンセルできる必要があります。

ここまで一緒ですか?

上記は、実際の Work タスクを TaskCompletionSources にラップする Task キャッシュを導入することで実現できます。その Task:s は UI コンポーネントに返されます。UI コンポーネントがそのタスクをキャンセルすると、TaskCompletionSource タスクのみが破棄され、基になるタスクは破棄されません。これはすべて良いです。UI コンポーネントは CancellationSource を作成し、キャンセル要求は通常のトップダウン設計であり、下部に連携する TaskCompletionSource タスクがあります。

さて、本当の問題に。アプリケーションの状態が変化した場合の対処方法 'Work' 関数を状態のコピーで動作させることは実現不可能であると仮定しましょう。

1 つの解決策は、タスク キャッシュ (またはそのあたり) の状態の変化をリッスンすることです。基になるタスク (Work 関数を実行するタスク) によって使用される CancellationToken がキャッシュにある場合、それをキャンセルできます。これにより、アタッチされたすべての TaskCompletionSources Task:s のキャンセルがトリガーされる可能性があり、したがって、両方の UI コンポーネントが Canceled タスクを取得します。これはある種のボトムアップキャンセルです。

これを行うための好ましい方法はありますか?どこかにそれを説明するデザインパターンはありますか?

ボトムアップキャンセルは実装できますが、ちょっと変な感じです。UI タスクは CancellationToken で作成されますが、別の (内部の) CancellationToken によりキャンセルされます。また、トークンが同じではないため、UI で OperationCancelledException を無視することはできません。これにより、(最終的に) 外側の Task:s ファイナライザーで例外がスローされます。

4

2 に答える 2

1

これが私の試みです:

// the Task for the current application state
Task<Result> _task;
// a CancellationTokenSource for the current application state
CancellationTokenSource _cts;

// called when the application state changes
void OnStateChange()
{
    // cancel the Task for the old application state
    if (_cts != null)
    {
        _cts.Cancel();
    }

    // new CancellationTokenSource for the new application state
    _cts = new CancellationTokenSource();
    // start the Task for the new application state
    _task = Task.Factory.StartNew<Result>(() => { ... }, _cts.Token);
}

// called by UI component
Task<Result> ComputeResultAsync(CancellationToken cancellationToken)
{
    var task = _task;
    if (cancellationToken.CanBeCanceled && !task.IsCompleted)
    {
        task = WrapTaskForCancellation(cancellationToken, task);
    }
    return task;
}

static Task<T> WrapTaskForCancellation<T>(
    CancellationToken cancellationToken, Task<T> task)
{
    var tcs = new TaskCompletionSource<T>();
    if (cancellationToken.IsCancellationRequested)
    {
        tcs.TrySetCanceled();
    }
    else
    {
        cancellationToken.Register(() =>
        {
            tcs.TrySetCanceled();
        });
        task.ContinueWith(antecedent =>
        {
            if (antecedent.IsFaulted)
            {
                tcs.TrySetException(antecedent.Exception.GetBaseException());
            }
            else if (antecedent.IsCanceled)
            {
                tcs.TrySetCanceled();
            }
            else
            {
                tcs.TrySetResult(antecedent.Result);
            }
        }, TaskContinuationOptions.ExecuteSynchronously);
    }
    return tcs.Task;
}
于 2012-05-21T16:23:37.093 に答える
1

貪欲な一連のタスク操作が必要なようです-タスク結果プロバイダーがあり、最初に完了した操作を返すタスクセットを構築します。例:

// Task Provider - basically, construct your first call as appropriate, and then 
//   invoke this on state change

public void OnStateChanged()
{
    if(_cts != null)
       _cts.Cancel();

    _cts = new CancellationTokenSource();
    _task = Task.Factory.StartNew(() =>
       {
           // Do Computation, checking for cts.IsCancellationRequested, etc
           return result;
       });
}

// Consumer 1

var cts = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
  {
       var waitForResultTask = Task.Factory.StartNew(() =>
          {
              // Internally, this is invoking the task and waiting for it's value
              return MyApplicationState.GetComputedValue();
          });

       // Note this task cares about being cancelled, not the one above
       var cancelWaitTask = Task.Factory.StartNew(() =>
         {
              while(!cts.IsCancellationRequested)
                 Thread.Sleep(25);

              return someDummyValue;
         });

       Task.WaitAny(waitForResultTask, cancelWaitTask);

       if(cancelWaitTask.IsComplete)
          return "Blah"; // I cancelled waiting on the original task, even though it is still waiting for it's response
       else
          return waitForResultTask.Result;
  });

現在、これを完全にテストしていませんが、トークンをキャンセルすることでタスクの待機を「キャンセル」できるはずです (したがって、「待機」タスクを強制的に最初に完了させてWaitAnyを押します)。 " 計算タスク。

もう 1 つは、「キャンセル」タスクを恐ろしくブロックせずに待機させるクリーンな方法を見つけ出すことです。良いスタートだと思います。

于 2012-05-21T15:57:01.677 に答える