环境: Windows Server 2012,.net 4.5,Visual Studio 2013。
注意:不是UI Application(因此与著名的async / await / synchronizationcontext问题无关)(参考:http : //channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Async-library-methods-should -考虑使用Task-ConfigureAwait-false-)
这是造成死锁的错别字。我在示例代码段(伪)下粘贴了导致死锁的示例。基本上,我不是根据'childtasks'组成所有内容,而是针对'outer task':(。看起来我不应该在看电视的同时写'async'代码:)。
我已经保留了原始代码段,以便为Kirill的响应提供上下文,因为它确实回答了我的第一个问题(前两个代码段与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;
}
当我将“展开的”延续任务添加到我粘贴在伪(实际应用中的模式/习惯用语块-不在示例中)下面的聚合/任务链中时,该代码片段将无限期等待,该代码片段将无限期等待“解包”任务已添加到列表中。VS Debugger的“ Debugger + Windows +线程”窗口显示线程只是在ManualResetEventSlim.Wait上阻塞。
可以与async / await一起使用的代码片段,以及删除未包装的任务的代码然后,我删除了(在调试时随机地)此展开的语句,并在lambda中使用了async / await(请参见下文)。令人惊讶的是它有效。但我不确定为什么:(?
问题
在下面的代码片段中,使用unwrap和async / await是否没有达到相同的目的?我最初只是喜欢代码片段#1,因为我只是想避免生成过多的代码,因为调试器不是那么友好(尤其是在错误通过链式任务传播的错误情况下-异常中的调用栈显示了movenext而不是我的实际代码) 。如果是,那么这是TPL中的错误吗?
我想念什么?如果它们相同,则首选哪种方法?
关于“调试器+任务”窗口的注释“调试器+任务”窗口没有显示任何详细信息(请注意,它在我的环境中无法正常工作(至少是我的理解),因为它从不显示未计划的任务,而通常显示活动任务)
无限期地等待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一起使用而不是解包的代码片段
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
好的,让我们尝试深入了解这里发生的事情。
首先,第一件事:传递给您的lambda的差异ContinueWith
微不足道:在两个示例中,该部分在功能上是相同的(至少就我所知)。
这是FooAsync
我用于测试的实现:
static Task FooAsync()
{
return Task.Delay(500);
}
我感到奇怪的是,使用此实现,您IndefinitelyBlockingTask
花费了两倍的时间TaskWhichWorks
(分别是1秒与〜500 ms)。显然,由于,行为已更改Unwrap
。
敏锐的眼睛可能会立即发现问题,但就我个人而言,我并没有使用任务延续或 Unwrap
那么多任务,因此花了一些时间才得以解决。
关键是:除非Unwrap
在两种情况下都使用连续性,否则安排的任务将ContinueWith
同步完成(并立即完成-无论循环中创建的任务需要多长时间)。在lambda内部创建的任务(Task.WhenAll(childTasks.ToArray())
,我们称其为内部任务)以即发即弃的方式进行调度,并在观察时运行。
Unwrap
ping从中返回的任务ContinueWith
意味着内部任务不再是一劳永逸的-它现在是执行链的一部分,并且当您将其添加到列表中时,外部任务(Task.WhenAll(tasks.ToArray())
)在内部任务完成之前无法完成)。
使用ContinueWith(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));
与
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);
}
回到原始问题
在当前的实现中,即使您拥有await Task.WhenAll(tasks.ToArray())
外部方法的最后一行,该方法仍会在lambda内部创建的任务完成之前返回ContinueWith
。即使内部创建的任务ContinueWith
从未完成(我的猜测就是您的生产代码中正在发生的事情),外部方法仍会返回正常。
就是这样,上面的代码中所有意外的事情都是由愚蠢造成的,ContinueWith
除非您使用,否则愚蠢的本质会“掉线” Unwrap
。async
/await
决不是原因或治愈方法(尽管,可以承认,它可以并且可能应该以更明智的方式用于重写您的方法-继续进行操作很难导致此类问题)。
那么生产中发生了什么
以上所有这些使我相信,在ContinueWith
lambda内启动的一项任务中存在死锁,导致该内部Task.WhenAll
人员永远无法完成生产调整。
不幸的是,您尚未发布该问题的简明再现(我想我可以为您提供上述信息,但实际上不是我的工作),甚至还没有生产代码,因此这是一个解决方案尽我所能
您未使用伪代码观察到所描述的行为的事实应该表明您可能最终将导致问题的位剥离了。如果您认为这听起来很愚蠢,那是因为这是为什么,尽管实际上这是我一段时间以来遇到的唯一最奇怪的异步问题,但我最终还是撤回了对该问题的最初支持。
结论:看你的ContinueWith
λ。
最终编辑
您坚持要这样做Unwrap
并await
做类似的事情,这是正确的(不是真的,因为它最终会干扰任务的组成,但实际上是正确的-至少出于此示例的目的)。但是,话虽如此,您从未使用完全重新创建过Unwrap
语义await
,因此,该方法的行为是否真的会引起很大的惊喜?这是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;
}
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句