C# async/await 控制台应用程序中的奇怪行为
C# async/await strange behavior in console app
我构建了一些 async/await 演示控制台应用程序并得到了奇怪的结果。代码:
class Program
{
public static void BeginLongIO(Action act)
{
Console.WriteLine("In BeginLongIO start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
act();
Console.WriteLine("In BeginLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
}
public static Int32 EndLongIO()
{
Console.WriteLine("In EndLongIO start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(500);
Console.WriteLine("In EndLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
return 42;
}
public static Task<Int32> LongIOAsync()
{
Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
var tcs = new TaskCompletionSource<Int32>();
BeginLongIO(() =>
{
try { tcs.TrySetResult(EndLongIO()); }
catch (Exception exc) { tcs.TrySetException(exc); }
});
Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
return tcs.Task;
}
public async static Task<Int32> DoAsync()
{
Console.WriteLine("In DoAsync start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
var res = await LongIOAsync();
Thread.Sleep(1000);
Console.WriteLine("In DoAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
return res;
}
static void Main(String[] args)
{
ticks = DateTime.Now.Ticks;
Console.WriteLine("In Main start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
DoAsync();
Console.WriteLine("In Main exec... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Console.WriteLine("In Main end... \t\t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
}
private static Int64 ticks;
}
结果如下:
可能我没有完全理解await到底是什么。我认为如果执行等待,那么执行 returns 到调用方方法和等待在另一个线程中运行的任务。在我的示例中,所有操作都在一个线程中执行,并且执行不会 returns 到 await 关键字之后的调用方方法。
真相在哪里?
这不是 async-await
的工作方式。
将方法标记为 async
不会创建任何后台线程。当您调用 async
方法时,它 运行 同步直到一个异步点,然后才 returns 到调用者。
那个异步点是你 await
一个任务还没有完成的时候。当它完成时,该方法的其余部分将被安排执行。此任务应代表实际的异步操作(如 I/O 或 Task.Delay
)。
在您的代码中没有异步点,没有返回调用线程的点。线程越来越深,并在 Thread.Sleep
上阻塞,直到这些方法完成并且 DoAsync
returns.
举个简单的例子:
public static void Main()
{
MainAsync().Wait();
}
public async Task MainAsync()
{
// calling thread
await Task.Delay(1000);
// different ThreadPool thread
}
这里我们有一个实际的异步点 (Task.Delay
) 调用线程 returns 到 Main
然后在任务上同步阻塞。一秒钟后,Task.Delay
任务完成,方法的其余部分在不同的 ThreadPool
线程上执行。
如果我们使用 Thread.Sleep
而不是 Task.Delay
那么它将全部 运行 在同一个调用线程上。
实际在后台线程上运行的行是
Task.Run( () => { } );
在您的示例中,您不是在等待该任务,而是在等待 TaskCompletionSource
public static Task<int> LongIOAsync()
{
var tcs = new TaskCompletionSource<Int32>();
Task.Run ( () => BeginLongIO(() =>
{
try { tcs.TrySetResult(EndLongIO()); }
catch (Exception exc) { tcs.TrySetException(exc); }
}));
return tcs.Task;
}
当等待 LongIOAsync 时,您正在等待来自 tcs 的任务 是从给予 Task.Run()
的委托中的后台线程设置的
应用此更改:
public static Task<Int32> LongIOAsync()
{
Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
var tcs = new TaskCompletionSource<Int32>();
Task.Run ( () => BeginLongIO(() =>
{
try { tcs.TrySetResult(EndLongIO()); }
catch (Exception exc) { tcs.TrySetException(exc); }
}));
Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
return tcs.Task;
}
或者在这种情况下,您可以等待 Task.Run() 的返回,
TaskCompletionSource 适用于您希望传递将任务设置为完成或其他方式的能力的情况。
简短的回答是 LongIOAsync() 正在阻塞。如果您 运行 在 GUI 程序中这样做,您实际上会看到 GUI 短暂冻结 - 这不是 async/await 应该工作的方式。因此整个事情都分崩离析了。
您需要将所有长 运行ning 操作包装在 Task 中,然后直接等待该 Task。在此期间不应阻止任何内容。
要真正理解这种行为,您需要首先理解 Task
是什么以及 async
和 await
对您的代码的实际作用。
Task
是 "an activity" 的 CLR 表示。它可能是在工作池线程上执行的方法。它可以是通过网络从数据库中检索某些数据的操作。它的通用性允许它封装许多不同的实现,但从根本上你需要理解它只是意味着 "an activity".
Task
class 为您提供了检查 activity 状态的方法:是否已完成、是否尚未开始、是否产生错误等. activity 的这种建模使我们能够更轻松地编写作为一系列活动构建的程序,而不是一系列方法调用。
考虑这个简单的代码:
public void FooBar()
{
Foo();
Bar();
}
这意味着“执行方法 Foo
,然后执行方法 Bar
。如果我们考虑 returns Task
来自 Foo
的实现并且Bar
,这些调用的组成是不同的:
public void FooBar()
{
Foo().Wait();
Bar().Wait();
}
现在的意思是 "Start a task using the method Foo
and wait for it to finish, then start a task using the method Bar
and wait for it to finish." 在 Task
上调用 Wait()
很少是正确的 - 它会导致当前线程阻塞,直到 Task
完成并可能导致死锁在一些常用的线程模型下 - 所以我们可以使用 async
和 await
来实现类似的效果而无需这个危险的调用。
public async Task FooBar()
{
await Foo();
await Bar();
}
async
关键字导致你的方法的执行被分解成块:每次你写await
,它都会接受以下代码并生成一个"continuation":一个方法在等待的任务完成后作为 Task
执行。
这与 Wait()
不同,因为 Task
没有链接到任何特定的执行模型。如果从 Foo()
返回的 Task
表示通过网络调用,则没有线程阻塞,等待结果 - 有一个 Task
等待操作完成。当操作完成时,Task
被安排执行 - 这个调度过程允许在 activity 的定义和执行它的方法之间进行分离,并且是任务使用的权力.
所以,方法可以概括为:
- 开始任务
Foo()
- 当该任务完成时开始任务
Bar
- 当该任务完成时表示方法任务已完成
在您的控制台应用程序中,您没有等待任何代表未决 IO 操作的 Task
,这就是为什么您看到线程被阻塞的原因 - 永远没有机会设置将执行的延续异步地。
我们可以修复您的 LongIOAsync 方法,以使用 Task.Delay()
方法以异步方式模拟您的长 IO。此方法 returns a Task
在指定时间段后完成。这为我们提供了异步延续的机会。
public static async Task<Int32> LongIOAsync()
{
Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1000);
Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
}
我构建了一些 async/await 演示控制台应用程序并得到了奇怪的结果。代码:
class Program
{
public static void BeginLongIO(Action act)
{
Console.WriteLine("In BeginLongIO start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
act();
Console.WriteLine("In BeginLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
}
public static Int32 EndLongIO()
{
Console.WriteLine("In EndLongIO start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(500);
Console.WriteLine("In EndLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
return 42;
}
public static Task<Int32> LongIOAsync()
{
Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
var tcs = new TaskCompletionSource<Int32>();
BeginLongIO(() =>
{
try { tcs.TrySetResult(EndLongIO()); }
catch (Exception exc) { tcs.TrySetException(exc); }
});
Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
return tcs.Task;
}
public async static Task<Int32> DoAsync()
{
Console.WriteLine("In DoAsync start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
var res = await LongIOAsync();
Thread.Sleep(1000);
Console.WriteLine("In DoAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
return res;
}
static void Main(String[] args)
{
ticks = DateTime.Now.Ticks;
Console.WriteLine("In Main start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
DoAsync();
Console.WriteLine("In Main exec... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Console.WriteLine("In Main end... \t\t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
}
private static Int64 ticks;
}
结果如下:
可能我没有完全理解await到底是什么。我认为如果执行等待,那么执行 returns 到调用方方法和等待在另一个线程中运行的任务。在我的示例中,所有操作都在一个线程中执行,并且执行不会 returns 到 await 关键字之后的调用方方法。 真相在哪里?
这不是 async-await
的工作方式。
将方法标记为 async
不会创建任何后台线程。当您调用 async
方法时,它 运行 同步直到一个异步点,然后才 returns 到调用者。
那个异步点是你 await
一个任务还没有完成的时候。当它完成时,该方法的其余部分将被安排执行。此任务应代表实际的异步操作(如 I/O 或 Task.Delay
)。
在您的代码中没有异步点,没有返回调用线程的点。线程越来越深,并在 Thread.Sleep
上阻塞,直到这些方法完成并且 DoAsync
returns.
举个简单的例子:
public static void Main()
{
MainAsync().Wait();
}
public async Task MainAsync()
{
// calling thread
await Task.Delay(1000);
// different ThreadPool thread
}
这里我们有一个实际的异步点 (Task.Delay
) 调用线程 returns 到 Main
然后在任务上同步阻塞。一秒钟后,Task.Delay
任务完成,方法的其余部分在不同的 ThreadPool
线程上执行。
如果我们使用 Thread.Sleep
而不是 Task.Delay
那么它将全部 运行 在同一个调用线程上。
实际在后台线程上运行的行是
Task.Run( () => { } );
在您的示例中,您不是在等待该任务,而是在等待 TaskCompletionSource
public static Task<int> LongIOAsync()
{
var tcs = new TaskCompletionSource<Int32>();
Task.Run ( () => BeginLongIO(() =>
{
try { tcs.TrySetResult(EndLongIO()); }
catch (Exception exc) { tcs.TrySetException(exc); }
}));
return tcs.Task;
}
当等待 LongIOAsync 时,您正在等待来自 tcs 的任务 是从给予 Task.Run()
的委托中的后台线程设置的应用此更改:
public static Task<Int32> LongIOAsync()
{
Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
var tcs = new TaskCompletionSource<Int32>();
Task.Run ( () => BeginLongIO(() =>
{
try { tcs.TrySetResult(EndLongIO()); }
catch (Exception exc) { tcs.TrySetException(exc); }
}));
Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
return tcs.Task;
}
或者在这种情况下,您可以等待 Task.Run() 的返回, TaskCompletionSource 适用于您希望传递将任务设置为完成或其他方式的能力的情况。
简短的回答是 LongIOAsync() 正在阻塞。如果您 运行 在 GUI 程序中这样做,您实际上会看到 GUI 短暂冻结 - 这不是 async/await 应该工作的方式。因此整个事情都分崩离析了。
您需要将所有长 运行ning 操作包装在 Task 中,然后直接等待该 Task。在此期间不应阻止任何内容。
要真正理解这种行为,您需要首先理解 Task
是什么以及 async
和 await
对您的代码的实际作用。
Task
是 "an activity" 的 CLR 表示。它可能是在工作池线程上执行的方法。它可以是通过网络从数据库中检索某些数据的操作。它的通用性允许它封装许多不同的实现,但从根本上你需要理解它只是意味着 "an activity".
Task
class 为您提供了检查 activity 状态的方法:是否已完成、是否尚未开始、是否产生错误等. activity 的这种建模使我们能够更轻松地编写作为一系列活动构建的程序,而不是一系列方法调用。
考虑这个简单的代码:
public void FooBar()
{
Foo();
Bar();
}
这意味着“执行方法 Foo
,然后执行方法 Bar
。如果我们考虑 returns Task
来自 Foo
的实现并且Bar
,这些调用的组成是不同的:
public void FooBar()
{
Foo().Wait();
Bar().Wait();
}
现在的意思是 "Start a task using the method Foo
and wait for it to finish, then start a task using the method Bar
and wait for it to finish." 在 Task
上调用 Wait()
很少是正确的 - 它会导致当前线程阻塞,直到 Task
完成并可能导致死锁在一些常用的线程模型下 - 所以我们可以使用 async
和 await
来实现类似的效果而无需这个危险的调用。
public async Task FooBar()
{
await Foo();
await Bar();
}
async
关键字导致你的方法的执行被分解成块:每次你写await
,它都会接受以下代码并生成一个"continuation":一个方法在等待的任务完成后作为 Task
执行。
这与 Wait()
不同,因为 Task
没有链接到任何特定的执行模型。如果从 Foo()
返回的 Task
表示通过网络调用,则没有线程阻塞,等待结果 - 有一个 Task
等待操作完成。当操作完成时,Task
被安排执行 - 这个调度过程允许在 activity 的定义和执行它的方法之间进行分离,并且是任务使用的权力.
所以,方法可以概括为:
- 开始任务
Foo()
- 当该任务完成时开始任务
Bar
- 当该任务完成时表示方法任务已完成
在您的控制台应用程序中,您没有等待任何代表未决 IO 操作的 Task
,这就是为什么您看到线程被阻塞的原因 - 永远没有机会设置将执行的延续异步地。
我们可以修复您的 LongIOAsync 方法,以使用 Task.Delay()
方法以异步方式模拟您的长 IO。此方法 returns a Task
在指定时间段后完成。这为我们提供了异步延续的机会。
public static async Task<Int32> LongIOAsync()
{
Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1000);
Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
}