C#深入理解异步编程async/await
async/await 本身是个语法糖
看我这篇文章: https://blog.csdn.net/weixin_46879188/article/details/120849575
C#的另一个语法糖:用yield实现IEnumerable接口也是采用这种技术。
使用async/await和.Result和.Wait()和.GetAwaiter().GetResult()的区别
用法:
await搭配 (返回Task/Task
.Wait()和GetAwaiter().GetResult() 搭配(返回Task/Task
.Result只能搭配 (返回Task
功能:
await是阻塞当前方法继续调用,但不阻塞当前线程;
.Result和.Wait()和.GetAwaiter().GetResult() 会在阻塞当前线程同时仍然会启动另外一个后台线程去执行这个函数。
在非常特殊的情况下,如果非得阻塞当前线程去执行这个 Task,可以考虑使用 .GetAwaiter().GetResult()。两者效果基本一样,阻塞当前线程同时会启动另外一个后台线程。但是如果使用 .Result 或 .Wait() 发生错误时会抛出 AggregateExecption,如果用Exception去也是可以捕捉到异常的 。而使用 .GetAwaiter().GetResult()则是会抛出实际的 Excepiton ,Excepiton 中的堆栈也符合我们所期望的。
主要是区分两个概念:
1.阻塞当前方法调用;
2.阻塞当前线程;
如下Demo:
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
Console.WriteLine("我是主线程,线程ID:" + Thread.CurrentThread.ManagedThreadId);
//task用法一
Task task1 = new Task(() => MyAction());
task1.Start();
//task用法二
var strRes = Task.Run<string>(() => { return GetReturnStr(); });
Console.WriteLine(strRes.Result);
Console.WriteLine("---------------------------------");
Console.WriteLine("①我是主线程,线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
var testResult = TestAsync();
testResult.GetAwaiter().OnCompleted(() => {
var id = testResult.Id;
Console.WriteLine("My name is: " + id);
});
var te = testResult.Result;
Test();
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"));
Console.WriteLine("⑥得到GetReturnResult()方法的结果一:{0}。当前时间:{1}", await name,
DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
Console.WriteLine("⑥得到GetReturnResult()方法的结果二:{0}。当前时间:{1}", name.GetAwaiter().GetResult(),
DateTime.Now.ToString("yyyy-MM-dd hh:MM:ss"));
}
static async Task<string> GetReturnResult()
{
Console.WriteLine("③执行Task.Run之前, 线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
return await Task.Run(() =>
{
Thread.Sleep(20000);
Console.WriteLine("⑤GetReturnResult()方法里面线程ID: {0}", Thread.CurrentThread.ManagedThreadId);
return "我是返回值";
});
}
static void MyAction()
{
Console.WriteLine("我是新进程,线程ID:() MyAction " + Thread.CurrentThread.ManagedThreadId);
}
static string GetReturnStr()
{
return "我是返回值 ,GetReturnStr 线程ID:" + Thread.CurrentThread.ManagedThreadId;
}
static async void Test()
{
Console.WriteLine("Test 线程ID:" + Thread.CurrentThread.ManagedThreadId);
await Task.Run(() => {
Thread.Sleep(5000);
Console.WriteLine("Test方法等待5秒开始执行");
});
Console.WriteLine("Test 线程ID:" + Thread.CurrentThread.ManagedThreadId);
}
输出结果如下:
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线程得到这个消息后,就去执行其中的任务)。
- ASP.NET同步上下文。它有以下特点:
- ①不会切换线程,因为后台线程没什么区别。
②会把线程的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,将剩下的任务再交给用户线程去执行。
下面是一个动态的解释:
本文转自 link,如有侵权,请联系删除。