繁体   English   中英

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

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

让我们考虑一下方法:

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

    }, token);
}

然后我有一个消费者:

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

和项目的示例:

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

那任务呢? 我无法将取消令牌放入项目集合中

如何杀死不响应的任务或解决这个问题?

我找到了一种解决方案,该解决方案可以将取消令牌放入源自第三方的项中:

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;
    }
}

现在我需要使用:

Items.ToCancellable(cts.token)

并在取消请求后不会挂起。

您无法真正取消不可取消的操作。 Stephen Toub在Parallel FX团队博客上的“ 如何取消不可取消的异步操作? ”中进行了详细介绍,但本质是您需要了解您实际想要做什么?

  1. 停止异步/长时间运行本身吗? 如果您无法通过信号通知操作,则无法以协作方式进行
  2. 停止等待操作完成,忽略任何结果吗? 这样做是可行的,但是由于明显的原因可能导致不可靠性。 您可以通过传递取消令牌的长时间操作来启动Task,也可以使用Stephen Toub描述的TaskCompletionSource。

您需要确定要寻找正确解决方案的行为

为什么不能将CancellationToken传递给Items()

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

当然,您必须将与传递给Foo()相同的令牌传递给Items()

尝试使用TaskCompletionSource并将其返回。 然后,可以将TaskCompletionSource设置为内部任务的运行结果(或错误)(如果内部任务运行到完成(或错误))。 但是,如果触发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;
}

这实际上不会杀死内部任务,但会给您一个任务,您可以在取消后立即继续执行。 为了杀死内部任务,因为它在无限超时中闲逛,我相信唯一可以做的就是获取对Thread.CurrentThread的引用,以在其中启动任务,然后从Foo调用taskThread.Abort() 。当然是不好的做法。 但是在这种情况下,您的问题确实归结为“我如何才能使长时间运行的函数在不访问代码的情况下终止”,这只能通过Thread.Abort

是否可以将Items为IEnumerable<Task<int>>而不是IEnumerable<int> 那你可以做

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

尽管使用Reactive Framework和执行items.ToObservable这样的事情可能更直接。 看起来像这样:

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;
}

如何围绕可枚举的包装创建包装,该包装本身可以在项目之间取消?

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

...虽然这似乎是Foo()已经完成的工作。 如果您在某个地方可以无限枚举此枚举(并且不仅很慢),那么您要做的就是在用户端向task.Wait()添加超时和/或取消令牌。

以前的解决方案基于乐观的假设,即可枚举可能不会挂起并且速度很快。 因此,有时我们可以牺牲系统线程池中的一个线程吗? 正如Dax Fohl指出的那样,即使其父任务已被取消异常杀死,该任务仍将处于活动状态。 在这方面,如果无限期冻结了几个集合,这可能会使默认的任务计划程序使用的基础ThreadPool中断。

因此,我重构了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;
    }
}

还有一个帮助班级来管理结果:

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

中止分离的线程是安全的。

我在这里看到的痛苦是创建线程很重。 这可以通过使用自定义线程池以及生产者-消费者模式来解决,该模式将能够处理中止异常,以便从池中删除损坏的线程。

另一个问题是在Join行。 这里最好的停顿是什么? 也许应该由用户负责并作为方法参数提供。

暂无
暂无

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

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