async/await 本身是个语法糖
看我这篇文章: https://blog.csdn.net/weixin_46879188/article/details/120849575
C#的另一个语法糖:用yield实现IEnumerable接口也是采用这种技术。
使用async/await和.Result和.Wait()和.GetAwaiter().GetResult()的区别
用法:
await搭配 (返回Task/Task<T>)使用。
.Wait()和GetAwaiter().GetResult() 搭配(返回Task/Task<T>)使用。
.Result只能搭配 (返回Task<T>)使用。
功能:
await是阻塞当前方法继续调用,但不阻塞当前线程;
.Result和.Wait()和.GetAwaiter().GetResult() 会在阻塞当前线程同时仍然会启动另外一个后台线程去执行这个函数。
在非常特殊的情况下,如果非得阻塞当前线程去执行这个 Task,可以考虑使用 .GetAwaiter().GetResult()。两者效果基本一样,阻塞当前线程同时会启动另外一个后台线程。但是如果使用 .Result 或 .Wait() 发生错误时会抛出 AggregateExecption,如果用Exception去也是可以捕捉到异常的 。而使用 .GetAwaiter().GetResult()则是会抛出实际的 Excepiton ,Excepiton 中的堆栈也符合我们所期望的。
主要是区分两个概念:
1.阻塞当前方法调用;
2.阻塞当前线程;
Async
Async 方法有三种可能的返回类型: Task、Task<T> 和 void。 当从同步转换为异步代码时,任何返回类型 T 的方法都会成为返回 Task<T> 的 async 方法,任何返回 void 的方法都会成为返回 Task 的 async 方法。返回 void 的 async 方法具有特定用途: 用于支持异步事件处理程序,async, await 底层是状态机, 而如果返回值是void的话,调度方是不会有等待行为的,因为没有awaiter(可以看看下面demo的static async void Test()方法)
如下Demo:
static void Main(string[] args)
{
Console.WriteLine("①我是主线程,线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
//同步方法调用异步方法,遇到异步方法内部的第一个await之后,不会等待await之后的代码执行完毕,会马上返回到Main()方法中,执行var testResult = TestAsync();之后的代码
var testResult = TestAsync();
//GetAwaiter().OnCompleted() 方法允许你将一个回调函数绑定到一个异步任务的完成事件上。当异步任务完成时,这个回调函数会被调用。这种方法的好处是它不会阻塞当前线程,因为回调函数会在另一个线程中执行。
testResult.GetAwaiter().OnCompleted(() => {
var id = testResult.Id;
Console.WriteLine("My name is: " + id);
});
Console.WriteLine("主线程执行完毕");
Console.ReadKey();
}
static async Task TestAsync()
{
Console.WriteLine("②调用GetReturnResult()之前,线程ID:{0}。当前时间:{1}", Thread.CurrentThread.ManagedThreadId,
DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
var name = GetReturnResult();
Console.WriteLine("④调用GetReturnResult()之后,线程ID:{0}。当前时间:{1}", Thread.CurrentThread.ManagedThreadId,
DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
//遇到await name后, 因为没有使用await方法,不会等待之后的代码执行完毕,会马上返回到Main()方法中,执行var testResult = TestAsync(); 之后的代码
Console.WriteLine("⑥得到GetReturnResult()方法的结果一:{0}。当前时间:{1}", await name,
DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
}
static async Task<string> GetReturnResult()
{
Console.WriteLine("③执行Task.Run之前, 线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
//遇到await Task.Run后, 因为没有使用await方法,不会等待之后的代码执行完毕,会马上返回到TestAsync()方法中,执行 var name = GetReturnResult(); 之后的代码
return await Task.Run(() =>
{
Thread.Sleep(5000);
Console.WriteLine("⑤GetReturnResult()方法里面线程ID: {0}", Thread.CurrentThread.ManagedThreadId);
return "我是返回值";
});
}
输出结果如下:
Execution Context 执行上下文
它在多线程的好比空气:你可以不知道它,但它非常重要。ExecutionContext是为了解决线程本地存储在多线程中无法传递的问题:总得有一种机制能够传递全局信息。否则只能通过函数调用参数传递了。
当一个线程发起异步调用的时候,ExecutionContext会自动的在线程之间传递以下信息:
线程安全设置
Host设置(与web服务有关)
Logical Call Context, 可以在其中保存和传递对象。
线程的Culture(从.NET 4.6以后)。
Synchronization Context 同步上下文
它是为了描述异步调用返回时的行为所创建的抽象。它有两个基本接口方法:
Send 同步地等待任务执行完毕。
Post 把任务发出去就不管了。
那么异步调用返回时的行为是什么意思?既然是抽象,那就会有具体的实现。后面我们会看到几种实现。
当开始异步调用时,C#会捕获(capture)当前线程的同步上下文,并保存到Task中。在异步调用返回时,需要恢复(resume)同步上下文。此时就会调用同步上下文的Send或者Post。
下面是几种典型的同步上下文实现:
1.UI同步上下文。由于UI界面操作必须在UI线程中进行,因此这个上下文做的事情就是把需要恢复的工作Marshal起来交给UI线程。(可能有人会好奇如何交给UI线程去做。简单来说, UI线程有个Windows消息循环,同步上下文将任务封装在一个特定消息中,UI线程得到这个消息后,就去执行其中的任务)。
2.
ASP.NET同步上下文。它有以下特点:
3.
①不会切换线程,因为后台线程没什么区别。
②会把线程的Principle和Culture传递过去。(因为ASP.NET依赖于此)
③在异步页面中记录尚未完成的IO数量。
4.默认同步上下文。就是线程池的调度器,基本上没有特别的操作。
最后,调用ConfigureAwait(false)时,就会跳过恢复同步上下文这一过程。所以,有时候必要(当没必要传递任何信息时,使用它可以提高效率),有时候又会出错。例如,UI程序的异步调用本来没问题,你加了这个语句,反而会造成修改界面的操作可能不在UI线程中执行,从而出错。但是注意,无论如何,执行上下文都是会传递的。
结合以上,第一段程序的更精确的编译后版本是这样的:
private void btnDoStuff_Click(int step)
{
switch (step)
{
case 0:
lblStatus.Content = "Doing Stuff";
Task t = Task.Delay(4000);
t.ContinueWith(
task => SynchronizationContext.Current.Post(
state => btnDoStuff_Click(task.Step),
task)
);
break;
case 1:
lblStatus.Content = "After await";
break;
}
}
到底是谁在执行异步调用?
这个问题曾经困扰我很久。如果我当前的线程调用一个异步调用后返回了,那到底是谁在完成真正调用的工作呢?答案是一个(或几个)共享的线程:线程池中的IO线程。
如下是一段代码:
async void GetButton_OnClick(object o, EventArgs e)
{
Task<Image> task = GetFaviconAsync(_url);
Image image = await task;
AddAFavicon(image);
}
async Task<Image> GetFaviconAsync(string url)
{
var task = _webClient.DownloadDataTaskAsync(url);
byte[] bytes = await task;
return MakeImage(bytes);
}
线程的执行情况如下图:
大部分的时间都在用户线程中。只有调用到非常底层,IO完成之后,才有IO线程被唤醒(见11),然后它调用Task的同步上下文的Post,将剩下的任务再交给用户线程去执行(Winform是委托UI线程。ASP.NET是可能是发起请求的线程,但更有可能是操作完成时空闲的任何线程。)。
下面是一个动态的解释: