[英]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团队博客上的“ 如何取消不可取消的异步操作? ”中进行了详细介绍,但本质是您需要了解您实际想要做什么?
您需要确定要寻找正确解决方案的行为
为什么不能将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.