调用Task里面带Wait关键词的方法,都是同步阻塞当前线程,等待异步任务完成后,主线程才继续往下走
调用Task带有when关键词的方法,内部会创建一个Task,返回Task,等待会在新创建这个Task里面异步等待,不会阻塞主线程(调用线程)往下继续执行
带有async await关键词的方法,遇到await时候,编译器会打包await出,该线程的上下文,内部机制把该上下文附加到await后面的异步任务中,通过任务调度器,传入到线程池等待空闲线程来执行任务,执行完毕后,再排队到任务调度中,找到空闲线程,恢复当时附带的上下文,继续执行await后续的代码
1 .NET 异步解说 - 知乎 (zhihu.com)
说了这么久还是没有解释 Task
到底是个什么东西,从上面的分析就可以得出,Task
其实就是一个所谓的调度单位,每个异步任务被封装为一个 Task
在 CLR 中被调度,而 Task
本身会运行在 CLR 中的预先分配好的线程池中。
总有很多人因为 Task
借助线程池执行而把 Task
归结为多线程模型,这是完全错误的。
这个时候有人跳出来了,说:你看下面这个代码
static async Task Main() { while (true) { Console.WriteLine(Environment.CurrentManagedThreadId); await Task.Delay(1000); } }
输出的线程 ID 不一样欸,你骗人,这明明就是多线程!对于这种言论,我也只能说这些人从原理上理解的就是错误的。
当代码执行到 await
的时候,此时当前的控制权就已经被让出了,当前线程并没有在阻塞地等待延时结束;待 Task.Delay()
完毕后,CLR 从线程池当中挑起了一个先前分配好的已有的但是空闲的线程,将让出控制权前的上下文信息恢复,使得该线程恰好可以从先前让出的位置继续执行下去。这个时候,可能挑到了先前让出前所在的那个线程,导致前后线程 ID 一致;也有可能挑到了另外一个和之前不一样的线程执行下面的代码,使得前后的线程 ID 不一致。在此过程中并没有任何的新线程被分配了出去。
2 [C#.NET 拾遗补漏]15:异步编程基础 - 知乎 (zhihu.com)
重要的是要把 Task 理解为发起异步工作的抽象,而不是对线程的抽象。
Task 提供了一个 API 协议,用于监视、等待和访问任务的结果值。比如,通过await
关键字等待任务执行完成,为使用 Task 提供了更高层次的抽象。
使用 await 允许你在任务运行期间执行其它有用的工作,将控制权交给其调用者,直到任务完成。你不再需要依赖回调或事件来在任务完成后继续执行后续工作。
使用 await 关键字告诉当前上下文赶紧生成快照并交出控制权,异步任务执行完成后会带着返回值去线程池排队等待可用线程,等到可用线程后,恢复上下文,线程继续执行后续代码。
3 抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext - 码农教程 (manongjc.com)
. await/async语法糖工作机制
微软提出了Task线程包装类和 await/async简化了异步编程的方式:
第②步:调用异步方法GetStringAsync时,开启异步任务;
第⑥步:遇到await关键字,框架会捕获调用线程的同步上下文(SynchronizationContext)对象,附加给异步任务;同时,控制权上交到上层调用函数;
第⑦步:异步任务完成,通过IO完成端口通知上层线程,
第⑧步:通过捕获的线程同步上下文执行后继代码块;
4 .net 中的async,await理解 - 酒香逢 - 博客园 (cnblogs.com)
理解:
1、async修饰的方法可理解为异步方法(必须要配合await,否则和普通方法无异)
2、当async方法执行遇到await,则立即将控制权转移到async方法的调用者
3、由调用者决定是否需要等待async方法执行完再继续往下执行
4、await会挂起当前方法,即阻塞当前方法继续往下执行,转交控制权给调用者
注意:如果调用一个async方 法,却不使用await关键字来标记一个挂起点的话,程序将会忽略async关键字并以同步的方式执行。编译器会对类似的问题发出警告。
5 使用 Async和 Await 的任务异步编程 (TAP) 模型 (C#) | Microsoft Docs
async 和 await
如果使用 async 修饰符将某种方法指定为异步方法,即启用以下两种功能。
-
标记的异步方法可以使用 await 来指定暂停点。
await
运算符通知编译器异步方法:在等待的异步过程完成后才能继续通过该点。 同时,控制返回至异步方法的调用方。异步方法在
await
表达式执行时暂停并不构成方法退出,只会导致finally
代码块不运行。 -
标记的异步方法本身可以通过调用它的方法等待。
异步方法通常包含 await
运算符的一个或多个实例,但缺少 await
表达式也不会导致生成编译器错误。 如果异步方法未使用 await
运算符标记暂停点,则该方法会作为同步方法执行,即使有 async
修饰符,也不例外。 编译器将为此类方法发布一个警告。
6 异步返回类型 (C#) | Microsoft Docs
Task 返回类型
不包含 return
语句的异步方法或包含不返回操作数的 return
语句的异步方法通常具有返回类型 Task。 如果此类方法同步运行,它们将返回 void
。 如果在异步方法中使用 Task 返回类型,调用方法可以使用 await
运算符暂停调用方的完成,直至被调用的异步方法结束。
下例中的 WaitAndApologizeAsync
方法不包含 return
语句,因此该方法会返回 Task 对象。 返回 Task
可等待 WaitAndApologizeAsync
。 Task 类型不包含 Result
属性,因为它不具有任何返回值。
public static async Task DisplayCurrentInfoAsync()//如果把改行的关键词async去掉,public static Task DispayCurrentInfoAsync(),没有return语句,编译器会报错 { await WaitAndApologizeAsync(); Console.WriteLine($"Today is {DateTime.Now:D}"); Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}"); Console.WriteLine("The current temperature is 76 degrees."); }//返回的是一个结果void,而不是一个task待执行任务 static async Task WaitAndApologizeAsync() { await Task.Delay(2000); Console.WriteLine("Sorry for the delay...\n"); } // Example output: // Sorry for the delay... // // Today is Monday, August 17, 2020 // The current time is 12:59:24.2183304 // The current temperature is 76 degrees.
static async Task<int> GetLeisureHoursAsync()
{
Console.WriteLine($"GetLeisureHoursAsync b=>{Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
DayOfWeek today = await Task.FromResult(DateTime.Now.DayOfWeek);
int leisureHours =
today is DayOfWeek.Saturday || today is DayOfWeek.Sunday
? 16 : 5;
Console.WriteLine($"GetLeisureHoursAsync e=>{Thread.CurrentThread.ManagedThreadId}");
return leisureHours;
}//返回的是一个Task<T>结果,T =int
static Task MyTest()
{
Console.WriteLine($"MyTest b=>{Thread.CurrentThread.ManagedThreadId}");
return new Task( () =>
{
Console.WriteLine($"myb=>{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"mye=>{Thread.CurrentThread.ManagedThreadId}");
});
}//返回的是一个新的Task任务,该任务还没执行,需要调用执行
//异步方法(带async关键词),返回的结果只有Task 也就是没有返回值,只想跟踪状态的,还有一个就是返回Task<TResult>,
//如果函数声明没有带async关键词,只返回了Task,那么返回的是一个待执行的Task任务,而不是一个结果,比如上面的方法MyTest(),返回的是一个待执行的任务。比如 public static Task Test(){ ... }
通过使用 await 语句而不是 await 表达式等待 WaitAndApologizeAsync
,类似于返回 void 的同步方法的调用语句。 Await 运算符的应用程序在这种情况下不生成值。 当 await
的右操作数是 Task<TResult> 时,await
表达式生成的结果为 T
。 当 await
的右操作数是 Task 时,await
及其操作数是一个语句。
可从 await 运算符的应用程序中分离对 WaitAndApologizeAsync
的调用,如以下代码所示。 但是,请记住,Task
没有 Result
属性,并且当 await 运算符应用于 Task
时不产生值。
7 异步编程系列第05章 Await究竟做了什么? - 坦荡 - 博客园 (cnblogs.com)
await究竟做了什么?
我们有两种角度来看待C#5.0的async功能特性,尤其是await关键字上发生了什么:
·作为一个语言的功能特性,他是一个供你学习的已经定义好的行为
·作为一个在编译时的转换,这是一个C#语法糖,为了简略之前复杂的异步代码
这都是真的;它们就像同一枚硬币的两面。在本章,我们将会集中在第一点上来探讨异步。在第十四章我们将会从另一个角度来探讨,即更复杂的,但是提供了一些细节使debug和性能考虑更加清晰。
当你的程序执行遇到await关键字时,我们想要发生两件事:
·为了使你的代码异步,当前执行你代码的线程应该被释放。这意味着,在普通,同步的角度来看,你的方法应该返回。
·当你await的Task完成时,你的方法应该从之前的位置继续,就像它没在早些时候被返回。
为了做到这个行为,你的方法必须在遇到await时暂停,然后在将来的某个时刻恢复执行。
我把这个过程当做一个休眠一台计算机的小规模情况来看(S4 sleep)。这个方法当前的状态会被存储起来(译者:状态存储起来,正如我们第二章厨房那个例子,厨师会把已放在烤箱中的食物的烹饪状态以标签的形式贴在上面),并且这个方法完全退出(厨师走了,可能去做其他事情了)。当一台计算机休眠,计算机的动态数据和运行数据被保存到磁盘,并且变得完全关闭。下面这段话和计算机休眠大概一个道理,一个正在await的方法除了用一点内存,不使用其他资源,那么可以看作这个正执行的线程已经被释放。
进一步采取类似上一段的类比:一个阻塞型方法更像你暂停一台计算机(S3 sleep),它虽然使用较少的资源,但从根本上来讲它一直在运行着。
在理想的情况下,我们希望编程者察觉不到这里的休眠。尽管实际上休眠和唤醒一个方法的中期执行是很复杂的,C#也将会确保你的代码被唤醒,就像什么都没发生一样。(译者:不得不赞叹微软对语法糖的封装和处理)。
为了准确的弄清楚在你使用await时C#到底为我们做了多少事情,我想列出所有关于方法状态的所有我们记住和了解的细节。
首先,你方法中本地的变量的值会被记住,包括以下值:
·你方法的参数
·在本范围内所有你定义的变量
·其他变量包括循环数
·如果你的方法非静态,那么包括this变量。这样,你类的成员变量在方法唤醒时都是可用的。
他们都被存在.NET 垃圾回收堆(GC堆)的一个对象上。因此当你使用await时,一个消耗一些资源的对象将会被分配,但是在大多数情况下不用担心性能问题。
C#也会记住在方法的什么位置会执行到await。这可以使用数字存储起来,用来表示await关键字在当前方法的位置。
在关于如何使用await关键字没有什么特别的限制,例如,他们可以被用在一个长表达式上,可能包含不止一个await:
int myNum = await AlexsMethodAsync(await myTask, await StuffAsync());
为了去记住剩余部分的表达式的状态在await某些东西时,增加了额外的条件。比如,当我们运行await StuffAsync()时,await myTask的结果需要被记住。.NET中间语言(IL)在栈上存储这种子类表达式,因此 ,这个栈就是我们await关键字需要存储的。
最重要的是,当程序执行到第一个await关键字时,方法便返回了(译者:关于方法在遇到await时返回,建议读者从第一章拆分的两个方法来理解)。如果它不是一个async void方法,一个Task在这个时刻被返回,因此调用者可以等待我们以某种方式完成。C#也必须存储一种操作返回的Task的方式,这样当你的方法完成,这个Task也变得completed,并且执行者也可以返回到方法的异步链当中。确切的机制将会在第十四章中介绍。
作为一个使await的过程尽量透明的部分,C#捕捉各种上下文在遇到await时,然后在恢复方法使将其恢复。
在所有事情中最重要的还是同步上下文(synchronization context),即可以被用于恢复方法在一个特殊类型的线程上。这对于UI app尤其重要,就是那种只能在正确的线程上操作UI的(就是winform wpf之类的)。同步上下文是一个复杂的话题,第八章将会详细解释。
其他类型的上下文也会被从当前调用的线程捕捉。他们的控制是通过一个相同名称的类来实现的,所以我将列出一些重要的上下文类型:
ExecutionContext
这是父级上下文,所有其他上下文都是它的一部分。这是.NET的系统功能,如Task使用其捕捉和传播上下文,但是它本身不包含什么行为。
SecurityContext
这是我们发现并找到通常被限制在当前线程的安全信息的地方。如果你的代码需要运行在特定的用户,你也许会,模拟或者扮演这个用户,或者ASP.NET将会帮你实现扮演。在这种情况下,模拟信息会存在SecurityContext。
CallContext(这个东西耳熟能详吧,相信用过EF的都知道)
这允许编程者存储他们在逻辑线程的生命周期中一直可用的数据。即使考虑到在很多情况下有不好的表现,它仍然可以避免程序中方法的参数传来传去。(译者:因为你存到callcontext里,随时都可以获取呀,不用通过传参数传来传去了)。LogicalCallContextis是一个相关的可以跨用应用程序域的。
值得注意的是线程本地存储(TLS),它和CallContext的目标相似,但它在异步的情况下是不工作的,因为在一个耗时操作中,线程被释放掉了,并且可能被用于处理其他事情了。你的方法也许被唤醒并执行在一个不同的线程上。
C#将会在你方法恢复(resume,这里就是单纯的“恢复”)的时候恢复(restore,我觉得这里指从内存中恢复)这些类型的上下文。恢复上下文将产生一些开销,比如,一个程序在使用模拟(之前的模拟身份之类的)的时候并大量使用async将会变得更慢一些。我建议必变.NET创建上下文的功能,除非你认为这真的有必要。