我最近正在阅读有关async / await的内容,而且我对以下事实感到困惑:我正在阅读的许多文章/帖子表明在使用async await(§8 Encoding of Pixel, Overlay and Waveform Data)时未创建新线程。
我创建了一个简单的控制台应用来测试它
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
MainAsync(args).Wait();
Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
static async Task MainAsync(string[] args)
{
Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);
await thisIsAsync();
}
private static async Task thisIsAsync()
{
Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1);
Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);
}
}
以下代码的输出是:
Main: 8
Main Async: 8
thisIsAsyncStart: 8
thisIsAsyncEnd: 9
Main End: 8
我是否遗漏了这一点,或者这个AsyncEnd的线程ID与其他动作不同?
修改
我已根据以下答案中的建议更新了代码await Task.Delay(1)
,但我仍然看到相同的结果。
从以下答案中引用:
Rather, it enables the method to be split into multiple pieces, some of which may run asynchronously
如果没有创建其他线程,我想知道asynchronously
部分在哪里运行?
如果它在同一个线程上运行,不应该由于长I / O请求而阻塞它,或者编译器是否足够聪明,如果它花费太长时间将该操作移动到另一个线程,并且毕竟使用了新线程? / p>
答案 0 :(得分:29)
我建议您阅读我的async
intro帖子,了解async
和await
个关键字。特别是,await
(默认情况下)将捕获“上下文”并使用该上下文来恢复其异步方法。此“上下文”是当前SynchronizationContext
(或TaskScheduler
,如果没有SynchronzationContext
)。
如果没有创建其他线程,我想知道异步部分在哪里运行?如果它在同一个线程上运行,不应该由于长I / O请求而阻塞它,或者编译器是否足够聪明,如果它花费太长时间将该操作移动到另一个线程,并且毕竟使用了新线程? / p>
我在博客上解释truly asynchronous operations do not "run" anywhere。在这种特殊情况下(Task.Delay(1)
),异步操作基于计时器,不在某处执行Thread.Sleep
时阻塞的线程。大多数I / O都以相同的方式完成。例如,HttpClient.GetAsync
基于重叠(异步)I / O,不在某处等待HTTP下载完成的线程被阻塞。
一旦了解await
如何使用其上下文,就可以更轻松地浏览原始代码:
static void Main(string[] args)
{
Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
MainAsync(args).Wait(); // Note: This is the same as "var task = MainAsync(args); task.Wait();"
Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
static async Task MainAsync(string[] args)
{
Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);
await thisIsAsync(); // Note: This is the same as "var task = thisIsAsync(); await task;"
}
private static async Task thisIsAsync()
{
Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1); // Note: This is the same as "var task = Task.Delay(1); await task;"
Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);
}
Main
并调用MainAsync
。MainAsync
并调用thisIsAsync
。thisIsAsync
并调用Task.Delay
。Task.Delay
做它的事情 - 启动一个计时器和诸如此类的东西 - 并返回一个不完整的任务(注意Task.Delay(0)
将返回一个完成的任务,这会改变行为)。thisIsAsync
并等待Task.Delay
返回的任务。由于任务不完整,因此会从thisIsAsync
返回不完整的任务。MainAsync
并等待thisIsAsync
返回的任务。由于任务不完整,因此会从MainAsync
返回不完整的任务。Main
并对Wait
返回的任务调用MainAsync
。这将阻止主线程,直到MainAsync
完成。Task.Delay
设置的计时器关闭时,thisIsAsync
将继续执行。由于SynchronizationContext
没有捕获TaskScheduler
或await
,因此它将继续在线程池线程上执行。thisIsAsync
的末尾,完成其任务。MainAsync
继续执行。由于没有await
捕获的上下文,它继续在线程池线程上执行(实际上是运行thisIsAsync
的相同线程)。MainAsync
的末尾,完成其任务。Wait
调用返回并继续执行Main
方法。用于继续thisIsAsync
和MainAsync
的线程池线程不再需要并返回到线程池。这里重要的一点是线程池使用,因为没有上下文。它不是“必要时”自动使用的。如果您要在GUI应用程序中运行相同的MainAsync
/ thisIsAsync
代码,那么您将看到非常不同的线程使用情况:UI线程有一个SynchronizationContext
,它将连续调度重新安排回UI线程,所以所有方法都将在同一个UI线程上恢复。
答案 1 :(得分:1)
使用async
制作方法并不意味着它会创建另一个线程。如果RunTime看到您的await
方法中使用async
调用的方法延迟,它退出该方法并在等待方法完成后等待,然后用另一个线程继续该方法。尝试将您的Task.Delay(2000)
更改为Task.Delay(0)
,您将看到它没有创建一个新的线程。
RunTime 会计算它,如果它需要创建它会创建,如果不是 - 不。我尝试你的例子0 ms并得到所有相同的线程:
Main: 1
Main Async: 1
thisIsAsyncStart: 1
thisIsAsyncEnd: 1
Main End: 1
Taken from Stephen Toub's blog:
“异步”关键字
“async”关键字在应用于方法时的作用是什么?
当您使用“async”关键字标记方法时,您真的在说 编译器有两件事:
- 您告诉编译器您希望能够在方法中使用“await”关键字(您可以使用await关键字if 并且只有当它所在的方法或lambda被标记为async时)。这样做 所以,你告诉编译器使用状态编译方法 机器,这样该方法将能够暂停然后恢复 在等待点异步。
- 您告诉编译器“解除”方法的结果或可能出现在返回类型中的任何异常。对于一种方法 返回任务或任务,这意味着任何返回的值或异常 在方法中未处理的内容存储在结果任务中。 对于返回void的方法,这意味着任何异常都是 通过任何方式传播到调用者的上下文 “SynchronizationContext”在方法时是最新的 初始调用。
醇>在方法上使用“async”关键字是否强制该方法的所有调用都是异步的?
没有。当您调用标记为“async”的方法时,它开始运行 同步在当前线程上。所以,如果你有同步 返回void的方法以及所做的更改方法都将其标记为 “async”,该方法的调用仍将同步运行。这个 无论您是否将返回类型保留为“void”或,都是如此 将其更改为“任务”。同样,如果你有一个同步方法 返回一些TResult,你所做的只是将其标记为“异步”并进行更改 返回类型为“任务”,该方法的调用仍然是 同步运行。
将方法标记为“async”不会影响方法是否运行 同步或异步完成。相反,它使... 方法被分成多个部分,其中一些可能会运行 异步,这样该方法可以异步完成。该 这些部分的边界只能在您明确编码的地方出现 一个使用“await”关键字,所以如果“await”在a中根本不被使用 方法的代码,只有一件,因为那件就会 开始同步运行,它(以及它的整个方法)将 同步完成。
答案 2 :(得分:0)
我想知道完全一样。对我来说,MSDN的解释是矛盾的:
async和await关键字不会导致其他线程 创建。异步方法不需要多线程,因为异步 方法不能在自己的线程上运行。
MSDN: Asynchronous Programming with async and await
await表达式不会阻止正在执行的线程。 [..]任务完成后,调用继续执行,异步方法的执行从中断处继续执行。
我不明白如何在不使用额外线程的情况下阻止原始线程。另外,“调用”措辞表明是在某处以某种方式使用的多个线程。
然后我意识到,一切都写得正确,没有使用任何其他线程这些关键字。通过设计Task
类来提供可能使用不同线程的机制 - 或者不是。
在Task.Delay()
方法stephen-cleary beautifully explained这些机制的同时,我扩展了MSDN示例以了解await
与Task.Run()
的行为方式:
private async void ds_StartButton_Click(object sender, EventArgs e)
{
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started MSDN Example ..." + Environment.NewLine);
// Call the method that runs asynchronously.
string result = await WaitAsynchronouslyAsync();
// Call the method that runs synchronously.
//string result = await WaitSynchronously ();
// Do other Schdaff
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #1 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #2 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #3 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #4 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #5 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #6 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #7 ..." + Environment.NewLine);
// Display the result.
textBox1.Text += result;
}
// The following method runs asynchronously. The UI thread is not
// blocked during the delay. You can move or resize the Form1 window
// while Task.Delay is running.
public async Task<string> WaitAsynchronouslyAsync()
{
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Entered WaitAsynchronouslyAsync()");
await Task.Delay(10000);
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Task.Delay done, starting random string generation now ...");
await Task.Run(() => LongComputation());
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Leaving WaitAsynchronouslyAsync() ...");
return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Example." + Environment.NewLine;
}
// The following method runs synchronously, despite the use of async.
// You cannot move or resize the Form1 window while Thread.Sleep
// is running because the UI thread is blocked.
public async Task<string> WaitSynchronously()
{
// Add a using directive for System.Threading.
Thread.Sleep(10000);
return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Bad Ass Example." + Environment.NewLine;
}
private void ds_ButtonTest_Click(object sender, EventArgs e)
{
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started Test ..." + Environment.NewLine);
Task<string> l_Task = WaitAsynchronouslyAsync();
//WaitAsynchronouslyAsync();
//textBox1.AppendText(l_Result);
}
private void LongComputation()
{
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Generating random string ...");
string l_RandomString = GetRandomString(10000000);
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Random string generated.");
}
/// <summary>Get random string with specified length</summary>
/// <param name="p_Length">Requested length of random string</param>
/// <param name="p_NoDots">Use case of this is unknown, but assumed to be importantly needed somewhere. Defaults to true therefore.
/// But due to huge performance implication, added this parameter to switch this off.</param>
/// <returns>Random string</returns>
public static string GetRandomString(int p_Length, bool p_NoDots = true)
{
StringBuilder l_StringBuilder = new StringBuilder();
string l_RandomString = string.Empty;
while (l_StringBuilder.Length <= p_Length)
{
l_RandomString = (p_NoDots ? System.IO.Path.GetRandomFileName().Replace(".", string.Empty) : System.IO.Path.GetRandomFileName());
l_StringBuilder.Append(l_RandomString);
}
l_RandomString = l_StringBuilder.ToString(0, p_Length);
l_StringBuilder = null;
return l_RandomString;
}
从输出中可以看出, 使用了多个线程 - 不是async/await
,而是Task.Run()
:
04.11.2016 12:38:06 [10] Entered WaitAsynchronouslyAsync()
04.11.2016 12:38:17 [10] Task.Delay done, starting random string generation now ...
04.11.2016 12:38:17 [12] Generating random string ...
04.11.2016 12:38:21 [12] Random string generated.
04.11.2016 12:38:21 [10] Leaving WaitAsynchronouslyAsync() ...
这就像往常一样,但我个人需要这个明确的例子来了解正在发生的事情,并将async/await
完成的工作与Task
完成的工作分开。
答案 3 :(得分:0)
对你的问题的解释非常好 https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/
调用控制台应用程序的Main方法时, SynchronizationContext.Current将返回null。这意味着如果 您在控制台应用程序中调用异步方法,除非您这样做 特别的东西,你的异步方法不会有线程 affinity:这些异步方法中的延续可以 最终在“任何地方”运行。