如果取消令牌无法访问,如何中止或终止TPL任务?

时间:2013-08-07 12:27:59

标签: c# task-parallel-library cancellation-token

让我们考虑一下方法:

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

task.Wait怎么样? 我无法将取消令牌放入项目集合

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

7 个答案:

答案 0 :(得分:3)

我找到了一个解决方案,允许将取消令牌放入源自各方的项目中:

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)

取消请求后,这不会挂起。

答案 1 :(得分:2)

您无法真正取消不可取消的操作。 Stephen Toub在Parallel FX团队的博客上详细介绍了“How do I cancel non-cancelable async operations?”,但其实质是你需要了解你真正想做的事情?

  1. 停止异步/长时间运行的操作?如果您没有办法发出操作信号,则无法以合作的方式进行操作
  2. 停止等待操作完成,忽略任何结果?这是可行的,但由于显而易见的原因可能导致不可靠。你可以通过传递取消令牌的长操作来启动任务,或者像Stephen Toub所描述的那样使用TaskCompletionSource。
  3. 您需要决定要找到正确解决方案的行为

答案 2 :(得分:1)

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

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

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

答案 3 :(得分:1)

尝试使用TaskCompletionSource并返回。然后,如果内部任务运行完成(或故障),则可以将TaskCompletionSource设置为结果(或错误)。但是如果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的引用,然后从taskThread.Abort()中调用Foo {1}},这当然是不好的做法。但在这种情况下,你的问题实际上归结为“如何在不访问代码的情况下终止长时间运行的函数”,这只能通过Thread.Abort来实现。

答案 4 :(得分:1)

您可以将项目设为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;
}

答案 5 :(得分:0)

如何围绕可在项目之间取消的枚举创建包装器?

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

......虽然这似乎是Foo()已经做的事情。如果你有一个地方可以无限地阻止这个可枚举的块(并且它不仅非常慢),那么你要做的就是在任务中添加一个超时和/或一个取消令牌。在消费者方面使用.Wait()

答案 6 :(得分:0)

我的previous solution基于乐观的假设,即可枚举可能不会挂起并且速度非常快。因此,我们有时可能会破坏系统线程池的一个线程?正如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; }
}

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

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

另一个问题是在加入线。这里最好的暂停是什么?也许这应该是用户收费并作为方法论证。