如何处理 TPL 中的任务取消
How to handle task cancellation in the TPL
美好的一天!我正在为 WinForms UI 编写一个帮助程序库。开始使用 TPL async/await 机制并遇到此类代码示例的问题:
private SynchronizationContext _context;
public void UpdateUI(Action action)
{
_context.Post(delegate { action(); }, null);
}
private async void button2_Click(object sender, EventArgs e)
{
var taskAwait = 4000;
var progressRefresh = 200;
var cancellationSource = new System.Threading.CancellationTokenSource();
await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });
Action usefulWork = () =>
{
try
{
Thread.Sleep(taskAwait);
cancellationSource.Cancel();
}
catch { }
};
Action progressUpdate = () =>
{
int i = 0;
while (i < 10)
{
UpdateUI(() => { button2.Text = "Processing " + i.ToString(); });
Thread.Sleep(progressRefresh);
i++;
}
cancellationSource.Cancel();
};
var usefulWorkTask = new Task(usefulWork, cancellationSource.Token);
var progressUpdateTask = new Task(progressUpdate, cancellationSource.Token);
try
{
cancellationSource.Token.ThrowIfCancellationRequested();
Task tWork = Task.Factory.StartNew(usefulWork, cancellationSource.Token);
Task tProgress = Task.Factory.StartNew(progressUpdate, cancellationSource.Token);
await Task.Run(() =>
{
try
{
var res = Task.WaitAny(new[] { tWork, tProgress }, cancellationSource.Token);
}
catch { }
}).ConfigureAwait(false);
}
catch (Exception ex)
{
}
await Task.Run(() => { UpdateUI(() => { button2.Text = "button2"; }); });
}
基本上,想法是 运行 两个并行任务 - 一个是进度条或任何更新和一种超时控制器,另一个是长 运行ning 任务本身。无论哪个任务先完成,都会取消另一个任务。因此,取消 "progress" 任务应该没有问题,因为它有一个循环,我可以在其中检查任务是否被标记为已取消。问题出在长 运行ning 上。它可以是 Thread.Sleep() 或 SqlConnection.Open()。当我 运行 CancellationSource.Cancel() 时,长 运行ning 任务继续工作并且不会取消。超时后,我对长时间 运行ning 任务或它可能导致的任何结果不感兴趣。
正如混乱的代码示例所暗示的那样,我尝试了很多变体,none 给了我想要的效果。 Task.WaitAny() 之类的东西会冻结 UI... 有没有一种方法可以使这种取消起作用,或者甚至可能是一种不同的方法来对这些东西进行编码?
更新:
public static class Taskhelpers
{
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
}
return await task;
}
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
}
await task;
}
}
.....
var taskAwait = 4000;
var progressRefresh = 200;
var cancellationSource = new System.Threading.CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
var usefulWorkTask = Task.Run(async () =>
{
try
{
System.Diagnostics.Trace.WriteLine("WORK : started");
await Task.Delay(taskAwait).WithCancellation(cancellationToken);
System.Diagnostics.Trace.WriteLine("WORK : finished");
}
catch (OperationCanceledException) { } // just drop out if got cancelled
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine("WORK : unexpected error : " + ex.Message);
}
}, cancellationToken);
var progressUpdatetask = Task.Run(async () =>
{
for (var i = 0; i < 25; i++)
{
if (!cancellationToken.IsCancellationRequested)
{
System.Diagnostics.Trace.WriteLine("==== : " + i.ToString());
await Task.Delay(progressRefresh);
}
}
},cancellationToken);
await Task.WhenAny(usefulWorkTask, progressUpdatetask);
cancellationSource.Cancel();
通过修改for (var i = 0; i < 25; i++)
限制i
我模拟长运行ning任务是否在进度任务之前完成或其他。按需工作。 WithCancellation
辅助方法可以完成这项工作,尽管目前有两种 'nested' Task.WhenAny
看起来很可疑。
我认为您需要在 progressUpdate
操作中检查 IsCancellationRequested
。
至于如何做你想做的事,this blog 讨论了一种扩展方法 WithCancellation
,它将使你停止等待你的长期 运行 任务。
当您在 button2_Click
上写类似 await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });
的内容时,您是从 UI 线程将操作调度到线程轮询线程,该线程轮询线程将操作发布到 UI线程。如果你直接调用动作,它会更快,因为它不会有两次上下文切换。
ConfigureAwait(false)
导致同步上下文未被捕获。我不应该在 UI 方法中使用,因为你肯定想在延续上做一些 UI 工作。
你不应该使用 Task.Factory.StartNew
而不是 Task.Run
除非你绝对有理由这样做。参见 this and this。
对于进度更新,考虑使用 Progress<T> class,因为它捕获同步上下文。
也许你应该尝试这样的事情:
private async void button2_Click(object sender, EventArgs e)
{
var taskAwait = 4000;
var cancellationSource = new CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
button2.Text = "Processing...";
var usefullWorkTask = Task.Run(async () =>
{
try
{
await Task.Dealy(taskAwait);
}
catch { }
},
cancellationToken);
var progress = new Progress<imt>(i => {
button2.Text = "Processing " + i.ToString();
});
var progressUpdateTask = Task.Run(async () =>
{
for(var i = 0; i < 10; i++)
{
progress.Report(i);
}
},
cancellationToken);
await Task.WhenAny(usefullWorkTask, progressUpdateTask);
cancellationSource.Cancel();
}
我同意 Paulo 回答中的所有观点 - 即使用现代解决方案(Task.Run
而不是 Task.Factory.StartNew
,Progress<T>
进行进度更新而不是手动发布到 SynchronizationContext
、Task.WhenAny
而不是异步代码的 Task.WaitAny
。
但要回答实际问题:
When I run CancellationSource.Cancel(), the long running task keeps working and does not cancel. After a timeout I am not interested in long running task or whatever it may result in.
这有两个部分:
- 如何编写响应取消请求的代码?
- 如何编写在取消后忽略任何响应的代码?
注意第一部分处理取消操作,第二部分实际上是处理取消等待 等待操作完成.
首先要注意的是:在操作本身中支持取消。对于 CPU 绑定代码(即 运行 循环),定期调用 token.ThrowIfCancellationRequested()
。对于 I/O-bound 代码,最好的选择是将 token
向下传递到下一个 API 层 - 大多数(但不是全部)I/O API 可以(应该)采取取消令牌。如果这不是一个选项,那么您可以选择忽略取消,或者您可以使用 token.Register
注册取消回调。有时,您可以从 Register
回调中调用一个单独的取消方法,有时您可以通过从回调中处理对象来使其工作(由于长期存在的 Win32 API 传统,这种方法通常有效当句柄关闭时取消句柄的所有 I/O)。不过,我不确定这是否适用于 SqlConnection.Open
。
接下来,取消等待。如果你只是想取消等待由于超时:
,这个相对简单
await Task.WhenAny(tWork, tProgress, Task.Delay(5000));
美好的一天!我正在为 WinForms UI 编写一个帮助程序库。开始使用 TPL async/await 机制并遇到此类代码示例的问题:
private SynchronizationContext _context;
public void UpdateUI(Action action)
{
_context.Post(delegate { action(); }, null);
}
private async void button2_Click(object sender, EventArgs e)
{
var taskAwait = 4000;
var progressRefresh = 200;
var cancellationSource = new System.Threading.CancellationTokenSource();
await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });
Action usefulWork = () =>
{
try
{
Thread.Sleep(taskAwait);
cancellationSource.Cancel();
}
catch { }
};
Action progressUpdate = () =>
{
int i = 0;
while (i < 10)
{
UpdateUI(() => { button2.Text = "Processing " + i.ToString(); });
Thread.Sleep(progressRefresh);
i++;
}
cancellationSource.Cancel();
};
var usefulWorkTask = new Task(usefulWork, cancellationSource.Token);
var progressUpdateTask = new Task(progressUpdate, cancellationSource.Token);
try
{
cancellationSource.Token.ThrowIfCancellationRequested();
Task tWork = Task.Factory.StartNew(usefulWork, cancellationSource.Token);
Task tProgress = Task.Factory.StartNew(progressUpdate, cancellationSource.Token);
await Task.Run(() =>
{
try
{
var res = Task.WaitAny(new[] { tWork, tProgress }, cancellationSource.Token);
}
catch { }
}).ConfigureAwait(false);
}
catch (Exception ex)
{
}
await Task.Run(() => { UpdateUI(() => { button2.Text = "button2"; }); });
}
基本上,想法是 运行 两个并行任务 - 一个是进度条或任何更新和一种超时控制器,另一个是长 运行ning 任务本身。无论哪个任务先完成,都会取消另一个任务。因此,取消 "progress" 任务应该没有问题,因为它有一个循环,我可以在其中检查任务是否被标记为已取消。问题出在长 运行ning 上。它可以是 Thread.Sleep() 或 SqlConnection.Open()。当我 运行 CancellationSource.Cancel() 时,长 运行ning 任务继续工作并且不会取消。超时后,我对长时间 运行ning 任务或它可能导致的任何结果不感兴趣。
正如混乱的代码示例所暗示的那样,我尝试了很多变体,none 给了我想要的效果。 Task.WaitAny() 之类的东西会冻结 UI... 有没有一种方法可以使这种取消起作用,或者甚至可能是一种不同的方法来对这些东西进行编码?
更新:
public static class Taskhelpers
{
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
}
return await task;
}
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
}
await task;
}
}
.....
var taskAwait = 4000;
var progressRefresh = 200;
var cancellationSource = new System.Threading.CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
var usefulWorkTask = Task.Run(async () =>
{
try
{
System.Diagnostics.Trace.WriteLine("WORK : started");
await Task.Delay(taskAwait).WithCancellation(cancellationToken);
System.Diagnostics.Trace.WriteLine("WORK : finished");
}
catch (OperationCanceledException) { } // just drop out if got cancelled
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine("WORK : unexpected error : " + ex.Message);
}
}, cancellationToken);
var progressUpdatetask = Task.Run(async () =>
{
for (var i = 0; i < 25; i++)
{
if (!cancellationToken.IsCancellationRequested)
{
System.Diagnostics.Trace.WriteLine("==== : " + i.ToString());
await Task.Delay(progressRefresh);
}
}
},cancellationToken);
await Task.WhenAny(usefulWorkTask, progressUpdatetask);
cancellationSource.Cancel();
通过修改for (var i = 0; i < 25; i++)
限制i
我模拟长运行ning任务是否在进度任务之前完成或其他。按需工作。 WithCancellation
辅助方法可以完成这项工作,尽管目前有两种 'nested' Task.WhenAny
看起来很可疑。
我认为您需要在 progressUpdate
操作中检查 IsCancellationRequested
。
至于如何做你想做的事,this blog 讨论了一种扩展方法 WithCancellation
,它将使你停止等待你的长期 运行 任务。
当您在 button2_Click
上写类似 await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });
的内容时,您是从 UI 线程将操作调度到线程轮询线程,该线程轮询线程将操作发布到 UI线程。如果你直接调用动作,它会更快,因为它不会有两次上下文切换。
ConfigureAwait(false)
导致同步上下文未被捕获。我不应该在 UI 方法中使用,因为你肯定想在延续上做一些 UI 工作。
你不应该使用 Task.Factory.StartNew
而不是 Task.Run
除非你绝对有理由这样做。参见 this and this。
对于进度更新,考虑使用 Progress<T> class,因为它捕获同步上下文。
也许你应该尝试这样的事情:
private async void button2_Click(object sender, EventArgs e)
{
var taskAwait = 4000;
var cancellationSource = new CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
button2.Text = "Processing...";
var usefullWorkTask = Task.Run(async () =>
{
try
{
await Task.Dealy(taskAwait);
}
catch { }
},
cancellationToken);
var progress = new Progress<imt>(i => {
button2.Text = "Processing " + i.ToString();
});
var progressUpdateTask = Task.Run(async () =>
{
for(var i = 0; i < 10; i++)
{
progress.Report(i);
}
},
cancellationToken);
await Task.WhenAny(usefullWorkTask, progressUpdateTask);
cancellationSource.Cancel();
}
我同意 Paulo 回答中的所有观点 - 即使用现代解决方案(Task.Run
而不是 Task.Factory.StartNew
,Progress<T>
进行进度更新而不是手动发布到 SynchronizationContext
、Task.WhenAny
而不是异步代码的 Task.WaitAny
。
但要回答实际问题:
When I run CancellationSource.Cancel(), the long running task keeps working and does not cancel. After a timeout I am not interested in long running task or whatever it may result in.
这有两个部分:
- 如何编写响应取消请求的代码?
- 如何编写在取消后忽略任何响应的代码?
注意第一部分处理取消操作,第二部分实际上是处理取消等待 等待操作完成.
首先要注意的是:在操作本身中支持取消。对于 CPU 绑定代码(即 运行 循环),定期调用 token.ThrowIfCancellationRequested()
。对于 I/O-bound 代码,最好的选择是将 token
向下传递到下一个 API 层 - 大多数(但不是全部)I/O API 可以(应该)采取取消令牌。如果这不是一个选项,那么您可以选择忽略取消,或者您可以使用 token.Register
注册取消回调。有时,您可以从 Register
回调中调用一个单独的取消方法,有时您可以通过从回调中处理对象来使其工作(由于长期存在的 Win32 API 传统,这种方法通常有效当句柄关闭时取消句柄的所有 I/O)。不过,我不确定这是否适用于 SqlConnection.Open
。
接下来,取消等待。如果你只是想取消等待由于超时:
,这个相对简单await Task.WhenAny(tWork, tProgress, Task.Delay(5000));