我有以下情况,我认为这可能很常见:
有一个任务(UI命令处理程序)可以同步或异步完成。
命令到达的速度可能快于处理速度。
如果已经有命令的待处理任务,则应将新的命令处理程序任务排队并按顺序处理。
每个新任务的结果可能取决于先前任务的结果。
应该注意取消,但是为了简单起见,我想将其保留在此问题的范围之外。另外,线程安全性(并发性)不是必需的,但必须支持重入。
这是我要实现的基本示例(为简单起见,作为控制台应用程序):
using System; using System.Threading.Tasks; namespace ConsoleApp { class Program { static void Main(string[] args) { var asyncOp = new AsyncOp<int>(); Func<int, Task<int>> handleAsync = async (arg) => { Console.WriteLine("this task arg: " + arg); //await Task.Delay(arg); // make it async return await Task.FromResult(arg); // sync }; Console.WriteLine("Test #1..."); asyncOp.RunAsync(() => handleAsync(1000)); asyncOp.RunAsync(() => handleAsync(900)); asyncOp.RunAsync(() => handleAsync(800)); asyncOp.CurrentTask.Wait(); Console.WriteLine("\nPress any key to continue to test #2..."); Console.ReadLine(); asyncOp.RunAsync(() => { asyncOp.RunAsync(() => handleAsync(200)); return handleAsync(100); }); asyncOp.CurrentTask.Wait(); Console.WriteLine("\nPress any key to exit..."); Console.ReadLine(); } // AsyncOp class AsyncOp<T> { Task<T> _pending = Task.FromResult(default(T)); public Task<T> CurrentTask { get { return _pending; } } public Task<T> RunAsync(Func<Task<T>> handler) { var pending = _pending; Func<Task<T>> wrapper = async () => { // await the prev task var prevResult = await pending; Console.WriteLine("\nprev task result: " + prevResult); // start and await the handler return await handler(); }; _pending = wrapper(); return _pending; } } } }
输出:
测试#1 ... 上一个任务结果:0 此任务参数:1000 上一个任务结果:1000 此任务arg:900 上一个任务结果:900 此任务arg:800 按任意键继续测试#2 ... 上一个任务结果:800 上一个任务结果:800 此任务arg:200 此任务arg:100 按任何一个键退出...
它会按照要求工作,直到在测试2中引入可重入性为止:
asyncOp.RunAsync(() => { asyncOp.RunAsync(() => handleAsync(200)); return handleAsync(100); });
所需的输出应该是100,200而不是200,100因为有已经是一个悬而未决外任务100。这显然是因为内部任务同步执行,从而破坏var pending = _pending; /* ... */ _pending = wrapper()了外部任务的逻辑。
100
200
var pending = _pending; /* ... */ _pending = wrapper()
如何使其也适用于测试2?
一种解决方案是使用强制每个任务执行异步Task.Factory.StartNew(..., TaskScheduler.FromCurrentSynchronizationContext()。但是,我不想在可能内部同步的命令处理程序上强加异步执行。另外,我也不想依赖于任何特定同步上下文的行为(即,Task.Factory.StartNew在实际开始创建的任务之前,依赖于它的返回)。
Task.Factory.StartNew(..., TaskScheduler.FromCurrentSynchronizationContext()
Task.Factory.StartNew
在现实生活的项目中,我负责AsyncOp上述工作,但无法控制命令处理程序(即内部的任何内容handleAsync)。
AsyncOp
handleAsync
我几乎忘记了可以Task手动构建而不需要启动或调度它。然后,“ Task.Factory.StartNew”与“ new Task(…)。Start”使我回到正轨。我认为这是Task<TResult>构造函数实际上可能有用的少数几种情况之一,还有嵌套的任务(Task<Task<T>>)和Task.Unwrap():
Task
Task<TResult>
Task<Task<T>>
Task.Unwrap()
// AsyncOp class AsyncOp<T> { Task<T> _pending = Task.FromResult(default(T)); public Task<T> CurrentTask { get { return _pending; } } public Task<T> RunAsync(Func<Task<T>> handler, bool useSynchronizationContext = false) { var pending = _pending; Func<Task<T>> wrapper = async () => { // await the prev task var prevResult = await pending; Console.WriteLine("\nprev task result: " + prevResult); // start and await the handler return await handler(); }; var task = new Task<Task<T>>(wrapper); var inner = task.Unwrap(); _pending = inner; task.RunSynchronously(useSynchronizationContext ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Current); return inner; } }
测试#1 ... 上一个任务结果:0 此任务参数:1000 上一个任务结果:1000 此任务arg:900 上一个任务结果:900 此任务arg:800 按任意键继续测试#2 ... 上一个任务结果:800 此任务arg:100 上一个任务结果:100 此任务arg:200
现在,如果需要,AsyncOp可以通过添加一个lockto保护来使线程安全变得非常容易_pending。
lock
_pending