延迟后,我需要执行一种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();
}
}
UPD:
这种方法也无一例外地起作用
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)); ;
}
答案 0 :(得分:0)
行为不一致的原因是,从根本上说,第一种情况下异步委托的使用不正确。 Task
构造函数只是不接收Func<Task>
,在与构造函数一起使用的情况下,您的异步委托始终被解释为async void
而不是async Task
。如果在async Task
方法中引发了异常,则该异常将被捕获并放入Task
对象中,而对于async void
方法而言,这是不正确的,在这种情况下,异常会冒出气泡来同步上下文,它属于未处理异常的类别(您可以在this Stephen Cleary文章中了解详细信息)。因此,在使用构造函数的情况下会发生什么:将创建并启动一个应该启动异步流的任务。一旦到达Task.Delay(...)
返回承诺的时间点,任务即完成,并且不再与Task.Delay
延续中发生的任何事情相关(您可以通过将断点设置为{{1}来轻松地检入调试器} value.cts.Cancel()
词典中的任务对象的状态为_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()
的使用存在某些危险,因此不认为它们是创建任务的野兽实践(请参见there)。但是,如果需要应用诸如Task.Factory.StartNew
之类的特定选项,而不能直接与LongRunning
配合使用,则可以在某些警告中使用它。
答案 1 :(得分:-2)
我最终得到了以下解决方案:
public void ExecuteAfter(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
var cts = new CancellationTokenSource();
var task = new Task(() =>
{
cts.Token.WaitHandle.WaitOne(waitBeforeExecute);
if(cts.Token.IsCancellationRequested) return;
action();
}, cts.Token, TaskCreationOptions.LongRunning);
cancellationId = Guid.NewGuid();
_tasks.Add(cancellationId, (task, cts));
task.Start(TaskScheduler.Default);
}