异步
引言
在现实生活中,我们迫切的需要做一件事,尤其是需要等待的时候,"异步"的例子在这里体现的尤为强烈.
当我们口渴时,需要等待一杯温开水来解我们于口渴之中.这时,等待这个关键词就体现出了我们要同步等待水开,并且,其他事请我们都做不了,刷个短视频,打两把游戏.都不行.
但我们在等待的同时,也继续着手中的游戏,短视频,这个就成为了异步.
同步->异步,这个过程真的使我们所消耗的时间减少了?
并没有,因为事情还是那个事情,没做完,我们也不能上赶子的去要结果.它只是提高了我们在同一时刻可以做多个事情.
正文
在C#中,通过2个关键字,一个特殊后缀和特殊返回值来加以修饰异步的特性.规则如下:
- await
- async
- 对应的异步方法,后缀名+Async
- 返回值类型Task类型.
终将还是要回到代码上的,在代码中,异步的场景主要体现在,做一个相当大的耗时操作时,由于CPU执行指令是顺序执行,会导致出些一些异常.
在进行一些大文件的读写操作时,会因为没有使用异步的情况下,导致程序以报异常的形式出场.
使用异步方法
static async Task Main(string[] args)
{
string filename = @".\1.txt";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000000; i++)
sb.AppendLine("asfasfddsafsafjsaljfsajfsalfj");
await File.WriteAllTextAsync(filename, sb.ToString());
//若在对应的异步方法添加await会在此有等待.
Console.WriteLine("Write after...");
string content = await File.ReadAllTextAsync(filename);
Console.WriteLine("read after...");
}
这里是进行了一个较大文件的读写操作,如果不适用异步的话,那么在读取文件时就会出问题.
首先,File.WriteAllTextAsync();
时一个异步方法,我们可以看到,前面构造了长度为n的字符串进行了n*10000000次的构造(数据量相当惊人).其后,我们开始对此进行了写入文件的操作.
上述,File.WriteAllTextAsync();
方法是一个异步方法,所以需要用await
加以修饰.期间,会一直等待此异步方法执行完毕.
其后,随着一波控制输出,就开始了读文件,与上述相似,需要加上await来取出执行后的结果.
小问题
尚且认为第一个异步方法可以不加await,如下:
static async Task Main(string[] args)
{
string filename = @".\1.txt";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000000; i++)
sb.AppendLine("asfasfddsafsafjsaljfsajfsalfj");
File.WriteAllTextAsync(filename, sb.ToString());
//若在对应的异步方法添加await会在此有等待.
Console.WriteLine("Write after...");
string content = await File.ReadAllTextAsync(filename);
Console.WriteLine("read after...");
}
那么,write这里就会出现同步情况,其WriteAllTextAsync()
内部是一个子线程的阻塞,所以,这里就会执行到```ReadAllTextAsync()` ``的位置,这时就会报出一个文件还在写就开始读的错误.
await就会加以等待该方法执行完毕.
记忆方法
await可以等待返回值Task的异步方法,加完之后,对应的方法需要async,并且修改返回值为Task
异步方法的委托使用
线程池对象:ThreadPool
ThreadPool.QueueUserWorkItem(async(obj) =>
{
StringBuilder sb = new StringBuilder();
string fileName = @"./2.txt";
for (int i = 0; i < 10000000; i++)
{
sb.AppendLine("djsalflsajflsafsaf");
}
while (true)
{
await File.WriteAllTextAsync(fileName, sb.ToString());
}
});
首先如果拉姆达函数体内有await关键字,那么在一个拉姆达函数形参的位置处加上async来进行修饰.
异步方法的编写
不知道写啥,上来就是如下:
返回值:Task
方法名:Async结尾
如果异步方法体内有await关键词出现,在该异步方法前用async关键词修饰.
特殊情况下的异步使用
在某些比较老的框架,如(.NET framework),可能要求Main方法返回值必须为void.winForm的事件函数中,也要求返回值为void
这个时候,有Wait() -> await
Result -> 取值
缺点:由于方法年代较早,使用过程中可能会出现死锁的 情况,需慎重使用
Wait()可以替代异步等待的作用,Task.Result就相当于取值的作用.
async Main的神秘面纱
static async Task Main(string[] args){
...;
}
如上的异步Main,其实已经不是底层在调用的那个Main了,通过IL反编译出可执行程序的dll文件就会发现,这个Main被以异步的方法进行调用了,并且内部所执行的所有await也都被反编译成了类.
系统会以普通的Main来调用执行异步Main.并进行GetWait(),GetResult()
异步Main将会成为一个类,然后利用状态机模型(不同的返回值,不同的状态)来进行对应的执行.这是await背后的事情.
所以await,async就是所谓的C#语法糖.
async背后的线程切换
await所修饰的函数为界,前面的与后面的代码段所执行的线程可能是不一样的.
原理:await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续代码.
处理的数据量较小时,前后线程号是一样的.
关于await的一些误区
误区一:await所执行的代码,或者是我们自己编写的代码,并不是单独放在一个线程中进行执行的.
代码如下:
static async Task Main(string[] args)
{
Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");
await HandlerAsync();
Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");
}
static async Task HandlerAsync()
{
Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");
while (true);
}
通过执行后可以发现,异步方法中的线程号一直都是之前所使用的线程号.
真正的切换线程号是在进行Task使用时更换线程的,代码如下:
static async Task Main(string[] args)
{
Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
Console.WriteLine($"Run()中的线程号:{Thread.CurrentThread.ManagedThreadId}");
while (true);
});
Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");
}
异步方法缺点
- 异步方法会被CLR生成一个类,运行效率没有普通方法高.
- 可能会占用非常多的线程.
小问题
为何有的方法本身就是异步的,但在声明该方法时,却没有在函数头标注async呢?
简单理解如下:在执行异步方法后会返回一个Task的值,如果不着急将其result返回的话(即没有在调用该异步方法前面标注await时),就可以不在前面标注为async.
换言之,async具有将一个类型T给包装为Task的泛型类型,具有一定的承上启下的作用.
如C#中,File.ReadAllTextAsync()方法.
public static Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default);
实验
我们知道ReadAllTextAsync的返回值为Task,通过我们初次对它的理解,可以得知,该返回值可以通过await进行调用.
static async Task Main(string[] args)
{
string c= await MyReadAsync(1);
Console.WriteLine(c);
}
static Task<string> MyReadAsync(int n)
{
switch (n)
{
case 1:
return File.ReadAllTextAsync(@"./1.txt");//直接返回执行结果Task<string>
case 2:
return File.ReadAllTextAsync(@"./2.txt");
default:
throw new ArgumentException();
}
}
若我们在返回结果上,加上await取出result时,就必须需要在函数前标注async,如下:
static async Task<string> MyReadAsync(int n)
{
switch (n)
{
case 1:
return await File.ReadAllTextAsync(@"./1.txt");
case 2:
return await File.ReadAllTextAsync(@"./2.txt");
default:
throw new ArgumentException();
}
}
但是,外部依然可以调用,这就可以发现,C#的语法糖赋予async的一种行为就是,在把结果返回后,async具有将其具体类型值包装为Task的形式.
若是需要将一个比较耗时的任务放到线程中进行执行的话,并得到结果的话,那么它的代码应该是这样的.
static async Task<int> Main(string[] args)
{
return await Task.Run(() =>
{
int result = 1;
for (int i = 1; i < 5; i++)
{
result *= i;
}
return result;
});
}
可以看到,Task.Run()方法,前面已经加以await的修饰了,这就意味着该方法也是返回了Task的这种类型.那么就会有async也将其result包装的作用.
若不着急使用结果,我们就可以不进行await,从而省去了async的简写.
static Task<int> Main(string[] args)
{
return Task.Run(() =>
{
int result = 1;
for (int i = 1; i < 5; i++)
{
result *= i;
}
return Task.FromResult(result);
});
}
通过这里可以发现,async具有转发异步执行的作用,await具有取出result并作进一步的数据加工的含义.这样才能够使async更进一步的连接好下一操作流程的异步处理.
异步编程是否能用Thread.Sleep()
?
若在异步方法中想睡眠一段时间,不要用Thread.Sleep()
,因为它会阻塞调用线程,而要用await Task.Delay();
作业:
封装一个异步方法,下载给
定的网址,如果下载失败,
则稍等500ms再重试,如果
重试三次仍然失败,则抛异
常“下载失败”
封装为异步方法:
private async Task DownloadWebPage(string uri)
{
int cnt = 0;
using (HttpClient httpClient = new HttpClient())
{
again:
string content = default(string);
try
{
content = await httpClient.GetStringAsync(uri);
//Thread.Sleep(3000); //有明显的阻塞
await Task.Delay(500);//很流畅
textBox1.Text = content;
}
catch (Exception ex)
{
if(ex.GetType() is HttpRequestException)
{
//重连次数.
if(cnt<3)
{
MessageBox.Show(ex.Message);
cnt++;
//实验Thread.Sleep()和Task.Delay()
//Thread.Sleep(3000);
await Task.Delay(500);
goto again;
}
else
{
throw new Exception("下载失败!");
}
}
}
}
}
使用Task.Run()方法,即开启子线程的方式
string c = await Task.Run(async () =>
{
int cnt = 0;
using (HttpClient httpClient = new HttpClient())
{
again:
string content = default(string);
try
{
content = await httpClient.GetStringAsync("https://www.bilibili.com/");
//两种睡眠方法在使用时均无很大的差别,但性能上Task.Delay();方法的使用会比Sleep好的多.
//Thread.Sleep(3000);
await Task.Delay(500);
}
catch (Exception ex)
{
if (ex.GetType() is HttpRequestException)
{
//重连次数.
if (cnt < 3)
{
MessageBox.Show(ex.Message);
cnt++;
//实验Thread.Sleep()和Task.Delay()
//Thread.Sleep(3000);
//await Task.Delay(500);
goto again;
}
}
}
return content;
}
});
异步编程 CancellationToken
作用:可以提高程序的工作效率
应用场景:当用户访问此网页时,由于特殊情况,取消了访问.而在之前向服务器发起的请求,在服务器中一直在计算,无疑浪费了服务器的计算资源.
所以,有很多的异步方法都提供了CancellationToken参数,用于获得提前终止执行的信号.
CancellationToken结构体
struct CancellationToken{
None;//空
bool isCancellationRequested;//是否取消.
Register(Action callback);//注册取消监听.
//若任务被取消,执行到这句话抛异常
void ThrowCancellationRequest(){
if(isCancellationRequested){
throw new Exception();
}
}
}
通过CancellationTokenSource来为CancellationToken创建对象.
CancelAfter();超时后发出取消信号
Cancel();发出取消信号
CancellationToken Token;
举个例子,在向某网站发起请求时,超时的时候将终止处理.但终止处理的种类有:
- 在达到该时间点时,完成此请求后,立即终止称延时终止;
- 立即终止;
就网络编程HttpClient而言,有两种方法可以这样做.
- 适用于立即终止的方法:
GetAsync(CancellationToken token)
; - 适用于延时终止的方法:
if(CancellationToken .IsCancelRequested)
的为真判断.也或者使用抛出异常的形式来结束处理:CancellationToken.ThrowIfCancelRequested()
延时终止处理的两种方法
static async Task Main(string[] args)
{
//创建CancellationToken
//方法1:
CancellationTokenSource source = new CancellationTokenSource();
source.CancelAfter(5000);//5s后,终止处理.
CancellationToken token = source.Token;
await DownloadAsync("https://www.youzack.com", 100,token);
Console.ReadLine();
}
首先使用了CancellationTokenSource进行创建,调用CancelAfter()方法设置5s后的终止时间.并获得其token,然后进行访问.
方法一:使用cancellationToken.IsCancellationRequested
static async Task DownloadAsync(string uri,int n,CancellationToken cancellationToken)
{
using HttpClient httpClient = new HttpClient();
for (int i = 0; i < n; i++)
{
await httpClient.GetStringAsync(uri);
Console.WriteLine($"第{i}次请求!");
//方法1:使用cancellationToken.IsCancellationRequested判断是否已经取消,取消就终止处理,退出
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("终止处理!");
break;
}
}
}
方法二:抛异常的形式,进行处理.
cancellationToken.ThrowIfCancellationRequested()
我们可以根据实际情况,来确定是否要对异常信息进行处理来决定是否捕获.
static async Task DownloadAsync(string uri,int n,CancellationToken cancellationToken)
{
using HttpClient httpClient = new HttpClient();
for (int i = 0; i < n; i++)
{
await httpClient.GetStringAsync(uri);
Console.WriteLine($"第{i}次请求!");
//方法2:使用ThrowIfCancellationRequested来抛出异常进行终止处理.
try
{
cancellationToken.ThrowIfCancellationRequested();
}
catch (Exception)
{
Console.WriteLine("终止处理!");
break;
}
}
}
立即终止处理
static async Task DownloadAsync2(string uri,int n,CancellationToken token)
{
using HttpClient httpClient = new HttpClient();
for (int i = 0; i < n; i++)
{
try
{
await httpClient.GetAsync(uri, token);//若超时,也将被终止请求.
Console.WriteLine($"第{i}次请求!");
}
catch (Exception ex)
{
if (ex is TaskCanceledException)
{
Console.WriteLine("请求终止!");
break;
}
}
}
}
以上是在进行处理过程中如果也超时了,则立即终止.
这种方式在web程序中,常见于那种即时取消请求的方式,如,按q键取消请求
static async Task Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
DownloadAsync2("https://www.youzack.com", 100,token);
while (Console.ReadKey().Key !=ConsoleKey.Q) ;
source.Cancel();//取消
Console.ReadLine();
}
static async Task DownloadAsync2(string uri,int n,CancellationToken token)
{
using HttpClient httpClient = new HttpClient();
for (int i = 0; i < n; i++)
{
try
{
await httpClient.GetAsync(uri, token);
Console.WriteLine($"第{i}次请求!");
}
catch (Exception ex)
{
Console.WriteLine("请求终止!");
break;
}
}
}
通过控制台输入,然后对CancellationTokenSource对象调用Cancel()方法即可.在DownloadAsync2方法中,通过捕获异常的方式来结束.
Core中的真实案例
以MVC为例,当要对CancellationToken进行处理时,通常是这样做的作为控制器行为函数的参数,由Core mvc框架为其传参,根据实际情况,在处理的函数中做操作.
public async Task<IActionResult> Index(CancellationToken token)
{
string uri = "https://www.youzack.com";
await DownloadAsync2(uri, 10000, token);
return View();
}
static async Task DownloadAsync2(string uri, int n, CancellationToken token)
{
using HttpClient httpClient = new HttpClient();
for (int i = 0; i < n; i++)
{
try
{
await httpClient.GetAsync(uri,token);
Debug.WriteLine($"第{i}次请求!");
}
catch (Exception ex)
{
Debug.WriteLine("请求终止!");
break;
}
}
}
可以发现我们无法对token进行修改,它是由core mvc框架管辖的,当请求发生状况时,会在GetAsync()方法中立即见效.
若取消token参数在GetAsync()方法中的传递,在取消对当前网页进行访问后,这些操作将一直在执行.非常浪费服务器的资源.
Task的WhenAll()和WhenAny()方法
Task类的重要方法:
Task<Task> WhenAny(lEnumerable<Task> tasks);//任何一个Task完成,Task就完成
Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks);//所有Task完成,Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。
FromResult() 创建普通数值的Task对象。
统计一个文件中的所有文件的字符数量总和
static async Task Main(string[] args)
{
string[] files=Directory.GetFiles(@"C:\Users\14095\Desktop\大三考试");
Task<int>[] tasks= new Task<int>[files.Length];//创建length个任务.
int cnt = 0;
foreach (var item in files)
{
Task<int> t=ReadCharactorCount(item);//逐个启动任务.
tasks[cnt++] = t;//赋值给任务数组.
}
int[] arr=await Task.WhenAll(tasks);//等待完成.
int sum = arr.Sum();
}
static async Task<int> ReadCharactorCount(string file)
{
string c=await File.ReadAllTextAsync(file);
return c.Length;
}
接口中的异步方法
async用于提示编译器为异步方法中的await代码进行分段处理,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async.
关于yield它是一个流水线式的返回,其内部也是通过状态机实现的,与async类似.
在C#8.0之前的版本中,yield与async无法在一起共同使用.即yield也是语法糖.
8.0之后(包括8.0),提供了一个叫做IAsyncEnumberable
的返回使用,注意不要带Task.
static async Task Main(string[] args)
{
await foreach (var item in Fun1())
{
Console.WriteLine(item);
}
}
static async IAsyncEnumerable<string> Fun1()
{
yield return "a";
yield return "f";
yield return "d";
}