注意:不是UI应用程序(因此与着名的async / await / synchronizationcontext问题无关)(参考:http://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Async-library-methods-should-consider-using-Task-ConfigureAwait-false-)
编辑
其实我的不好 – 这是TYPO导致了交易.我粘贴了下面的样本片段(伪),导致死锁.基本上,我没有基于’childtasks’进行组合,而是在’外部任务’上做了:(.看起来我不应该在看电视时写’异步’代码:).
我已经离开原始代码片段,因为它确实回答了我的第一个问题(与我之前的两个代码片段中的async / await和unwrap的区别).死锁让我分心看到实际的问题:).谢谢大家的评论.
static void Main(string[] args) { Task t = IndefinitelyBlockingTask(); t.Wait(); } static Task IndefinitelyBlockingTask() { List<Task> tasks = new List<Task>(); Task task = FooAsync(); tasks.Add(task); Task<Task> continuationTask = task.ContinueWith(t => { Task.Delay(10000); List<Task> childtasks = new List<Task>(); ////get child tasks //now INSTEAD OF ADDING CHILD TASKS,i added outer method TASKS. Typo :(:)! Task wa = Task.WhenAll(tasks/*TYPO*/); return wa; },TaskContinuationOptions.OnlyOnRanToCompletion); tasks.Add(continuationTask); Task unwrappedTask = continuationTask.Unwrap(); tasks.Add(unwrappedTask); Task whenall = Task.WhenAll(tasks.ToArray()); return whenall; }
代码片段在“解包”延续任务被无限期等待时被添加到我已经粘贴在伪(我的实际应用中的模式/成语块 – 而不是样本)下面的任务的聚合/链中,代码片段(#1)无限期地等待’unwrapped’任务已添加到列表中. VS Debugger的“Debugger windows threads”窗口显示该线程只是在ManualResetEventSlim.Wait上阻塞.
代码片段与async / await一起使用,并删除未解包的任务然后我删除(在调试时随机),这个unwrap语句并在lambda中使用async / await(请参见下文).令人惊讶的是它有效.但我不确定为什么:(?.
问题
>不使用unwrap和async / await在下面的代码片段中提供相同的用途吗?我最初只是首选片段#1,因为我只是想避免生成过多的代码,因为调试器不是那么友好(特别是在异常通过链式任务传播的错误情况下 – 异常中的callstack显示movenext而不是我的实际代码) .如果是,那么这是TPL中的错误吗?
>我错过了什么?如果它们相同,哪种方法更受欢迎?
关于Debugger Tasks窗口’Debugger Tasks’窗口的注意事项没有显示任何细节(注意它在我的环境中工作不正确(至少我的理解),因为它从不显示未安排的任务并且显示活动任务)
无限期等待ManualResetEventSlim.Wait的代码片段
static Task IndefinitelyBlockingTask() { List<Task> tasks = new List<Task>(); Task task = FooAsync(); tasks.Add(task); Task<Task> continuationTask = task.ContinueWith(t => { List<Task> childTasks = new List<Task>(); for (int i = 1; i <= 5; i++) { var ct = FooAsync(); childTasks.Add(ct); } Task wa = Task.WhenAll(childTasks.ToArray()); return wa; },TaskContinuationOptions.OnlyOnRanToCompletion); tasks.Add(continuationTask); Task unwrappedTask = continuationTask.Unwrap(); //commenting below code and using async/await in lambda works (please see below code snippet) tasks.Add(unwrappedTask); Task whenall = Task.WhenAll(tasks.ToArray()); return whenall; }
代码片段在lambda中使用async / await而不是unwrap
static Task TaskWhichWorks() { List<Task> tasks = new List<Task>(); Task task = FooAsync(); tasks.Add(task); Task<Task> continuationTask = task.ContinueWith(async t => { List<Task> childTasks = new List<Task>(); for (int i = 1; i <= 5; i++) { var ct = FooAsync(); childTasks.Add(ct); } Task wa = Task.WhenAll(childTasks.ToArray()); await wa.ConfigureAwait(continueOnCapturedContext: false); },TaskContinuationOptions.OnlyOnRanToCompletion); tasks.Add(continuationTask); Task whenall = Task.WhenAll(tasks.ToArray()); return whenall; }
mscorlib.dll!System.Threading.ManualResetEventSlim.Wait(int millisecondsTimeout,System.Threading.CancellationToken cancellationToken) Unknown mscorlib.dll!System.Threading.Tasks.Task.SpinThenBlockingWait(int millisecondsTimeout,System.Threading.CancellationToken cancellationToken) Unknown mscorlib.dll!System.Threading.Tasks.Task.InternalWait(int millisecondsTimeout,System.Threading.CancellationToken cancellationToken) Unknown mscorlib.dll!System.Threading.Tasks.Task.Wait(int millisecondsTimeout,System.Threading.CancellationToken cancellationToken) Unknown mscorlib.dll!System.Threading.Tasks.Task.Wait() Unknown
最好的祝福
解决方法
首先要做的事情是:传递给您的ContinueWith的lambda的差异是微不足道的:在功能上,这部分在两个例子中是相同的(至少就我所见).
这是我用于测试的FooAsync实现:
static Task FooAsync() { return Task.Delay(500); }
我发现很奇怪的是,使用这个实现你的IndefinitelyBlockingTask花费的时间是TaskWhichWorks的两倍(分别为1秒vs~500 ms).显然,由于Unwrap,行为已经改变.
有敏锐眼光的人可能会立即发现问题,但我个人不会使用任务延续或解开那么多,所以花了一点时间沉入其中.
下面是踢球者:除非你在两种情况下都使用Unwrap继续,否则ContinueWith安排的任务会同步完成(并立即完成 – 无论循环内创建的任务有多长时间).在lambda(Task.WhenAll(childTasks.ToArray())中创建的任务,让我们称之为内部任务)以一种即发即忘的方式进行调度并运行.
展开从ContinueWith返回的任务意味着内部任务不再是一劳永逸 – 它现在是执行链的一部分,当你将它添加到列表中时,外部任务(Task.WhenAll(tasks.ToArray( )))在内部任务完成之前无法完成).
使用ContinueWith(async()=> {})不会改变上述行为,因为async lambda返回的任务不会自动解包(想想
// These two have similar behavIoUr and // are interchangeable for our purposes. Task.Run(() => Task.Delay(500)) Task.Run(async () => await Task.Delay(500));
VS
Task.Factory.StartNew(() => Task.Delay(500))
Task.Run调用内置Unwrap(见http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs#0fb2b4d9262599b9#references); StartNew调用没有,它返回的任务只是立即完成而不等待内部任务.在这方面,ContinueWith类似于StartNew.
边注
重现使用Unwrap时观察到的行为的另一种方法是确保在循环内创建的任务(或它们的延续)附加到父级,导致父任务(由ContinueWith创建)不会转换到完成状态,直到所有子级任务已经完成.
for (int i = 1; i <= 5; i++) { var ct = FooAsync().ContinueWith(_ => { },TaskContinuationOptions.AttachedToParent); childTasks.Add(ct); }
回到原来的问题
在您当前的实现中,即使您等待Task.WhenAll(tasks.ToArray())作为外部方法的最后一行,该方法仍将在ContinueWith lambda内创建的任务完成之前返回.即使在ContinueWith中创建的任务永远不会完成(我的猜测就是你的生产代码中正是发生的事情),外部方法仍然会返回正常.
所以就是这样,上面代码所有出乎意料的事情都是由愚蠢的ContinueWith引起的,除非你使用Unwrap,否则它基本上会“掉头”. async / await绝不是原因或治愈方法(虽然,不可否认,它可以并且可能应该用于以更合理的方式重写您的方法 – 延续很难处理导致诸如此类的问题).
那么生产中发生了什么
以上所有这些让我相信,在您的ContinueWith lambda中旋转的任务之一中存在死锁,从而导致那个内在的Task.WhenAll永远不会在生产修剪中完成.
不幸的是,你还没有发布一个简洁的问题重复(我想我可以为你做这些信息,但这不是我的工作),甚至生产代码,所以这是一个很大的解决方案我可以给.
您没有使用伪代码观察所描述的行为这一事实应该暗示您可能最终剥离导致问题的位.如果你认为这听起来很愚蠢,那是因为它是,这就是为什么我最终收回我原来的问题,尽管它是我偶然遇到的最奇怪的异常问题.
结论:看看你的ContinueWith lambda.
最后编辑
你坚持认为Unwrap和await做类似的事情,这是真的(不是因为它最终会混淆任务组合,而是真实的 – 至少在这个例子的目的).但是,话虽如此,你从来没有完全使用await重新创建Unwrap语义,所以这个方法的行为有什么不同呢?这是TaskWhichWorks的await,其行为类似于Unwrap示例(当应用于您的生产代码时,它也容易受到死锁问题的影响):
static async Task TaskWhichUsedToWorkButNotAnymore() { List<Task> tasks = new List<Task>(); Task task = FooAsync(); tasks.Add(task); Task<Task> continuationTask = task.ContinueWith(async t => { List<Task> childTasks = new List<Task>(); for (int i = 1; i <= 5; i++) { var ct = FooAsync(); childTasks.Add(ct); } Task wa = Task.WhenAll(childTasks.ToArray()); await wa.ConfigureAwait(continueOnCapturedContext: false); },TaskContinuationOptions.OnlyOnRanToCompletion); tasks.Add(continuationTask); // Let's Unwrap the async/await way. // Pay attention to the return type. // The resulting task represents the // completion of the task started inside // (and returned by) the ContinueWith delegate. // Without this you have no reference,and no // way of waiting for,the inner task. Task unwrappedTask = await continuationTask; // Boom! This method now has the // same behavIoUr as the other one. tasks.Add(unwrappedTask); await Task.WhenAll(tasks.ToArray()); // Another way of "unwrapping" the // continuation just to drive the point home. // This will complete immediately as the // continuation task as well as the task // started inside,and returned by the continuation // task,have both completed at this point. await await continuationTask; }