11、异步编程1
误解:
什么是异步编程?
举例:服务员点菜;
- 同步:你叫服务员,服务员拿菜单给你站着等待你点菜,你点完菜后给服务员拿到后厨;
- 异步:你叫服务器,服务员拿菜单给你,服务员先去忙了,等你点完菜后,再叫服务员拿好点好的菜单到后厨;
- 你叫服务员,相当于线程处理你的业务
- 你在点餐,相当于在处理耗时操作
- 服务员等你点菜,相当于当前线程在等待你处理耗时操作
- 服务员去忙,相当于你处理耗时操作,当前线程去忙其他了
- 再叫服务员拿好点好的菜单到后厨,相当于重新申请一个线程(不一定是先前的服务员)来帮你处理耗时出来的结果;
异步点餐一定会提升单个客户点餐速度吗?不能;
能减少耗时操作吗?也不能;
异步能服务更多的客户,也就是提高并发数量;
异步的主要特色是:不等,即意思是不在主线程里等, 不会阻塞, 在子线程里还是要等
异步能使得单个请求处理效率变快吗?不会,异步只是提高web服务器能够同时请求数量,仅此而已;
async、await ≠ 多线程
12、异步编程2 async/await使用
异步方法:用async关键字修饰的方法
异步方法的返回值一般是 Task<T>
,T
是真正的返回值类型,Task<int>
。惯例:异步方法名字以ASync结尾。
即使方法没有返回值,也最好把返回值类型void声明为非泛型的Task;
调用异步方法时,一般在方法前加上await关,这样拿到的返回值就是泛型指定的T类型;
异步方法的“传染性”:一个方法中如果有await调用,则这个方法也必须修饰为async;
static async Task Main(string[] args){
string fileName = "d:/1.txt";
File.Delete(fileName);
File.WriteAllTextAsync(fileName, "hello,world.");
String s = await File.ReadAllTextAsync(fileName);
Console.WriteLine(s);
}
不加await的调用异步方法:
案例1:
static async Task Main(string[] args){
string fileName = "d:/1.txt";
File.Delete(fileName);
StringBuilder sb = new ();
for(int i = 0; i< 10000; i++) sb.AppendLine("hello");
File.WriteAllTextAsync(fileName, sb.ToString()); // 这里不加await,会直接到下面读取
String s = await File.ReadAllTextAsync(fileName); // 上面没写完,直接读取会报错
Console.WriteLine(s);
}
案例2:
public async Task GetSomeDataAsync()
{
Console.WriteLine("Run Start." + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1000);
Console.WriteLine("Run End." + Thread.CurrentThread.ManagedThreadId);
}
static async Task Main(string[] args)
{
Console.WriteLine("Main Start." + Thread.CurrentThread.ManagedThreadId);
Task<string> task = GetSomeDataAsync(); // 没有使用await关键字
// 继续执行后续代码
Console.WriteLine("Main End." + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
13、异步编程3 编写异步方法
static async Task Main(string[] args)
{
await DownloadHtmlAsync("https://www.baidu.com/", "d:/1.txt");
}
static Task<int> DownloadHtmlAsync(string url, string filename){
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(filename, html);
return html.length;
}
理解 .Wait() 和 .Result (不建议使用)
用于等待异步任务完成并获取其结果的方法
用在哪里?对于方法不能是async,且方法内部调用其他方法只有同步的话,就只能使用 .Wait 或者 .Result 了:
static void Mian(string[] args){ // 假设 Main 没有 async
// 假设没有同步方法 WriteAllText()
File.WriteAllTextAsync(@"d:/1.txt", "hhhhhhhhhhhhhhhhhh").Wait();
}
.Wait() 无返回值;
.Result 有返回值;
.Wait()
是Task
类的实例方法,可以直接在Task
对象上调用,例如task.Wait()
。
.Wait()
会将异步任务中的异常封装在AggregateException
中并抛出,需要进行适当的异常处理。如果异步任务发生多个异常,AggregateException
将包含这些异常的集合。
.Result
是Task
类的属性,可以通过访问Task
对象的Result
属性来获取任务的结果,例如task.Result
。
.Result
会将异步任务中的异常直接抛出,不会进行额外的封装。如果异步任务发生多个异常,.Result
只会抛出其中一个异常。
都是阻塞操作,与 await 不同的是,await 不等待直接返回,而 .Wait/.Result 会阻塞等待耗时操作完成;
死锁:
使用.Wait()或.Result来等待异步任务的完成可能会导致死锁的风险,特别是在UI线程或主线程中使用时。
- 在UI线程或主线程中使用
.Wait()
或.Result
:如果在UI线程或主线程中使用.Wait()
或.Result
来等待异步任务的完成,而该任务需要访问UI或主线程上的资源,就会发生死锁。因为.Wait()
或.Result
会阻塞当前线程,而异步任务需要在UI或主线程上执行,两者相互等待对方完成,导致死锁。
- 使用同步上下文的异步方法:某些异步方法会使用同步上下文(例如
ConfigureAwait(true)
),这会导致异步任务在原始的上下文中执行。如果在该上下文中使用.Wait()
或.Result
来等待任务的完成,就会发生死锁。因为同步上下文可能需要在当前线程上执行任务,而.Wait()
或.Result
会阻塞当前线程,导致死锁。
所以,不建议使用 .Wait() 和 .Result
异步调用的好处?
ThreadPool.QueueUserWorkItem(async (obj) => { // 模拟并发请求
while(true){
await File.WriteAllTextAsync(@"d:/1.txt", "hhhhhhhhhhhhhhhhhh");
Console.WriteLine("xxxxxxxxxxx");
}
});
Console.Read();
加深理解 async/await
static async Task Run()
{
Console.WriteLine("Run Start." + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(5000);
// 这里重新申请了线程,来执行后面的代码
Console.WriteLine("Run End." + Thread.CurrentThread.ManagedThreadId);
}
static async Task StartRun()
{
Console.WriteLine("StartRun Start." + Thread.CurrentThread.ManagedThreadId);
await Run();
Console.WriteLine("StartRun End." + Thread.CurrentThread.ManagedThreadId);
}
static async Task Main(string[] args)
{
StartRun();
Console.WriteLine("Main End." + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.ReadKey();
}
------------------------
StartRun Start.1
Run Start.1
Main End.1 <---- 输出这里后等待了5秒
Run End.3
StartRun End.3
14、异步编程4 async/await 原理
将下面代码编译,用ILSPY反编译工具查看:
用ILSpy反编译dll(.exe只是windows下的启动器)成c#4.0版本,即:
- 需要改为 C# 4.0/VS2010
- 视图 - 显示所有类型和成员
static async Task Main(string[] args)
{
using(HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync("https://www.baidu.com/");
Console.WriteLine(html);
}
string destFilePath = "D:/1.txt";
string content = "Hello async and await";
await File.WriteAllTextAsync(destFilePath, content);
string content2 = await File.ReadAllTextAsync(destFilePath);
Console.WriteLine(content2);
Console.Read();
}
await、async是“语法糖”,最终编译成“状态机调用”
总结:async的方法会被C#编译器编译成一个类,主要根据await调用进行切分为多个状态,对async方法的调用会被拆分为对MoveNext的调用。
用await看似是“等待”,经过编译后,其实没有“wait
为什么要把一个async方法拆分为多个状态然后分为多次调用?
异步的可以避免线程等待耗时操作”但是await还是等待呀?
15、异步编程5 async背后的线程切换
await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。
案例1:
static async Task Main(string[] args){
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
StringBuilder sb = new ();
for(int i = 0; i< 10000; i++) sb.AppendLine("hello");
await File.WriteAllTextAsync("d:/1.txt", sb.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
// -------------- 多运行几次查看结果
1
8
案例2:
static async Task Run()
{
Console.WriteLine("Run Start." + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(5000);
// 这里重新申请了线程,来执行后面的代码
Console.WriteLine("Run End." + Thread.CurrentThread.ManagedThreadId);
}
static async Task StartRun()
{
Console.WriteLine("StartRun Start." + Thread.CurrentThread.ManagedThreadId);
await Run();
Console.WriteLine("StartRun End." + Thread.CurrentThread.ManagedThreadId);
}
static async Task Main(string[] args)
{
StartRun();
Console.WriteLine("Main End." + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.ReadKey();
}
------------------------
StartRun Start.1
Run Start.1
Main End.1 <---- 输出这里后等待了5秒
Run End.3
StartRun End.3
16、异步编程6 异步方法不等于多线程
异步方法的代码并不会自动在新线程中执行,除非把代码放到新线程中执行。
public static async Task CalcAsync()
{
Console.WriteLine("CalcAsync Start " + Thread.CurrentThread.ManagedThreadId);
await Task.Run(() =>
{
Console.WriteLine("Task Start " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000); // 模拟耗时操作
Console.WriteLine("Task End" + Thread.CurrentThread.ManagedThreadId);
});
Console.WriteLine("CalcAsync End " + Thread.CurrentThread.ManagedThreadId);
}
static async Task Main(string[] args)
{
Console.WriteLine("Main Start " + Thread.CurrentThread.ManagedThreadId);
await CalcAsync();
Console.WriteLine("Main End " + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
// --------------
Main Start 1
CalcAsync Start 1
Task Start 8 <------这里输出后等待3s
Task End 8
CalcAsync End 8
Main End 8
17、异步编程7 为什么有的异步方法没有async?
比如:File.ReadAllTextAsync();
await 和 async 成对出现的,方法没有await,那么也就不需要async了....
其他案例:
public static Task<string> CalcAsync()
{
return Task.Run(() => // 直接返回 Task<string>
{
Thread.Sleep(3000); // 模拟耗时操作
return Task.FromResult("ok"); // 将 字符串 包装成 Task<string>
});
}
static async Task Main(string[] args)
{
string result = await CalcAsync();
await Console.Out.WriteLineAsync(result);
Console.ReadKey();
}
async方法缺点:
1、异步方法会生成一个类运行效率没有普通方法高;
2、可能会占用非常多的线程;
优点:
写起来简单、提高请求数量,响应速度正常;
18、异步编程8 不要使用Shellp()
如果想在异步方法中暂停一段时间,不要用Thread.Sleep,因为它会阻塞调用线程,而要用 await Task.Delay()
。
举例:下载一个网址,3秒后下载另一个:
在控制台中没看到区别,但是放到WinForm程序中就能看到区别了。
ASP.NET Core 中也看不到区别,但是Sleep()会降低并发。
用winfrom操作
private async void button1_Click(object sender, EventArgs e){
using(HttpClient httpClient = new ()){
string s1 = awiat httpClient.GetStringAsync("https://www.baidu.com/");
textBox1.Text = s1.SubString(0,100);
// Thread.Sleep(3000); // 会阻塞主线程(UI线程)
await Task.Delay(3000); // 不会阻塞主线程(UI线程)
string s2 = awiat httpClient.GetStringAsync("https://www.so.cn/");
textBox2.Text = s2.SubString(0,100);
Console.ReadKey();
}
}
19、异步编程9 CancellationToken
有时需要提前终止任务,比如:请求超时、用户取消请求。
很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。
CancellationToken 结构体
None:空
- bool lsCancellationRequested 向异步中发送是否取消
- (*)Register(Action callback) 注册取消监听(不常用)
- ThrowlfCancellationRequested() 如果任务被取消,执行到这句话就抛异常。
CancellationTokenSource 通过这个类创建 CancellationToken 对象;
-
CancelAfter() 超时后发出取消信号
-
Cancel() 发出取消信号
CancellationToken Token
例子:为“下载一个网址N次”的方法增加取消功能。
分别用 GetStringAsync + IsCancellationRequested、GetStringAsync + ThrowlfCancellationRequested()、带 CancellationToken 的 GetAsync() 分别实现。
取消分别用超时、用户敲按键(不能await)实现。
static HttpClient client = new HttpClient();
public static async Task DownloadAsync(string url, int n, CancellationToken cancellationToken)
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
await Console.Out.WriteLineAsync($"{DateTime.Now}: {html}");
#region 方法1,判断是否取消
//if (cancellationToken.IsCancellationRequested)
//{
// Console.WriteLine("请求被取消.");
// break;
//}
#endregion
#region 方法2,取消状态时抛出异常
cancellationToken.ThrowIfCancellationRequested();
#endregion
}
}
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(1000); // 超时触发,超过1s就取消执行
CancellationToken cToken = cts.Token;
try
{
await DownloadAsync("https://www.baidu.com", 100, cToken);
}
catch (OperationCanceledException e)
{
await Console.Out.WriteLineAsync("任务被取消了...");
}
// cts.Cancel(); // 手动触发取消执行
Console.ReadKey();
}
ASP.NET Core 开发中,一般不需要自已处理 CancellationToken、CancellationTokenSource这些,只要做到“能转发CancellationToken就转发“即可。ASP.NET Core 会对于用户请求中断进行处理。
(*)演示一下ASP.NETCore中的使用: 写一个方法,Delay1000次,用Debug.WriteLine(输出,访问中间跳到放到其他网站。
static HttpClient client = new HttpClient();
public async Task<IActionResult> Index(CancellationToken cancellationToken){
await DownloadAsync("https://www.baidu.com", 1000, cancellationToken); // 请求后一直刷新输出,只有离开页面才停止
//await DownloadAsync("https://www.baidu.com", 1000, CancellationToken.None); // 请求后一直刷新输出,只有停止服务器才停止
return View();
}
public static async Task DownloadAsync(string url, int n, CancellationToken cancellationToken)
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
Debug.WriteLine(html);
if (cancellationToken.IsCancellationRequested)
{
Debug.WriteLine("请求被取消.");
break;
}
}
}
20、异步编程10 WhenAll
1.Task<Task> WhenAny(lEnumerable<Task> tasks)
等,任何一个Task完成,Task就完成。
2.Task<TResultl> WhenAll<TResult>(params Task<TResult>[]tasks)
等,所有Task完成Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。
3.FromResult() 创建普通数值的Task对象。
Task<string> t1 = File.ReadAllTextAsync("D:/1.txt");
Task<string> t2 = File.ReadAllTextAsync("D:/2.txt");
Task<string> t3 = File.ReadAllTextAsync("D:/3.txt");
string[] results = await Task.WhenAll(t1, t2, t3);
string s1 = results[0];
string s2 = results[1];
string s3 = results[2];
21、异步编程11 异步其他问题
接口中的异步方法:
async是提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async。
interface ITest
{
async Task<int> GetChatCount(string file);
}
// 只能在具有正文的方法中使用“async”修饰符
所以,应该这样:
interface ITest
{
Task<int> GetChatCount(string file);
}
class Test1 : ITest
{
public async Task<int> GetChatCount(string file)
{
return await Task.FromResult(100);
}
}
异步与yield:
复习:yield return 不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提升性能。
foreach(var item in Test()){
Console.WriteLine(item);
}
static IEnumerable<string> Test(){
yield return "hello";
yield return "yzk";
yield return "youzack";
}
在旧版c#中,async方法中不能用yield。从C#8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用 await foreach() 即可。
await foreach(var item in Test()){
Console.WriteLine(item);
}
static async IAsyncEnumerable<string> Test(){
yield return "hello";
yield return "yzk";
yield return "youzack";
}
ASP.NET Core和控制台项目中没有 SynchronizationContext,因此不用管 ConfigureAwait(false)等这些了。
不要同步、异步混用。