1 为什么用异步编程
餐馆点菜的方式有:1. 通过服务员点菜。服务员拿着菜单站在桌子旁边,你要什么她给你点什么
2.给你菜单,你自己点,她去服务别的顾客
这两种方式有哪些不同呢?方式1,服务员一次只能服务一桌顾客,其他顾客都得等着;方式2,服务员可以服务多桌顾客。其实这和服务器响应请求的模式很像。
方式1的处理方式就像下图,每次user(顾客)给服务器(服务员)发送一条请求(点菜),服务器再给处理程序发送请求,处理程序处理完成后告诉服务器,服务器再返回给user。
所以,使用异步编程可以增加服务器的响应数量。
2 异步编程的误区
-
异步编程可以加快单个请求的处理速度
还是以点餐为例。方式1和方式2相比,方式2可以减少顾客等待的时间吗?很显然,不能。因为等待时间取决于后厨做菜所花费的时间,虽然和点餐花费时间也有关系,但是并不是由点餐花费时间决定的。所以,异步编程不可以加快单个请求的处理速度,它只是能够让web服务器处理更多的请求。 -
异步编程是多线程
在c#中,可以用async await进行调用异步方法进行多线程编程,但是异步编程不是多线程。
3 async await 基本使用
异步方法:用关键字async修饰的方法
- 异步方法的返回值一般是Task,T是真正的返回类型,例如Task。异步方法一般以async结尾,例如 GetAsync()
- 即使异步方法没有返回值,也最好把返回值类型声明为非范型的Task
public void Get() {}
public async Task Get(){} // get的异步方法 没有返回值,返回类型为task
- 调用异步方法时,一般在方法前加上await关键字,这样拿到的返回值就是范型指定的T类型
- 异步方法的传染性:一个方法中如果有await调用,则这个方法必须用async修饰
现在给目录下写一个txt,然后将写入内容再次读出来。
static void Main(string[] args)
{
// 同步方法
string path = @"~Documents\学习\1.txt";
File.WriteAllText(path, "hello");
string content = File.ReadAllText(path);
Console.WriteLine(content);
Console.ReadKey();
}
现在来用异步方法
static void Main(string[] args)
{
// 异步方法
string path = @"~Documents\学习\1.txt";
await File.WriteAllTextAsync(path, "hello"); // 用await调用异步方法
string content = File.ReadAllText(path);
Console.WriteLine(content);
Console.ReadKey();
}
我们的main方法没有加async关键字,会报错。其实在用await时,vs会自动帮我们给方法加上async关键字,这里是为了演示错误,我故意去掉了。
正确写法
static async Task Main(string[] args) // 方法中有await时,方法必须加上async关键字;方法没有返回值时,返回值类型用task
{
// 异步方法
string path = @"~Documents\学习\1.txt";
await File.WriteAllTextAsync(path, "hello"); // 用await调用异步方法
//ReadAllTextAsync的返回值类型其实是Task<string>,
//但是因为用await调用异步方法可以直接得到T类型的实际类型,所以这里可以直接用stirng来接收返回值
string content = await File.ReadAllTextAsync(path);
Console.WriteLine(content);
Console.ReadKey();
}
当一个方法既有同步方法又有异步方法时,我们推荐使用异步方法,因为异步方法可以提高系统的并发量。.net core中,有些方法就只有异步方法了,微软去掉了它的同步方法。
4. 编写异步方法
1.读取网页内容,并把它写入到文件中
- 无返回值
// 调用自己写的异步方法
static async Task Main(string[] args) // 方法中有await时,方法必须加上async关键字;方法没有返回值时,返回值类型用task
{
// 异步方法
string path = @"~Documents\学习\1.txt";
string url = "https://www.baidu.com/";
await DownloadHtml(url, path);
Console.ReadKey();
}
public static async Task DownloadHtml(string url, string path)
{
using (HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(path, html);
}
}
- 有返回值
异步方法的返回值类型是Task,但是我们在方法中可以直接返回int类型,因为它会自动转换成Task类型,在program中,我们可以直接打印,因为程序会自动将Task里面的int取出来。
public static async Task<int> DownloadHtml(string url, string path)
{
using (HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(path, html);
return html.Length;
}
}
static async Task Main(string[] args) // 方法中有await时,方法必须加上async关键字;方法没有返回值时,返回值类型用task
{
// 异步方法
string path = @"~Documents\学习\1.txt";
string url = "https://www.baidu.com/";
//await DownloadHtml(url, path);
Console.WriteLine(await DownloadHtml(url, path));
Console.ReadKey();
}
2.如果方法不支持加关键字async,但是又必须调用异步方法,那该怎么办?
- 异步方法有返回值 用.Result
//假如main方法不支持添加async关键字,但是又必须调用异步方法
static void Main(string[] args)
{
// 异步方法
string path = @"~Documents\学习\1.txt";
string s = File.ReadAllTextAsync(path).Result; // 不用await关键字 直接调用,然后用.result得到结果
Console.WriteLine(s);
Console.ReadKey();
}
- 异步方法没有返回值 用.wait()
//假如main方法不支持添加async关键字,但是又必须调用异步方法
// 1. 有返回值 用result
//2. 无返回值 用wait
static void Main(string[] args)
{
// 异步方法
string path = @"~Documents\学习\1.txt";
File.WriteAllTextAsync(path, "hahahhahahahah").Wait(); // 无返回值
string s = File.ReadAllTextAsync(path).Result; // 不用await关键字 直接调用,然后用.result得到结果
Console.WriteLine(s);
Console.ReadKey();
}
但是尽量不要用这两种方法,因为会有死锁的风险。.netcore中,大部分都支持异步方法,所以不到逼不得已,不要使用result wait。
3. 异步委托
如果委托中用到了异步方法,则需要在lamda表达式前面加上async关键字。
//异步委托
static async Task Main(string[] args)
{
// 异步方法
string path = @"~Documents\学习\1.txt";
ThreadPool.QueueUserWorkItem(async (o) =>
{
await File.WriteAllTextAsync(path, "ttttttt");
});
Console.ReadKey();
}
5 await async原理
用ILSpy反编译dll成4.0版本,就能看见荣利利己的的底层IL代码。
await async是“语法糖”,最终变异成“状态机”调用
总结:添加async关键字的方法会被c#编译器编译成一个类,会根据await调用进行切分为多个状态,对async的调用会被拆分为对MoveNext的调用。
用await看似等待,经过反编译后,其实没有等待。
6. ASYNC背后的线程切换
await调用的等待期间,.net会把当前的线程返回给线程池,等异步方法调用执行完成后,框架会从线程池再取出来一个线程执行后续的代码。
static async Task Main(string[] args)
{
Console.WriteLine($"调用异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
string path = @"~Documents\学习\1.txt";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append("hahahhahhhhhhhhhhhhhhaaaaaaaa");
}
string content = sb.ToString();
await File.WriteAllTextAsync(path, content);
Console.WriteLine($"调用异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
这里有个小细节,如果写入的非常快,线程有可能是不切换的,例:
static async Task Main(string[] args)
{
Console.WriteLine($"调用异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
string path = @"~Documents\学习\1.txt";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++)
{
sb.Append("hahahhahhhhhhhhhhhhhhaaaaaaaa");
}
string content = sb.ToString();
await File.WriteAllTextAsync(path, content);
Console.WriteLine($"调用异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
这是CLR的优化。要等待时,如果发现程序已经执行完了,就没必要切换线程了,剩下的代码就在之前的线程上继续执行就可以了。
我们自己来写一个异步方法,看一下背后的线程切换
static async Task Main(string[] args)
{
Console.WriteLine($"调用自己写的异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
await CalcAsync(5000);
Console.WriteLine($"调用自己写的异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
public static async Task<double> CalcAsync(int a)
{
double result = 0;
// 这样写,并不户自动切换线程,之后把代码放到task.run中才会切换线程
Random random = new Random();
for (int i = 0; i < a* a; i++)
{
result += random.NextDouble();
}
return result;
}
我们可以看到线程并没有切换。这是因为异步编程并不会自动切换线程,如果想切换线程,必须手动将代码放到Task中去执行。
public static async Task<double> CalcAsync(int a)
{
double result = 0;
await Task.Run(() =>
{
Random random = new Random();
for (int i = 0; i < a* a; i++)
{
result += random.NextDouble();
}
});
return result;
}
static async Task Main(string[] args)
{
Console.WriteLine($"调用自己写的异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
await CalcAsync(5000);
Console.WriteLine($"调用自己写的异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
所以才说,异步编程不是多线程。它不会自动切换,而是需要我们手动将代码放到task中才可以。那为什么我们调用其他写好的异步方法时会切换呢?那是因为它内部已经放到了task中。
7 为什么有的异步方法没有标async
如果一个异步方法只是对其他异步方法的调用,没有复杂的逻辑,那么就可以去掉async。以上面自己写的异步方法为例
// 不写await async
public static Task<double> CalcAsync2(int a)
{
double result = 0;
Task.Run(() =>
{
Random random = new Random();
for (int i = 0; i < a * a; i++)
{
result += random.NextDouble();
}
});
return Task.FromResult(result);
}
static async Task Main(string[] args)
{
Console.WriteLine($"调用自己写的异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
await CalcAsync2(5000);
Console.WriteLine($"调用自己写的异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
8 不要用sleep
如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用await task.delay()。
9 CancellationToken
有时需要提前终止任务,比如:请求超时,用户取消请求
很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号
CancellationToken结构体
None:空
bool IsCancellationRequested 是否取消
(*)Register(Action callback)注册取消监听 ----- 很少用
ThrowIfCancellationRequested() 如果任务被取消,执行到这里就抛异常
我们一般通过CancellationTokenSource来创建CancellationToken对象,不直接new
CancellationTokenSource
CancelAfter() 超时后发出取消信号
Cancel() 发出取消信号
CancellationToken Token
下面来做一个案例。
为“下载一个网址n次”的方法增加取消功能。分别用GetStringAsync + IsCancellationRequested 、GetStringAsync + ThrowIfCancellationRequested 、带CancellationToken的GeAsync()分别实现。取消分别用户超时、用户敲键盘(不能await)实现
先写一个普通下载
static async Task Main(string[] args)
{
await Download1("https://www.youzack.com", 2000);
Console.Read();
}
// 下载一个网站 普通下载
public static async Task Download1(string url, int n)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
Console.WriteLine(html);
}
}
}
再写一个用GetStringAsync + IsCancellationRequested方式响应用户取消请求的:
static async Task Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
source.CancelAfter(5000); // 5s后取消请求
await Download1("https://www.youzack.com", 2000, source.Token);
Console.Read();
}
// 下载一个网站 响应用户取消请求
public static async Task Download2(string url, int n,CancellationToken token)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
if (token.IsCancellationRequested)
{
Console.WriteLine("用户取消请求");
}
Console.WriteLine(html);
}
}
}
这里有个需要注意的点:我们设置5s后停止请求,但是如果5s内下载操作已经完成,那么程序会正常终止,不会再打印“用户取消请求”这条信息。这一点可以通过将2000改为20验证。
再写一个用GetStringAsync + ThrowIfCancellationRequested方式响应用户取消请求的:
public static async Task Download2(string url, int n, CancellationToken token)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
token.ThrowIfCancellationRequested();
Console.WriteLine(html);
}
}
}
这样写,程序会通过报错的方式来终止操作。
再写一个通过CancellationToken的GeAsync()方式响应用户取消请求的:
public static async Task Download3(string url, int n, CancellationToken token)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
var responseMessage = await client.GetAsync(url, token);
string html = await responseMessage.Content.ReadAsStringAsync();
Console.WriteLine(html);
}
}
}
这个也是通过报异常的方式终止操作。
ASP.NET Core开发中,一般不需要自己处理CancellationToken、CancellationTokenSource这些,知道做到“能转发CancellationToken就转发”即可。ASP.NET Core会对用户请求中断进行处理。
10 异步编程 WhenAll
task类的重要方法:
- TaskWhenAny(IEnumerable tasks) 任何一个task完成,task就完成
- Task<TResult[]>WhenAll<TResult[]>(params Task<TResult[]> tasks) 所有task完成,task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。
- FromResult() 创建普通数值的Task对象
有3个txt,分别读取它们,然后等全部读取完毕再打印它们的内容。
static async Task Main(string[] args)
{
// 这里没有使用await,所以不会等1读取完再去读取2,等2读取完再去读取3
Task<string> s1 = File.ReadAllTextAsync(@"1.txt");
Task<string> s2 = File.ReadAllTextAsync(@"2.txt");
Task<string> s3 = File.ReadAllTextAsync(@"3.txt");
// 这里用whenall,等3个都读取完毕再打印
string[] strs = await Task.WhenAll(s1, s2, s3);
Console.WriteLine(strs[0] + " " + strs[1] + " " + strs[2]);
Console.ReadKey();
}
11 异步的其他问题
1. 接口中的异步方法
async是为了提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来说没有区别。因此对于接口中的方法或抽象方法不能修饰为async。如果方法需要异步,可以在它的继承类中写async。
如:
给接口加上async关键字会报错
public interface AsyncInterface
{
async Task<int> GetFileCount();
}
给实现类中加async不报错
public class TestClass : AsyncInterface
{
public async Task<int> GetFileCount()
{
string path = @"~Documents\学习\1.txt";
string content = await File.ReadAllTextAsync(path);
return content.Length;
}
}
2. 异步与yield
yield不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提高性能。
在旧版本c#中,async方法中不能用yield。从c#8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach即可。