博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

.NET 异步 async await Task的一些摘抄记录

Posted on 2022-04-19 22:37  qianyz  阅读(377)  评论(0编辑  收藏  举报

调用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 修饰符,也不例外。 编译器将为此类方法发布一个警告。


异步返回类型 (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创建上下文的功能,除非你认为这真的有必要。