概述
环境:
- window 10
- centos 8.2
- .netframework
- .netcore
参考:
《MSDN:异步编程模型 (APM)》
《MSDN:异步编程模式》
《MSDN:I/o 完成端口》
《Linux下的I/O复用与epoll详解》
《浅谈async、await关键字 => 深谈async、await关键字》
《重新认识 async/await 语法糖》
一、异步的概念
和同步相反,指在处理任务时可以不用等待任务结果而继续向后执行,这样就能减少等待耗费的时间,从而达到高效利用服务器资源的效果。
异步的目的:合理规划任务的执行顺序,避免不必要的任务等待,减少服务器资源消耗,从而在服务器性能以及用户的交互体验上达到满意的效果。
下图所示为同步和异步的区别:
多线程和异步的区别:
多线程是异步的主要实现方式,事实上几乎所有的异步都有多线程的影子。但异步是从任务执行效果的角度看的,而多线程是现代计算机的一个基础功能。
二、同步代码带来的问题
同步的问题根源:阻塞当前线程、占用CPU和内存资源。
在不同的场景中,同步所带来的影响如下:
异步就是要解决这些问题的,这里先看一下上面简单问题的处理方案:
-
多个代码块的串行:将代码块并行
public static void Main(string[] args) { //以给小明增加语文课程为例,任务1和任务2并行,最终和任务3串行 //任务1: 找到小明,Id=1 var task1 = Task.Run(() => { Thread.Sleep(2000); return 1; }); //任务2: 找到语文课程,Id=1 var task2 = Task.Run(() => { Thread.Sleep(2000); return 1; }); //任务3: 给小明增加语文课程 Task.WaitAll(task1, task2); var userId = task1.Result; var bookId = task2.Result; //插入到数据库(UserId=1,BookId=1) //End }
-
winform界面的按钮事件:耗时代码采用异步调用
private void button1_Click(object sender, EventArgs e) { button1.Text = "运算中..."; button1.Enabled = false; Task.Run(() => { var st = new Stopwatch(); st.Start(); //耗时运算或者是耗时I/O,只要耗时就行 for (var i = 0; i < 10000 * 10000; i++) { var obj = new { Id = i, Name = "Name" + i }; } st.Stop(); //运算完成 this.BeginInvoke(new MethodInvoker(() => { button1.Text = $"运算完成:{st.ElapsedMilliseconds}毫秒"; button1.Enabled = true; })); }); }
三、服务器高并发问题的解决方案 - epoll和IOCP
如果传统web服务器对网卡的读写采用同步调用的话,在高并发下很容易卡死,同样,如果我们程序中同步读写数据库,在高并发下也很容易卡死。
linux中的epoll(事件-响应)模型就是为了解决这种问题出现的。在window中叫做IOCP(I/O Completion Ports,也叫作完成端口)。
下面用一个场景类比epoll和IOCP的处理模型(epoll和IOCP在原理上是相同的)。
场景是这样的:
有一个小镇,里面的人们要去镇外采购食物(表示:客户端向服务器发起请求),需要经过一条不宽的河流(表示:服务器的CPU、内存等资源有限),河流的管理者负责组织船只运输往返的人们(表示:CPU在时间切片内调度线程,每一个船只表示一个线程)。
最初的时候,运作机制是这样的:每当一个人请求采购时,河流管理者就会派一个船只负责接送采购人并且在采购人进行采购时在岸边等待直到采购完成后将采购人送回。然后,船只才会接收下一次任务,调度图如下:
这在采购人数量少的时候没有问题,河流管理的井井有条,不过当有一天小镇的人都出来采购了,大概有1万人(高并发),河流管理者还是按照这种模式去运作,于是,当人们请求采购时,河流管理者还是每来一个人都安排专职船只去接送,由于船只将采购人送到对岸后不会立刻返回,造成大量的船只停靠在对岸不能接受新的任务,所以河流管理者不得不派新的船只下水,这样一来,河流中立刻积累了上万条船只。而这就让本就不宽的河流拥堵不堪,河流根本无法通行,最终交通瘫痪(服务器崩溃),人们长时间等不到船只,只能悻悻而归(服务器长时间未响应,超时异常),如下图所示:
经过这次教训后,河流管理者决定必须限制下水的船只数量(设置线程池中的最大线程数,比如:100),这样就可以防止河流卡死了。
当下次1万人同时采购时,河流管理者还是派专职船只进行接送,不过最多允许100个船只下水,所以河流并没有那么拥挤,已经出发的人们都能正常的采购到食物回家,但是岸边有很多采购人在焦急的排队等待船只,这样慢悠悠的过了好长时间,人群开始焦躁,最后悻悻而归。
又经过教训后,河流管理者痛定思痛,根本的原因还是船只将采购人送到对岸后没有立刻返回,造成了浪费(阻塞调用,浪费线程资源),于是河流管理者开始改革。
首先,它规定所有的船只一旦将人送到岸后就立刻返回,马上接收新的调度任务,其次采购人完成采购后需要主动请求河流管理者派船只将自己送回去(I/O设备向CPU发中断信号),最后,为了防止河流拥堵,河流管理者还是限制下水的船只数量。
经过这次改革后,当1万个采购者同时出现时,河流管理者仅用100艘快船就迅速的将采购人转移到了对岸,当他们采购完成后又迅速的将他们送了回来,如下图所示:
改革后的方案也称之为epoll模型(linux),当然在window中叫做IOCP。
四、linux中的select、poll和epoll
通过上面的故事,我们知道了epoll的机制,显然它是高并发下最有效的请求响应模型。
然而,当epoll出现之前,linux为了解决高并发问题还制定了select和poll模型,不过它们两个都有着很明显的缺点,可以认为它们是残次品吧。
但是,在并发程度不高的情况下,select和poll的性能和epoll也不相上下,所以nginx也是支持select和poll的(能用epoll的就用epoll
)。
五、window中的IOCP
IOCP的原理和epoll一致。不过仍有些不同:
在.net framewok时代,线程池中的线程被分为两类,工作线程
和完成端口线程
。两者的区别是 “完成端口线程专门为I/O设备读写完成后回调使用的。”
ThreadPool.SetMaxThreads(workerThreads: 100, completionPortThreads: 100);
对于为什么要把线程人为的划分为两类,我还没找到准确的答案,大概是当时的设计者担心线程池内的线程被用光而导致没有线程用于回调,进而导致无限等待了吧。。。
在.net core时代,设计者有意弱化IOCP的概念,即:不想再区分工作线程
和完成端口线程
了。所以在.net framework中的I/O回调代码中,我们能明显看到完成端口线程
可用数量的减少,但是在.net core中同样的I/O回调代码下,我们仅能观察到工作线程
的可用数量减少,而完成端口线程
的可用数量始终保持不变。
另外,当我们代码中强制调用线程池中的完成端口线程
时(.net core),window下能观察到完成端口线程
的可用数量减少,但是在linux下抛出异常,测试代码如下:
// .net core
class Program
{
static void Main(string[] args)
{
ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
Console.WriteLine($"workerThreads= {workerThreads} , completionPortThreads= {completionPortThreads}");
unsafe
{
Overlapped overlapped = new Overlapped();
NativeOverlapped* pOverlapped = overlapped.Pack((uint errorCode, uint numBytes, NativeOverlapped* _overlapped) =>
{
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
Console.WriteLine($"workerThreads= {workerThreads} , completionPortThreads= {completionPortThreads}");
Console.WriteLine($"here");
}, null);
ThreadPool.UnsafeQueueNativeOverlapped(pOverlapped);
}
Console.ReadLine();
}
}
window下的运行结果:
centos下运行报错:
Unhandled exception. System.PlatformNotSupportedException: Operation is not supported on this platform.
at System.Threading.ThreadPool.PostQueuedCompletionStatus(NativeOverlapped* overlapped)
at System.Threading.ThreadPool.UnsafeQueueNativeOverlapped(NativeOverlapped* overlapped)
那么,IOCP的概念究竟应不应该去掉呢?完成端口线程
有没有发挥到它的作用呢?
我认为:IOCP可以去掉,完成端口线程
发挥的作用有限。
试想一下,I/O设备回调用的是完成端口线程
,如果回调中再发起I/O读写,那么回调就又使用完成端口线程
,这样下去,我们只要发起一次回调,之后的代码都将运行在完成端口线程
上了。。。
听起来很恐怖的样子,不过我没有试验,不知道真正效果如何。
另外,epoll中也没有专门给I/O回调配置线程不也运行的挺好的嘛。
实际上,我认为,只要代码中涉及到I/O读写的代码调用处都使用异步的话应对高并发就没有问题了。不用再去区分什么工作线程
和完成端口线程
了,反正代码运行的都很快,但凡慢一点的都交给I/O回调了,不用担心I/O回调时没有线程可用。
在.net 源码中能看到关于IOCP的讨论,这里粘贴出来:
https://github.com/orgs/dotnet/projects/25#card-52268925
https://github.com/dotnet/runtime/issues/46610
六、.net中的异步实现方式
异步的作用是将串行的代码改成并行的,当代码并行后,当前线程是释放回线程池还是阻塞以等待异步执行完毕,可以将异步作用分为两个:
- 异步后,当前线程不发生阻塞,释放回线程池,这适合高并发编程;
- 异步后,当前线程需要等待异步执行完毕,在并发不高的情况下,这没什么问题;
看下面使用异步后还阻塞当前线程的代码(task.Wait()
和task.Result
均会阻塞线程):
当然,有一些地方即使使用了异步也是需要等待异步结果返回的,这时候,我们可以选择将代码全部异步话,即:从头到尾改成异步的,中间不要有阻塞代码,这样就能避免线程的空转消耗,解决高并发问题,如下:
从上面可以看到异步代码有两种释放方式,一个是异步后使当前线程同步等待异步结果,一个是代码完全异步化。
为了,解决代码书写的问题,微软
- 在.net 1.0的时候提出了
异步编程模型(APM)
;以IAsyncResult接口为核心,支持异步的接口命名规则为: BeginXXXX、EndXXXX
- 在net 2.0的时候提出了
基于事件实现的异步编程(EAP)
;支持异步的方法以XXXXAsync命名,外加XXXXCompleted事件
- 在.net 4.0的时候提出了
基于任务实现的异步编程(TAP)
;是我们现在常用的task模式,await/async task
1. 异步编程模型(APM)
这个编程模型以IAsyncResult
接口为核心,任何支持异步的方法都应该返回这个接口,并且具有AsyncCallback
回调:
观察下IAsyncResult
接口:
很简洁的四个属性表达了异步编程的核心:
当我们调用BeginXXXX异步调用时,虽然没有立刻返回结果,但是给了我们一个信物(就是IAsyncResult
),凭借这个信物,我们可以轮训是否异步完成(IsCompleted==true
),或者直接阻塞当前线程以等待异步结束(AsyncWaitHandle.WaitOne()
)。
虽然凭借这个信物我们可以判断异步是否结束,并在结束后去做一些事情,但这和上面讲的河流摆渡者模型并不符合,也没办法解决高并发问题,因为基于epoll或IOCP
模型实现的高并发必须有回调出现,所以BeginXXXX方法中会有AsyncCallback
委托,这个委托的定义也非常简单,如下:
AsyncCallback
委托接收IAsyncResult
入参。
IAsyncResult
中还有一个AsyncState
属性,它可以用来将一些上下文信息作为对象传递到异步代码块中,可以根据需要任意指定。
IAsyncResult
中还有一个CompletedSynchronously
属性,这个属性用来判断代码完成后它是不是同步完成的?听起来,这很矛盾,不应该有这个属性。但在.net framewoke下FileStream的构造函数中有一个参数useAsync,当传递true时FileStream的读写才会用异步,否则还是同步,所以CompletedSynchronously
属性在FileStream的BeginXXXX的回调中就显现出作用了。
下面是模拟的APM模式代码:
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} main thread {Thread.CurrentThread.ManagedThreadId}");
var fs = new MyAsyncFileSteam();
var bs = new byte[10];
var asyncRes2 = fs.BeginRead(bs, 0, 10, asyncRes =>
{
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 异步回调 thread {Thread.CurrentThread.ManagedThreadId}");
}, new { Name = "小明" });
asyncRes2.AsyncWaitHandle.WaitOne();
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} main thread {Thread.CurrentThread.ManagedThreadId}");
int len = fs.EndRead(asyncRes2);
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} main thread {Thread.CurrentThread.ManagedThreadId} len={len}");
Console.ReadLine();
}
}
public class MyAsyncFileSteam
{
private Dictionary<IAsyncResult, int> _dic = new Dictionary<IAsyncResult, int>();
public IAsyncResult BeginRead(byte[] arr, int offSet, int length, AsyncCallback asyncCallback, object state)
{
var manEventReset = new ManualResetEvent(false);
var res = new MyAsyncResult(manEventReset, state);
// 模拟读文件,实际上应该是调用设备IO,读写完成后设备发出DMA中断到CPU,然后再回调用户代码
ThreadPool.QueueUserWorkItem(_state =>
{
// 假设最多文件只有一个字节
for (var i = offSet; i < offSet + 1; i++)
{
arr[offSet] = 1;
}
//模拟耗时
Thread.Sleep(3000);
_dic.Add(res, 1);
manEventReset.Set();
res.SetCompleted();
ThreadPool.QueueUserWorkItem(_state2 =>
{
asyncCallback(res);
});
});
return res;
}
public int EndRead(IAsyncResult asyncResult)
{
asyncResult.AsyncWaitHandle.WaitOne();
return _dic[asyncResult];
}
public class MyAsyncResult : IAsyncResult
{
private readonly WaitHandle waitHandle;
private readonly object state;
private bool isCompleted = false;
public MyAsyncResult(WaitHandle waitHandle, object state)
{
this.waitHandle = waitHandle;
this.state = state;
}
internal void SetCompleted()
{
isCompleted = true;
}
public bool IsCompleted => isCompleted;
public WaitHandle AsyncWaitHandle => waitHandle;
public object AsyncState => state;
public bool CompletedSynchronously => isCompleted;
}
}
输出如下:
2. 基于事件实现的异步编程(EAP)
根据上面介绍,使用APM方式已经可以实现异步编程了,但是使用起来比较麻烦,因为要想不阻塞当前线程就必须利用回调,而回调后会发生线程切换,这对Winform线程来说是致命的(默认,winform中只允许UI线程访问控件)。
所以,基于事件的异步编程应运而生,它不仅简化了异步代码的编写还能让回调代码运行在主线程中。
典型的组件示例是PictureBox
,新建一个winform工程,在窗体上拖拽一个Lable标签、一个PictureBox控件、一个Button按钮,代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void PictureBox1_LoadProgressChanged(object sender, ProgressChangedEventArgs e)
{
label1.Text = $"thread:{Thread.CurrentThread.ManagedThreadId} " + e.ProgressPercentage + "%";
}
private void PictureBox1_LoadCompleted(object sender, AsyncCompletedEventArgs e)
{
if (e.Cancelled)
{
label1.Text = $"thread:{Thread.CurrentThread.ManagedThreadId} " + "取消图片加载";
}
else
{
label1.Text = $"thread:{Thread.CurrentThread.ManagedThreadId} " + "图片加载完成";
}
}
private void button1_Click(object sender, EventArgs e)
{
pictureBox1.LoadAsync("https://file01.16sucai.com/d/file/2013/0708/20130708051417442.jpg");
pictureBox1.LoadCompleted += PictureBox1_LoadCompleted;
pictureBox1.LoadProgressChanged += PictureBox1_LoadProgressChanged;
//pictureBox1.CancelAsync();
}
}
}
运行效果如下:
可以看到,基于事件的模型的异步代码确实要比APM模型简单多了,而且还支持取消异步(如果涉及设备IO的话,需要设备支持取消操作)。
除了PictureBox控件,最能代表EAP模型的还有BackgroundWorker
组件,它的示例MSDN上已经有详细的介绍,请参照:《MSDN:BackgroundWorker 类》
3. 基于任务实现的异步编程(TAP)
虽然有了APM和EPM两种异步模型,但在高并发时编写代码还是有难度:
- 像APM一样多层嵌套回调会形成回调地狱,不好看也不易阅读;
- EPM的事件触发调用并不适合高并发场景;
所以,微软提供了Task类,借助Task和async/await 可以将代码块自动切割并交个线程池去运行,这样既达到高性能运行又便于书写和理解,代码如下:
可以看到,虽然我们的代码中没有回调出现,但是代码已经被async/await 分割成了4块并交给线程池自动调度,其中主线程已经被释放,而线程池中id为3的线程被运行了3次。
其实Task
也是一个IAsyncResult
,如下:
那么,IAsyncResult的四个属性Task必然也有,不过Task实现的时候对AsyncWaitHandle和CompletedSynchronously使用接口显示实现,所以我们必须将Task转成IAsyncResult后才能调用,如下:
七、async/await语法糖之状态机
从上面的分析中,我们知道:具有async/await修饰的Task代码块会被自动切割,这里就来看一下它是怎么被切割的。
以下面代码示例:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine($"before task.run main thread:{Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
Console.WriteLine($"task.run1 thread:{Thread.CurrentThread.ManagedThreadId}");
});
await Task.Run(() =>
{
Console.WriteLine($"task.run2 thread:{Thread.CurrentThread.ManagedThreadId}");
});
await Task.Run(() =>
{
Console.WriteLine($"task.run3 thread:{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"after task.run main thread:{Thread.CurrentThread.ManagedThreadId}");
}
}
}
这个代码编译后被分割成了5块,为了能在ILSpy中观察到编译后的代码,设置ILSpy选项:
设置后打开dll文件:
注意:可执行程序集的入口点不一定是main函数,程序集的头信息可以记录入口位置,在dnSpy中观察如下:
为了能更好的阅读,我们将代码拷贝到VS中,剔除掉代码中的<>符号并修复掉其他报错信息,最终如下:
// ConsoleApp2.Program
using ConsoleApp2;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
internal class Program
{
[CompilerGenerated]
private sealed class Maind__0 : IAsyncStateMachine
{
public int state__1;
public AsyncTaskMethodBuilder t__builder;
public string[] args;
private TaskAwaiter u__1;
private void MoveNext()
{
int num = state__1;
try
{
TaskAwaiter awaiter3;
TaskAwaiter awaiter2;
TaskAwaiter awaiter;
switch (num)
{
default:
Console.WriteLine($"before task.run main thread:{Thread.CurrentThread.ManagedThreadId}");
awaiter3 = Task.Run(delegate
{
Console.WriteLine($"task.run1 thread:{Thread.CurrentThread.ManagedThreadId}");
}).GetAwaiter();
if (!awaiter3.IsCompleted)
{
num = (state__1 = 0);
u__1 = awaiter3;
Maind__0 stateMachine = this;
t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
return;
}
goto IL_00c0;
case 0:
awaiter3 = u__1;
u__1 = default(TaskAwaiter);
num = (state__1 = -1);
goto IL_00c0;
case 1:
awaiter2 = u__1;
u__1 = default(TaskAwaiter);
num = (state__1 = -1);
goto IL_013e;
case 2:
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (state__1 = -1);
break;
}
IL_00c0:
awaiter3.GetResult();
awaiter2 = Task.Run(delegate
{
Console.WriteLine($"task.run2 thread:{Thread.CurrentThread.ManagedThreadId}");
}).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (state__1 = 1);
u__1 = awaiter2;
Maind__0 stateMachine = this;
t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
goto IL_013e;
IL_013e:
awaiter2.GetResult();
awaiter = Task.Run(delegate
{
Console.WriteLine($"task.run3 thread:{Thread.CurrentThread.ManagedThreadId}");
}).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (state__1 = 2);
u__1 = awaiter;
Maind__0 stateMachine = this;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
break;
}
awaiter.GetResult();
Console.WriteLine($"after task.run main thread:{Thread.CurrentThread.ManagedThreadId}");
}
catch (Exception exception)
{
state__1 = -2;
t__builder.SetException(exception);
return;
}
state__1 = -2;
t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(Maind__0))]
[DebuggerStepThrough]
private static Task MainTask(string[] args)
{
Maind__0 stateMachine = new Maind__0();
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.args = args;
stateMachine.state__1 = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[DebuggerStepThrough]
private static void Main(string[] args)
{
MainTask(args).GetAwaiter().GetResult();
}
}
这里可以自行调试运行一下,观察下运行的顺序就明白了vs是怎么将切分的代码块按顺序运行的。
主要逻辑如下:
- 第一步:创建状态机,继承IAsyncStateMachine,设置其状态为-1;
- 第二步:执行状态机的MoveNext()方法,因为状态为-1,所以执行了切割的第一块代码,并使用Task开启了第二块代码的调用;
- 第三步:第二块代码调用完成后,状态机构造器设置状态机状态为0,并调用状态机的MoveNext()方法;
- 第四步:执行状态机的MoveNext()方法,因为状态为0,所以使用Task开启了第三块代码的调用;
- 第五步:第三块代码调用完成后,状态机构造器设置状态机状态为1,并调用状态机的MoveNext()方法;
- 第六步:执行状态机的MoveNext()方法,因为状态为1,所以使用Task开启了第四块代码的调用;
- 第七步:第四块代码调用完成后,状态机构造器设置状态机状态为2,并调用状态机的MoveNext()方法;
- 第八步:执行状态机的MoveNext()方法,因为状态为2,所以直接执行了第五块代码;
从分析过程可以看出,vs对async/await标记的方法生成了状态机,并按照await位置将方法中的代码块切割,最后使用状态机的回调机制按顺序执行代码,达到了最终的效果。
如果让我们自己优化上面的代码,我们会怎么生成呢?
最简单的写法如下:
static Task Main(string[] args)
{
Console.WriteLine($"before task.run main thread:{Thread.CurrentThread.ManagedThreadId}");
return Task.Run(() =>
{
Console.WriteLine($"task.run1 thread:{Thread.CurrentThread.ManagedThreadId}");
}).ContinueWith(task =>
{
Console.WriteLine($"task.run2 thread:{Thread.CurrentThread.ManagedThreadId}");
}).ContinueWith(task =>
{
Console.WriteLine($"task.run3 thread:{Thread.CurrentThread.ManagedThreadId}");
}).ContinueWith(task =>
{
Console.WriteLine($"after task.run main thread:{Thread.CurrentThread.ManagedThreadId}");
});
}
八、关于ConfigureAwait(false)
在书写异步代码时,我们经常看到如下代码:
await httpClient.GetAsync(“https://www.baidu.com”).ConfigureAwait(false);
这里的ConfigureAwait
的作用是什么呢?
答:告诉task运行时不捕获当前的上下文。也可以简单的理解为:告诉Task调度程序,后面的代码没必要在原来的线程上运行。这就等于解放了Task调度器的手脚,Task调度时将更加迅速,因为调度器没必要等待原有线程是否空闲。
以下面代码分别在.net core3.1和.net framework平台下运行的效果说明其差别:
// 控制台程序
public class Class2
{
public static async Task Main(string[] args)
{
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} main:{Thread.CurrentThread.ManagedThreadId}");
var httpClient = new HttpClient();
//.ConfigureAwait(true)等于不写
//控制台下.ConfigureAwait(true)不起作用,即:调度器都是从线程池里面随机取的线程取运行后续代码
//所以控制台下,写不写.ConfigureAwait(false)都一样
await httpClient.GetAsync("https://www.baidu.com").ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} main:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadLine();
}
}
// winform
private async void button2_Click(object sender, EventArgs e)
{
MessageBox.Show($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} main:{Thread.CurrentThread.ManagedThreadId}");
button1.Text = "请求中...";
var httpClient = new HttpClient();
//.ConfigureAwait(true)等于不写
//winform下.ConfigureAwait(true)会使后续的代码还在原来线程上运行,而.ConfigureAwait(false)则告诉调度器,后续的代码从线程池里任意找一个线程运行即可
//所以,winform下写.ConfigureAwait(false)一定要注意后续代码不能访问UI控件
await httpClient.GetAsync("https://www.baidu.com").ConfigureAwait(true);
MessageBox.Show($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} main:{Thread.CurrentThread.ManagedThreadId}");
button1.Text = "button1";
}
// asp.net core 3.1
[ApiController]
[Route("[controller]/[action]")]
public class DemoController : ControllerBase
{
public async Task<string> Get()
{
var str = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} before: {Thread.CurrentThread.ManagedThreadId}";
var httpClient = new HttpClient();
// .ConfigureAwait(true)等于不写
// asp.net core 下.ConfigureAwait(true)不起作用,即:调度器都是从线程池里面随机取的线程取运行后续代码
// 所以asp.net core 下,写不写.ConfigureAwait(false)都一样
await httpClient.GetAsync("http://www.baidu.com").ConfigureAwait(false);
str += "rn" + $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} after: {Thread.CurrentThread.ManagedThreadId}";
return str;
}
}
关于ConfigureAwait(false)
的更多解释:《理解C#中的ExecutionContext vs SynchronizationContext》
关于SynchronizationContext
参照:《c#:深入理解SynchronizationContext》
最后
以上就是缓慢冬日为你收集整理的c#: 异步代码是如何解决高并发问题的?async/await、Task、IOCP/epoll的全部内容,希望文章能够帮你解决c#: 异步代码是如何解决高并发问题的?async/await、Task、IOCP/epoll所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复