让我们考虑一下方法:
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怎么样? 我无法将取消令牌放入项目集合。
如何杀死没有响应的任务或解决这个问题?
答案 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?”,但其实质是你需要了解你真正想做的事情?
您需要决定要找到正确解决方案的行为
答案 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; }
}
中止分离的线程是安全的。
我在这里看到的痛苦是一个很重的线程创造。这可以通过使用自定义线程池以及生产者 - 消费者模式来解决,该模式将能够处理中止异常,以便从池中删除损坏的线程。
另一个问题是在加入线。这里最好的暂停是什么?也许这应该是用户收费并作为方法论证。