c# - 如何毫无例外地取消任务?
问题描述
LongRunning
我需要延迟后执行一种任务。每个任务都可以取消。我更TPL
喜欢cancellationToken
.
由于我的任务运行时间很长,并且在开始任务之前必须将其放入字典中,因此我必须使用new Task()
. 但是我遇到了不同的行为——当任务是在它抛出new Task()
之后创建的,而使用它创建的任务不会抛出异常。Cancel()
TaskCanceledException
Task.Run
通常我需要认识到差异而不是得到TaskCanceledException
.
这是我的代码:
internal sealed class Worker : IDisposable
{
private readonly IDictionary<Guid, (Task task, CancellationTokenSource cts)> _tasks =
new Dictionary<Guid, (Task task, CancellationTokenSource cts)>();
public void ExecuteAfter(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
var cts = new CancellationTokenSource();
var task = new Task(async () =>
{
await Task.Delay(waitBeforeExecute, cts.Token);
action();
}, cts.Token, TaskCreationOptions.LongRunning);
cancellationId = Guid.NewGuid();
_tasks.Add(cancellationId, (task, cts));
task.Start(TaskScheduler.Default);
}
public void ExecuteAfter2(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
var cts = new CancellationTokenSource();
cancellationId = Guid.NewGuid();
_tasks.Add(cancellationId, (Task.Run(async () =>
{
await Task.Delay(waitBeforeExecute, cts.Token);
action();
}, cts.Token), cts));
}
public void Abort(Guid cancellationId)
{
if (_tasks.TryGetValue(cancellationId, out var value))
{
value.cts.Cancel();
//value.task.Wait();
_tasks.Remove(cancellationId);
Dispose(value.cts);
Dispose(value.task);
}
}
public void Dispose()
{
if (_tasks.Count > 0)
{
foreach (var t in _tasks)
{
Dispose(t.Value.cts);
Dispose(t.Value.task);
}
_tasks.Clear();
}
}
private static void Dispose(IDisposable obj)
{
if (obj == null)
{
return;
}
try
{
obj.Dispose();
}
catch (Exception ex)
{
//Log.Exception(ex);
}
}
}
internal class Program
{
private static void Main(string[] args)
{
Action act = () => Console.WriteLine("......");
Console.WriteLine("Started");
using (var w = new Worker())
{
w.ExecuteAfter(act, TimeSpan.FromMilliseconds(10000), out var id);
//w.ExecuteAfter2(act, TimeSpan.FromMilliseconds(10000), out var id);
Thread.Sleep(3000);
w.Abort(id);
}
Console.WriteLine("Enter to exit");
Console.ReadKey();
}
}
升级版:
这种方法也无一例外地有效
public void ExecuteAfter3(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
var cts = new CancellationTokenSource();
cancellationId = Guid.NewGuid();
_tasks.Add(cancellationId, (Task.Factory.StartNew(async () =>
{
await Task.Delay(waitBeforeExecute, cts.Token);
action();
}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default), cts)); ;
}
解决方案
不一致行为的原因是从根本上不正确地使用了第一种情况下的异步委托。构造Task
函数只是不接收Func<Task>
,并且您的异步委托总是被解释为async void
不async Task
使用构造函数的情况。如果在方法中引发异常,async Task
它会被捕获并放入Task
对象中,这对于方法来说是不正确的async void
,在这种情况下,异常只会从方法中冒泡到同步上下文中,并属于未处理异常的类别(您可以熟悉这篇Stephen Cleary 文章中的详细信息)。那么在使用构造函数的情况下会发生什么:创建并启动一个应该启动异步流程的任务。一旦到达点Task.Delay(...)
返回一个承诺,任务完成并且它与Task.Delay
继续发生的任何事情都没有关系(您可以通过设置断点来轻松检查调试器,以确保字典value.cts.Cancel()
中的任务对象具有状态,而任务委托基本上仍在运行) . 当请求取消时,方法内部会引发异常,并且不存在任何 Promise 对象被提升到应用程序域。_tasks
RanToCompletetion
Task.Delay
在情况Task.Run
不同的情况下,因为此方法的重载能够在内部接受Func<Task>
或Func<Task<T>>
解包任务,以返回底层承诺而不是包装任务,从而确保_tasks
字典中的正确任务对象和正确的错误处理。
第三种情况尽管它没有抛出异常,但它是部分正确的。不像Task.Run
,Task.Factory.StartNew
不会打开底层任务以返回承诺,因此存储在 中的_tasks
任务只是包装器任务,就像在构造函数的情况下一样(您可以再次使用调试器检查其状态)。但是它能够理解Func<Task>
参数,因此异步委托具有async Task
签名,至少允许在底层任务中处理和存储异常。为了获得这个基础任务,Task.Factory.StartNew
您需要使用Unwrap()
扩展方法自己解开任务。
由于Task.Factory.StartNew
与它的应用程序相关的某些危险,它不被视为创建任务的野兽实践(参见那里)。但是,如果您需要应用LongRunning
无法直接应用的特定选项,则它可以与一些警告一起使用Task.Run
。
推荐阅读
- android-studio - 加载程序中的错误:java.lang.NullPointerException:尝试调用接口方法“boolean android.database.Cursor.moveToFirst()”
- r - R - 未列出嵌套的日期列表
- r - 朴素贝叶斯 2 类分类,下标越界
- string - Python 3.6 字符串格式化
- .net-core - 无法在 Windows Server 2008 R2 中运行 dotnet core 2 控制台应用程序
- apache-spark - PySpark - 拆分字符串列并连接其中的一部分以形成新列
- javascript - NodeJS,module.export 异步属性
- git - Git合并指定提交
- c++ - 使用 C++ 和修改基类的私有/受保护属性
- asp.net - ASP.net 内部服务器错误 500.19