在中止TPL长时间运行的任务 [英] Aborting a long running task in TPL

查看:115
本文介绍了在中止TPL长时间运行的任务的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们的应用程序使用TPL序列化(潜在的)长期运行的工作单元。工作(任务)的创建是用户驱动的,并且可以随时被取消。为了有一个响应的用户界面,如果不再需要当前的一项工作,我们想放弃我们在做什么,并立即启动不同的任务。

Our application uses the TPL to serialize (potentially) long running units of work. The creation of work (tasks) is user-driven and may be cancelled at any time. In order to have a responsive user interface, if the current piece of work is no longer required we would like to abandon what we were doing, and immediately start a different task.

任务正在排队这样的事情:

Tasks are queued up something like this:

private Task workQueue;
private void DoWorkAsync
    (Action<WorkCompletedEventArgs> callback, CancellationToken token) 
{
   if (workQueue == null)
   {
      workQueue = Task.Factory.StartWork
          (() => DoWork(callback, token), token);
   }
   else 
   {
      workQueue.ContinueWork(t => DoWork(callback, token), token);
   }
}



的DoWork 方法中包含一个长期运行的调用,因此它并不像经常检查 token.IsCancellationRequested 如果/当检测取消想逃。在长时间运行工作会阻塞任务的延续,直到它完成,即使任务被取消。

The DoWork method contains a long running call, so it is not as simple as constantly checking the status of token.IsCancellationRequested and bailing if/when a cancel is detected. The long running work will block the Task continuations until it finishes, even if the task is cancelled.

我想出了两个采样方法来解决这个问题,但我不相信,要么是正确的。我创建简单的控制台应用程序来演示他们如何工作。

I have come up with two sample methods to work around this issue, but am not convinced that either are proper. I created simple console applications to demonstrate how they work.

要注意的重要一点是,延续大火之前的原始任务完成

The important point to note is that the continuation fires before the original task completes.

尝试#1:内部任务

static void Main(string[] args)
{
   CancellationTokenSource cts = new CancellationTokenSource();
   var token = cts.Token;
   token.Register(() => Console.WriteLine("Token cancelled"));
   // Initial work
   var t = Task.Factory.StartNew(() =>
     {
        Console.WriteLine("Doing work");

      // Wrap the long running work in a task, and then wait for it to complete
      // or the token to be cancelled.
        var innerT = Task.Factory.StartNew(() => Thread.Sleep(3000), token);
        innerT.Wait(token);
        token.ThrowIfCancellationRequested();
        Console.WriteLine("Completed.");
     }
     , token);
   // Second chunk of work which, in the real world, would be identical to the
   // first chunk of work.
   t.ContinueWith((lastTask) =>
         {
             Console.WriteLine("Continuation started");
         });

   // Give the user 3s to cancel the first batch of work
   Console.ReadKey();
   if (t.Status == TaskStatus.Running)
   {
      Console.WriteLine("Cancel requested");
      cts.Cancel();
      Console.ReadKey();
   }
}

这工作,但innerT任务感到非常kludgey给我。它也有迫使我重构,在这个方式排队的工作我的代码各部分的缺点,通过迫使在新任务中的所有长时间运行的调用包装起来。

This works, but the "innerT" Task feels extremely kludgey to me. It also has the drawback of forcing me to refactor all parts of my code that queue up work in this manner, by necessitating the wrapping up of all long running calls in a new Task.

尝试#2:TaskCompletionSource修修补补

static void Main(string[] args)
{  var tcs = new TaskCompletionSource<object>();
//Wire up the token's cancellation to trigger the TaskCompletionSource's cancellation
   CancellationTokenSource cts = new CancellationTokenSource();
   var token = cts.Token;
   token.Register(() =>
         {   Console.WriteLine("Token cancelled");
             tcs.SetCanceled();
          });
   var innerT = Task.Factory.StartNew(() =>
      {
          Console.WriteLine("Doing work");
          Thread.Sleep(3000);
          Console.WriteLine("Completed.");
    // When the work has complete, set the TaskCompletionSource so that the
    // continuation will fire.
          tcs.SetResult(null);
       });
   // Second chunk of work which, in the real world, would be identical to the
   // first chunk of work.
   // Note that we continue when the TaskCompletionSource's task finishes,
   // not the above innerT task.
   tcs.Task.ContinueWith((lastTask) =>
      {
         Console.WriteLine("Continuation started");
      });
   // Give the user 3s to cancel the first batch of work
   Console.ReadKey();
   if (innerT.Status == TaskStatus.Running)
   {
      Console.WriteLine("Cancel requested");
      cts.Cancel();
      Console.ReadKey();
   }
}



此外,这工作,但现在我有两个问题:

Again this works, but now I have two problems:

A)这感觉就像我滥用TaskCompletionSource从不使用它的结果,而只设置为空时,我已经完成了我的工作。

a) It feels like I'm abusing TaskCompletionSource by never using it's result, and just setting null when I've finished my work.

b)为了正确线了延续,我需要保持处理工作的独特TaskCompletionSource以前的单位,而不是被它创建的任务上。这在技术上是可能的,但同样感觉笨拙和奇怪的。

b) In order to properly wire up continuations I need to keep a handle on the previous unit of work's unique TaskCompletionSource, and not the task that was created for it. This is technically possible, but again feels clunky and strange.

在哪里何去何从?

要重申,我的问题是:这些方法之一的正确的方式来解决这个问题,还是有一个更正确的/优雅的解决方案,让我过早地终止长时间运行的任务并立即开始延续?我的选择是一个低影响的解决方案,但我愿意承担一些巨大的重构,如果是做正确的事。

To reiterate, my question is: are either of these methods the "correct" way to tackle this problem, or is there a more correct/elegant solution that will allow me to prematurely abort a long running task and immediately starting a continuation? My preference is for a low-impact solution, but I'd be willing to undertake some huge refactoring if it's the right thing to do.

另外,是TPL甚至正确的工具的工作,还是我失去了一个更好的任务队列机制。我的目标框架是.NET 4.0。

Alternately, is the TPL even the correct tool for the job, or am I missing a better task queuing mechanism. My target framework is .NET 4.0.

推荐答案

这里真正的问题是,在DoWork的长期运行的呼叫不会cancellation-知道的。如果我理解正确,你在做什么在这里是不是真的取消长时间运行的工作,而只是允许继续执行,而当工作完成了在取消的任务,忽视的结果。例如,如果您使用的内部任务模式调用CrunchNumbers(),这需要几分钟的时间,取消外任务将允许进行延续,但CrunchNumbers()将继续在后台,直到完成执行。

The real issue here is that the long-running call in DoWork is not cancellation-aware. If I understand correctly, what you're doing here is not really cancelling the long-running work, but merely allowing the continuation to execute and, when the work completes on the cancelled task, ignoring the result. For example, if you used the inner task pattern to call CrunchNumbers(), which takes several minutes, cancelling the outer task will allow continuation to occur, but CrunchNumbers() will continue to execute in the background until completion.

我不认为有比周围让你的长期运行的电话支持取消此之外的任何实际的方式。通常这是不可能的(他们可能会阻止的API调用,为消除没有API的支持。)如果是这样的话,它是真正的API中的一个缺陷;你可以检查以查看是否有可能被用来在可以取消的方式执行该操作备用API调用。一个黑客的方法,这是捕获时启动任务正在使用的任务底层线程的引用,然后调用了Thread.interrupt。这会醒来,从不同的睡眠状态的线程,并允许它终止,但在一个潜在的讨厌的方法。最坏的情况下,你甚至可以调用Thread.Abort的,但是这更成问题,不推荐。

I don't think there's any real way around this other than making your long-running calls support cancellation. Often this isn't possible (they may be blocking API calls, with no API support for cancellation.) When this is the case, it's really a flaw in the API; you may check to see if there are alternate API calls that could be used to perform the operation in a way that can be cancelled. One hack approach to this is to capture a reference to the underlying Thread being used by the Task when the Task is started and then call Thread.Interrupt. This will wake up the thread from various sleep states and allow it to terminate, but in a potentially nasty way. Worst case, you can even call Thread.Abort, but that's even more problematic and not recommended.

下面是一个刺在基于委托的包装。这是未经测试,但我认为它会做的伎俩;觉得免费的,如果你把它的工作,并有修复/改进编辑答案。

Here is a stab at a delegate-based wrapper. It's untested, but I think it will do the trick; feel free to edit the answer if you make it work and have fixes/improvements.

public sealed class AbandonableTask
{
    private readonly CancellationToken _token;
    private readonly Action _beginWork;
    private readonly Action _blockingWork;
    private readonly Action<Task> _afterComplete;

    private AbandonableTask(CancellationToken token, 
                            Action beginWork, 
                            Action blockingWork, 
                            Action<Task> afterComplete)
    {
        if (blockingWork == null) throw new ArgumentNullException("blockingWork");

        _token = token;
        _beginWork = beginWork;
        _blockingWork = blockingWork;
        _afterComplete = afterComplete;
    }

    private void RunTask()
    {
        if (_beginWork != null)
            _beginWork();

        var innerTask = new Task(_blockingWork, 
                                 _token, 
                                 TaskCreationOptions.LongRunning);
        innerTask.Start();

        innerTask.Wait(_token);
        if (innerTask.IsCompleted && _afterComplete != null)
        {
            _afterComplete(innerTask);
        }
    }

    public static Task Start(CancellationToken token, 
                             Action blockingWork, 
                             Action beginWork = null, 
                             Action<Task> afterComplete = null)
    {
        if (blockingWork == null) throw new ArgumentNullException("blockingWork");

        var worker = new AbandonableTask(token, beginWork, blockingWork, afterComplete);
        var outerTask = new Task(worker.RunTask, token);
        outerTask.Start();
        return outerTask;
    }
}

这篇关于在中止TPL长时间运行的任务的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆