Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
3.5k views
in Technique[技术] by (71.8m points)

c# - How to safely cancel a task using a CancellationToken and await Task.WhenAll

I have a framework which creates a CancellationTokenSource, configures CancelAfter, then calls an async method and passes the Token. The async method then spawns many tasks, passing the cancellation token to each of them, and then awaits the collection of tasks. These tasks each contain logic to gracefully cancel by polling IsCancellationRequested.

My issue is that if I pass the CancellationToken into Task.Run() an AggregateException is thrown containing a TaskCanceledException. This prevents the tasks from gracefully canceling.

To get around this I can not pass the CancelationToken into Task.Run, however I'm not sure what I will be losing. For instance, I like the idea that if my task hangs and cannot perform the graceful cancel this exception will force it down. I was thinking I could string along two CancelationTokens to handle this, one 'graceful' and the other 'force'. However, I don't like that solution.

Here is some psudo-code representing what I described above..

public async Task Main()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    cts.CancelAfter(30000);
    await  this.Run(cts.Token);
}

public async Task Run(CancellationToken cancelationToken)
{
    HashSet<Task> tasks = new HashSet<Task>();
    foreach (var work in this.GetWorkNotPictured)
    {
        // Here is where I could pass the Token, 
        //   however If I do I cannot cancel gracefully
        //   My dilemma here is by not passing I lose the ability to force
        //   down the thread (via exception) if         
        //   it's hung for whatever reason
        tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken))
    }

    await Task.WhenAll(tasks);

    // Clean up regardless of if we canceled
    this.CleanUpAfterWork();

    // It is now safe to throw as we have gracefully canceled
    cancelationToken.ThrowIfCancellationRequested();
}

public static void DoWork(work, cancelationToken)
{
    while (work.IsWorking)
    {
        if (cancelationToken.IsCancellationRequested)
          return // cancel gracefully
        work.DoNextWork();
    }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I recommend that you follow the standard cancellation pattern of throwing an exception rather than just returning:

public static void DoWork(work, cancellationToken)
{
  while (work.IsWorking)
  {
    cancellationToken.ThrowIfCancellationRequested();
    work.DoNextWork();
  }
}

If you have cleanup work to do, that's what finally is for (or using, if you can refactor that way):

public async Task Run(CancellationToken cancellationToken)
{
  HashSet<Task> tasks = new HashSet<Task>();
  foreach (var work in this.GetWorkNotPictured)
  {
    tasks.Add(Task.Run(() => this.DoWork(work, cancellationToken))
  }

  try
  {
    await Task.WhenAll(tasks);
  }
  finally
  {
    this.CleanUpAfterWork();
  }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...