我想做一件简单的事情(假设ContinueWith
是线程安全的(:
readonly Task _task = Task.CompletedTask;
// thread A: conditionally prolong the task
if(condition)
_task.ContinueWith(o => ...);
// thread B: await for everything
await _task;
问题:在上面的代码中,await _task
立即返回,忽略是否有内部任务。
扩展_task
可以扩展多个ContinueWith
的要求,我该如何等待所有这些都完成?
当然我可以尝试用旧的线程安全方式:
Task _task = Task.CompletedTask;
readonly object _lock = new object();
// thread A
if(condition)
_lock(_lock)
_task = _task.ContinueWith(o => ...); // storing the latest inner task
// thread B
lock(_lock)
await _task;
或者将任务存储在线程安全集合中并使用Task.WaitAll
。
但我很好奇第一个片段是否有简单的修复方法?
您的旧版本还可以,除了保持锁定的await
;您应该在持有锁的同时将_task
复制到本地变量,释放锁,然后才复制await
但ContinueWith工作流并不是实现该逻辑的最佳方式。ContinueWith是在.NET 4.0中引入的,在异步等待成为该语言的一部分之前。
在现代C#中,我认为最好这样做:
static async Task runTasks()
{
if( condition1 )
await handle1();
if( condition2 )
await handle2();
}
readonly Task _task = runTasks();
或者如果你想让它们并行运行:
IEnumerable<Task> parallelTasks()
{
if( condition1 )
yield return handle1();
if( condition2 )
yield return handle2();
}
readonly Task _task = Task.WhenAll( parallelTasks() );
附言:如果条件是动态变化的,对于顺序版本,您应该在第一个等待之前将其值复制到局部变量。
更新:所以你是说你的线程A延长了任务以响应动态发生的一些事件?如果是的话,那么您的旧方法是可以的,它很简单而且有效,只需修复lock(){ await ... }
并发错误即可。
从理论上讲,更清洁的方法可能有点像反应堆模式,但不幸的是,它要复杂得多。这是我的一些稍微相似的开源示例。您不需要并发限制信号量,也不需要第二个队列,但您需要公开Task类型的属性,用CompletedTask初始化它,在发布第一个任务时用TaskCompletionSource<bool>
从post方法创建的新任务替换,并在没有更多发布任务时在runAsyncProcessor方法中完成该任务。
在原始代码中,await _task;
会立即返回,因为task
已完成。你需要做
_task = _task.ContinueWith(...);
你不必使用锁。你在同一条线上工作。在您的"锁定"版本的代码中,您确实使用了_task = _task.ContinueWith(...)
,这就是它工作的原因,而不是因为锁定。
EDIT:刚才看到您想在一个线程中执行_task = _task.ContinueWith(...);
和_task = _task.ContinueWith(...)
。
这和设计一样糟糕。永远不要将线程和任务结合在一起。您应该选择仅任务的解决方案,或者选择仅线程的解决方案。
如果您选择仅限任务的解决方案(推荐(,只需创建一个良好的工作任务序列,然后根据前一个任务的结果决定如何执行每个任务。
如果您选择仅限线程的解决方案,则可能需要使用ManualResetEvent
句柄来同步您的任务。