同一任务的多个等待可能会导致阻塞
Multiple await on same task may cause blocking
在同一个任务上使用多个等待要小心。
我在尝试使用 BlockingCollection.GetConsumingEnumerable()
方法时遇到过这种情况。
并以这个简化的测试结束。
class TestTwoAwaiters
{
public void Test()
{
var t = Task.Delay(1000).ContinueWith(_ => Utils.WriteLine("task complete"));
var w1 = FirstAwaiter(t);
var w2 = SecondAwaiter(t);
Task.WaitAll(w1, w2);
}
private async Task FirstAwaiter(Task t)
{
await t;
//await t.ContinueWith(_ => { });
Utils.WriteLine("first wait complete");
Task.Delay(3000).Wait(); // execute blocking operation
}
private async Task SecondAwaiter(Task t)
{
await t;
Utils.WriteLine("second wait complete");
Task.Delay(3000).Wait(); // execute blocking operation
}
}
我认为这里的问题是任务的继续将相应地在一个线程上执行订阅者。
如果一个等待者执行阻塞操作(例如 BlockingCollection.GetConsumingEnumerable()
的屈服),它将阻止其他等待者并且他们无法继续他们的工作。
我认为一个可能的解决方案是在等待任务之前调用 ContinueWith()
。
它将把一个延续分成两部分,阻塞操作将在一个新线程上执行。
有人可以确认或反驳多次等待任务的可能性吗?
如果它很常见,那么绕过阻塞的正确方法是什么?
考虑以下代码:
private static async Task Test() {
Console.WriteLine("1: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("2: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("3: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("4: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
如果你 运行 它,你将看到以下输出:
1: 9, thread pool: False
2: 6, thread pool: True
3: 6, thread pool: True
4: 6, thread pool: True
你在这里看到,如果没有 SynchonizationContext(或者你不使用 ConfigureAwait)并且等待完成后它已经 运行正在线程池线程上,它将不会 更改线程以继续。这正是您的代码中发生的事情:在 "await t" 语句在 FirstAwaiter 和 SecondAwaiter 中完成后,继续 运行s 在同一线程上 在这两种情况下,因为它是线程延迟(1000)运行 的池线程。当然,当 FirstAwaiter 执行它的延续时,SecondAwaiter 将阻塞,因为它的延续被发布到同一个线程池线程。
编辑:如果您将使用 ContinueWith 而不是 await,您可以"fix" 解决您的问题(但仍请注意对您问题的评论):
internal class TestTwoAwaiters {
public void Test() {
Console.WriteLine("Mail thread is {0}", Thread.CurrentThread.ManagedThreadId);
var t = Task.Delay(1000).ContinueWith(_ => {
Console.WriteLine("task complete on {0}", Thread.CurrentThread.ManagedThreadId);
});
var w1 = FirstAwaiter(t);
var w2 = SecondAwaiter(t);
Task.WaitAll(w1, w2);
}
private static Task FirstAwaiter(Task t) {
Console.WriteLine("First await on {0}", Thread.CurrentThread.ManagedThreadId);
return t.ContinueWith(_ =>
{
Console.WriteLine("first wait complete on {0}", Thread.CurrentThread.ManagedThreadId);
Task.Delay(3000).Wait();
});
}
private static Task SecondAwaiter(Task t) {
Console.WriteLine("Second await on {0}", Thread.CurrentThread.ManagedThreadId);
return t.ContinueWith(_ => {
Console.WriteLine("Second wait complete on {0}", Thread.CurrentThread.ManagedThreadId);
Task.Delay(3000).Wait();
});
}
}
这里有两种扩展方法,一种用于Task
,一种用于Task<TResult>
,保证await
之后的异步延续。结果和异常按预期传播。
public static class TaskExtensions
{
/// <summary>Creates a continuation that executes asynchronously when the target
/// <see cref="Task"/> completes.</summary>
public static Task ContinueAsync(this Task task)
{
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
/// <summary>Creates a continuation that executes asynchronously when the target
/// <see cref="Task{TResult}"/> completes.</summary>
public static Task<TResult> ContinueAsync<TResult>(this Task<TResult> task)
{
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
}
用法示例:
await t.ContinueAsync();
更新: 同步执行延续的问题行为仅影响 .NET Framework。 .NET Core 不受影响(延续在线程池线程中异步执行),因此上述解决方法仅对 .NET Framework 上的应用程序有用 运行。
在同一个任务上使用多个等待要小心。
我在尝试使用 BlockingCollection.GetConsumingEnumerable()
方法时遇到过这种情况。
并以这个简化的测试结束。
class TestTwoAwaiters
{
public void Test()
{
var t = Task.Delay(1000).ContinueWith(_ => Utils.WriteLine("task complete"));
var w1 = FirstAwaiter(t);
var w2 = SecondAwaiter(t);
Task.WaitAll(w1, w2);
}
private async Task FirstAwaiter(Task t)
{
await t;
//await t.ContinueWith(_ => { });
Utils.WriteLine("first wait complete");
Task.Delay(3000).Wait(); // execute blocking operation
}
private async Task SecondAwaiter(Task t)
{
await t;
Utils.WriteLine("second wait complete");
Task.Delay(3000).Wait(); // execute blocking operation
}
}
我认为这里的问题是任务的继续将相应地在一个线程上执行订阅者。
如果一个等待者执行阻塞操作(例如 BlockingCollection.GetConsumingEnumerable()
的屈服),它将阻止其他等待者并且他们无法继续他们的工作。
我认为一个可能的解决方案是在等待任务之前调用 ContinueWith()
。
它将把一个延续分成两部分,阻塞操作将在一个新线程上执行。
有人可以确认或反驳多次等待任务的可能性吗? 如果它很常见,那么绕过阻塞的正确方法是什么?
考虑以下代码:
private static async Task Test() {
Console.WriteLine("1: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("2: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("3: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("4: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
如果你 运行 它,你将看到以下输出:
1: 9, thread pool: False
2: 6, thread pool: True
3: 6, thread pool: True
4: 6, thread pool: True
你在这里看到,如果没有 SynchonizationContext(或者你不使用 ConfigureAwait)并且等待完成后它已经 运行正在线程池线程上,它将不会 更改线程以继续。这正是您的代码中发生的事情:在 "await t" 语句在 FirstAwaiter 和 SecondAwaiter 中完成后,继续 运行s 在同一线程上 在这两种情况下,因为它是线程延迟(1000)运行 的池线程。当然,当 FirstAwaiter 执行它的延续时,SecondAwaiter 将阻塞,因为它的延续被发布到同一个线程池线程。
编辑:如果您将使用 ContinueWith 而不是 await,您可以"fix" 解决您的问题(但仍请注意对您问题的评论):
internal class TestTwoAwaiters {
public void Test() {
Console.WriteLine("Mail thread is {0}", Thread.CurrentThread.ManagedThreadId);
var t = Task.Delay(1000).ContinueWith(_ => {
Console.WriteLine("task complete on {0}", Thread.CurrentThread.ManagedThreadId);
});
var w1 = FirstAwaiter(t);
var w2 = SecondAwaiter(t);
Task.WaitAll(w1, w2);
}
private static Task FirstAwaiter(Task t) {
Console.WriteLine("First await on {0}", Thread.CurrentThread.ManagedThreadId);
return t.ContinueWith(_ =>
{
Console.WriteLine("first wait complete on {0}", Thread.CurrentThread.ManagedThreadId);
Task.Delay(3000).Wait();
});
}
private static Task SecondAwaiter(Task t) {
Console.WriteLine("Second await on {0}", Thread.CurrentThread.ManagedThreadId);
return t.ContinueWith(_ => {
Console.WriteLine("Second wait complete on {0}", Thread.CurrentThread.ManagedThreadId);
Task.Delay(3000).Wait();
});
}
}
这里有两种扩展方法,一种用于Task
,一种用于Task<TResult>
,保证await
之后的异步延续。结果和异常按预期传播。
public static class TaskExtensions
{
/// <summary>Creates a continuation that executes asynchronously when the target
/// <see cref="Task"/> completes.</summary>
public static Task ContinueAsync(this Task task)
{
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
/// <summary>Creates a continuation that executes asynchronously when the target
/// <see cref="Task{TResult}"/> completes.</summary>
public static Task<TResult> ContinueAsync<TResult>(this Task<TResult> task)
{
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
}
用法示例:
await t.ContinueAsync();
更新: 同步执行延续的问题行为仅影响 .NET Framework。 .NET Core 不受影响(延续在线程池线程中异步执行),因此上述解决方法仅对 .NET Framework 上的应用程序有用 运行。