简体   繁体   English

无法使用取消令牌时如何中止或终止TPL任务?

[英]How to abort or terminate a task of TPL when cancellation token is unreachable?

Let's consider the method: 让我们考虑一下方法:

Task Foo(IEnumerable items, CancellationToken token)
{
    return Task.Run(() =>
    {
        foreach (var i in items)
            token.ThrowIfCancellationRequested();

    }, token);
}

Then I have a consumer: 然后我有一个消费者:

var cts = new CancellationTokenSource();
var task = Foo(Items, cts.token);
task.Wait();

And the example of Items: 和项目的示例:

IEnumerable Items
{
    get
    {
        yield return 0;
        Task.Delay(Timeout.InfiniteTimeSpan).Wait();
        yield return 1;
    }
}

What about task.Wait? 那任务呢? I cannot put my cancel token into collection of items . 我无法将取消令牌放入项目集合中

How to kill the not responding task or get around this? 如何杀死不响应的任务或解决这个问题?

I found one solution that allows to put cancellation token into Items originating from thid parties: 我找到了一种解决方案,该解决方案可以将取消令牌放入源自第三方的项中:

public static IEnumerable<T> ToCancellable<T>(this IEnumerable<T> @this, CancellationToken token)
{
    var enumerator = @this.GetEnumerator();

    for (; ; )
    {
        var task = Task.Run(() => enumerator.MoveNext(), token);
        task.Wait(token);

        if (!task.Result)
            yield break;

        yield return enumerator.Current;
    }
}

Now I need to use: 现在我需要使用:

Items.ToCancellable(cts.token)

And that will not hang after cancel request. 并在取消请求后不会挂起。

You can't really cancel a non-cancellable operation. 您无法真正取消不可取消的操作。 Stephen Toub goes into details in " How do I cancel non-cancelable async operations? " on the Parallel FX Team's blog but the essence is that you need to understand what you actually want to do? Stephen Toub在Parallel FX团队博客上的“ 如何取消不可取消的异步操作? ”中进行了详细介绍,但本质是您需要了解您实际想要做什么?

  1. Stop the asynchronous/long-running operation itself? 停止异步/长时间运行本身吗? Not doable in a cooperative way, if you don't have a way to signal the operation 如果您无法通过信号通知操作,则无法以协作方式进行
  2. Stop waiting for the operation to finish, ignoring any results? 停止等待操作完成,忽略任何结果吗? That's doable, but can lead to unreliability for obvious reasons. 这样做是可行的,但是由于明显的原因可能导致不可靠性。 You can start a Task with the long operation passing a cancellation token, or use a TaskCompletionSource as Stephen Toub describes. 您可以通过传递取消令牌的长时间操作来启动Task,也可以使用Stephen Toub描述的TaskCompletionSource。

You need to decide which behavior you want to find the proper solution 您需要确定要寻找正确解决方案的行为

Why can't you pass the CancellationToken to Items() ? 为什么不能将CancellationToken传递给Items()

IEnumerable Items(CancellationToken ct)
{
    yield return 0;
    Task.Delay(Timeout.InfiniteTimeSpan, ct).Wait();
    yield return 1;
}

You would have to pass the same token to Items() as you pass to Foo() , of course. 当然,您必须将与传递给Foo()相同的令牌传递给Items()

Try using a TaskCompletionSource and returning that. 尝试使用TaskCompletionSource并将其返回。 You can then set the TaskCompletionSource to the result (or the error) of the inner task if it runs to completion (or faults). 然后,可以将TaskCompletionSource设置为内部任务的运行结果(或错误)(如果内部任务运行到完成(或错误))。 But you can set it to canceled immediately if the CancellationToken gets triggered. 但是,如果触发CancellationToken则可以将其设置为立即CancellationToken

Task<int> Foo(IEnumerable<int> items, CancellationToken token)
{
    var tcs = new TaskCompletionSource<int>();
    token.Register(() => tcs.TrySetCanceled());
    var innerTask = Task.Factory.StartNew(() =>
    {
        foreach (var i in items)
            token.ThrowIfCancellationRequested();
        return 7;
    }, token);
    innerTask.ContinueWith(task => tcs.TrySetResult(task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
    innerTask.ContinueWith(task => tcs.TrySetException(task.Exception), TaskContinuationOptions.OnlyOnFaulted);
    return tcs.Task;
}

This won't actually kill the inner task, but it'll give you a task that you can continue from immediately on cancellation. 这实际上不会杀死内部任务,但会给您一个任务,您可以在取消后立即继续执行。 To kill the inner task since it's hanging out in an infinite timeout, I believe the only thing you can do is to grab a reference to Thread.CurrentThread where you start the task, and then call taskThread.Abort() from within Foo , which of course is bad practice. 为了杀死内部任务,因为它在无限超时中闲逛,我相信唯一可以做的就是获取对Thread.CurrentThread的引用,以在其中启动任务,然后从Foo调用taskThread.Abort() 。当然是不好的做法。 But in this case your question really comes down to "how can I make a long running function terminate without having access to the code", which is only doable via Thread.Abort . 但是在这种情况下,您的问题确实归结为“我如何才能使长时间运行的函数在不访问代码的情况下终止”,这只能通过Thread.Abort

Can you have Items be IEnumerable<Task<int>> instead of IEnumerable<int> ? 是否可以将Items为IEnumerable<Task<int>>而不是IEnumerable<int> Then you could do 那你可以做

return Task.Run(() =>
{
    foreach (var task in tasks)
    {
        task.Wait(token);
        token.ThrowIfCancellationRequested();
        var i = task.Result;
    }
}, token);

Although something like this may be more straightforward to do using Reactive Framework and doing items.ToObservable . 尽管使用Reactive Framework和执行items.ToObservable这样的事情可能更直接。 That would look like this: 看起来像这样:

static Task<int> Foo(IEnumerable<int> items, CancellationToken token)
{
    var sum = 0;
    var tcs = new TaskCompletionSource<int>();
    var obs = items.ToObservable(ThreadPoolScheduler.Instance);
    token.Register(() => tcs.TrySetCanceled());
    obs.Subscribe(i => sum += i, tcs.SetException, () => tcs.TrySetResult(sum), token);
    return tcs.Task;
}

How about creating a wrapper around the enumerable that is itself cancellable between items? 如何围绕可枚举的包装创建包装,该包装本身可以在项目之间取消?

IEnumerable<T> CancellableEnum<T>(IEnumerable<T> items, CancellationToken ct) {
    foreach (var item in items) {
        ct.ThrowIfCancellationRequested();
        yield return item;
    }
}

...though that seems to be kind of what Foo() already does. ...虽然这似乎是Foo()已经完成的工作。 If you have some place where this enumerable blocks literally infinitely (and it's not just very slow), then what you would do is add a timeout and/or a cancellation token to the task.Wait() on the consumer side. 如果您在某个地方可以无限枚举此枚举(并且不仅很慢),那么您要做的就是在用户端向task.Wait()添加超时和/或取消令牌。

My previous solution was based on an optimistic assumption that the enumerable is likely to not hang and is quite fast. 以前的解决方案基于乐观的假设,即可枚举可能不会挂起并且速度很快。 Thus we could sometimes sucrifice one thread of the system's thread pool? 因此,有时我们可以牺牲系统线程池中的一个线程吗? As Dax Fohl pointed out, the task will be still active even if its parent task has been killed by cancel exception. 正如Dax Fohl指出的那样,即使其父任务已被取消异常杀死,该任务仍将处于活动状态。 And in this regard, that could chock up the underlying ThreadPool, which is used by default task scheduler, if several collections have been frozen indefinitely. 在这方面,如果无限期冻结了几个集合,这可能会使默认的任务计划程序使用的基础ThreadPool中断。

Consequently I have refactored ToCancellable method: 因此,我重构了ToCancellable方法:

public static IEnumerable<T> ToCancellable<T>(this IEnumerable<T> @this, CancellationToken token)
{
    var enumerator = @this.GetEnumerator();
    var state = new State();

    for (; ; )
    {
        token.ThrowIfCancellationRequested();

        var thread = new Thread(s => { ((State)s).Result = enumerator.MoveNext(); }) { IsBackground = true, Priority = ThreadPriority.Lowest };
        thread.Start(state);

        try
        {
            while (!thread.Join(10))
                token.ThrowIfCancellationRequested();
        }
        catch (OperationCanceledException)
        {
            thread.Abort();
            throw;
        }

        if (!state.Result)
            yield break;

        yield return enumerator.Current;
    }
}

And a helping class to manage the result: 还有一个帮助班级来管理结果:

class State
{
    public bool Result { get; set; }
}

It is safe to abort a detached thread. 中止分离的线程是安全的。

The pain, that I see here is a thread creation which is heavy. 我在这里看到的痛苦是创建线程很重。 That could be solved by using custom thread pool along with producer-consumer pattern that will be able to handle abort exceptions in order to remove broken thread from the pool. 这可以通过使用自定义线程池以及生产者-消费者模式来解决,该模式将能够处理中止异常,以便从池中删除损坏的线程。

Another problem is at Join line. 另一个问题是在Join行。 What is the best pause here? 这里最好的停顿是什么? Maybe that should be in user charge and shiped as a method argument. 也许应该由用户负责并作为方法参数提供。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM