视频链接:.NET 6教程,.Net Core 2022视频教程,杨中科主讲_哔哩哔哩_bilibili
异步编程
为什么要异步编程?
几个误区:(1)异步不一定能提高效率(2)异步不是多线程
传统多线程异步开发太麻烦。
C#关键字:async、await。【async、await不等于“多线程”】
async、await的基本使用
“异步方法”:用async关键字修饰的方法
1)异步方法的返回值一般是Task
2)即使方法没有返回值,也最好把返回值声明为非泛型的Task。
3)调用泛型方法时,一般在方法前加上await关键字,这样拿到的返回值就是泛型指定的T类型
4)异步方法的“传染性”:一个方法中如果有await调用,则这个方法也必须修饰为async
示例:
// 同步
class Program {
static void Main(String[] args) {
string path = @"E:\Biao\MyProjects\test.txt";
File.WriteAllText(path, "hello");
string content = File.ReadAllText(path);
Console.WriteLine(content);
}
}
// 异步
class Program {
static async Task Main(String[] args) {
string path = @"E:\Biao\MyProjects\test.txt";
await File.WriteAllTextAsync(path, "Hello,你好");
string content = await File.ReadAllTextAsync(path);
Console.WriteLine(content);
}
}
编写异步方法
示例:编写有返回值的和无返回值的异步方法
class Program {
static async Task Main(String[] args) {
string url = "https://www.baidu.com";
string file = @"E:\Biao\MyProjects\test.txt";
int len = await DownLoadUriResult(url, file);
Console.WriteLine("ok " + len);
}
// 无返回值
static async Task DownLoadUrl(string url,string filename) {
using (HttpClient client = new HttpClient()) {
string content = await client.GetStringAsync(url);
await File.WriteAllTextAsync(filename, content);
}
}
// 有返回值
static async Task<int> DownLoadUriResult(string url, string file) {
using (HttpClient client = new HttpClient()) {
string content = await client.GetStringAsync(url);
// string content = client.GetStringAsync(url).Result; // 与上一句效果一下,但避免
await File.WriteAllTextAsync(file, content);
return content.Length;
}
}
}
如果同样的功能,既有同步方法,又有异步方法,那么首先使用异步方法。
对于不支持的异步方法怎么办? Wait((无返回值); Result(有返回值)。风险:死锁。【尽量不用】
异步委托
如果需要把一个异步方法放到Lambda表达式,那么这个Lambda表达式添加个async,变成异步Lambda表达式
ThreadPoo1. QueueUserWorkItem(async(obj) => {
await SomeAsync();
});
async、await原理揭秘
【ILSpy 反编译工具】
总结
**async **的方法会被C#编译器编译成一个类,会主要根据 await 调用进行切分为多个状态,对 async 方法的调用会被拆分为对MoveNext的调用。
用await看似是“等待”,经过编译后,其实没有“wait"
async背后的线程切换
await 调用的等待期间,**.NET **会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。
验证:
static async Task Main(String[] args) {
string file = @"E:\Biao\MyProjects\test.txt";
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.Append("xxxxxxxxxxxxxxxxxxxxxxx");
}
await File.WriteAllTextAsync(file, sb.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
得到结果1和4,说明调用异步方法前后线程变了。当然有可能结果相同,这取决线程池的调度。
异步方法不等于多线程
异步方法中的代码并不会自动在新线程中执行,除非把代码放到新线程(Task.Run())中执行。
static async Task Main(String[] args) {
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Test();// 耗时操作
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
static async Task Test() {
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
string file = @"E:\Biao\MyProjects\test.txt";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.Append("xxxxxxxxxxxxxxxxxxxxxxx");
}
await File.WriteAllTextAsync(file, sb.ToString());
}
结果显示:调用异步方法前的id和调用异步方法时的id都是1。说明了异步方法中不会自动在新线程中执行
为什么有的异步方法没标async?
对比:
static async Task Main(String[] args) {
string result = await WriteNoAsync(1);
Console.WriteLine(result);
}
// 正常的带有async的异步方法
static async Task<string> WriteAsync(int num) {
if (num == 1) {
string s = await File.ReadAllTextAsync(@"E:\Biao\MyProjects\test.txt");
return s;
} else if (num == 2) {
string s = await File.ReadAllTextAsync(@"E:\Biao\MyProjects\test2.txt");
return s;
} else {
throw new Exception();
}
}
// 不带有async的异步方法:Task<string> 直接就返回Task<string>类型的,没有问题。
static Task<string> WriteNoAsync(int num) {
if (num == 1) {
return File.ReadAllTextAsync(@"E:\Biao\MyProjects\test.txt");
} else if (num == 2) {
return File.ReadAllTextAsync(@"E:\Biao\MyProjects\test2.txt");
} else {
throw new Exception();
}
}
那么为什么要写不带有async的异步方法?
那么我们什么时候要使用async呢?
不要用Sleep()
如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程;
而要用** await Task.Delay()。**
举例:下载一个网址,3秒后下载另一个。【使用Sleep,界面会卡死,会降低并发】
CancellationToken结构体和CancellationTokenSource类
CancellationToken用来干什么?
有时需要提前终止任务,比如:请求超时、用户取消请求。
很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。
示例:使用不同方法
static async Task Main(String[] args) {
string url = "https://www.baidu.com";
int n = 100;
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(2000);
//await DownNUrlAsync(url, n);
//await DownNUrl2Async(url, n,cts.Token);
await DownNUrl3Async(url, n, cts.Token);
}
// 不设置多少秒自动取消,而是自己设置某些操作来取消。比如下列的按q键,就取消了
/*
static async Task Main(String[] args) {
string url = "https://www.baidu.com";
int n = 100;
CancellationTokenSource cts = new CancellationTokenSource();
DownNUrl2Async(url, n,cts.Token);
while (Console.ReadLine() != "q") {
}
cts.Cancel();
Console.ReadLine();
}
*/
// 版本1:不加超时操作
static async Task DownNUrlAsync(string url,int n) {
using(var client = new HttpClient()) {
for (int i = 0; i < n; i++) {
string s = await client.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now.ToString()}:" + s);
}
}
}
// 版本2:加超时操作
static async Task DownNUrl2Async(string url, int n,CancellationToken token) {
using (var client = new HttpClient()) {
for (int i = 0; i < n; i++) {
string s = await client.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now.ToString()}:" + s);
if(token.IsCancellationRequested) {
Console.WriteLine("请求被取消");
break;
}
// 不要if-break,直接使用throw,抛出异常。【不建议】
//token.ThrowIfCancellationRequested();
}
}
}
// 版本3:加超时操作 【调用自带CancellationToken的方法:GetAsync】 这样超时的后果是抛出一个异常
static async Task DownNUrl3Async(string url, int n, CancellationToken token) {
using (var client = new HttpClient()) {
for (int i = 0; i < n; i++) {
var resp = await client.GetAsync(url, token);
string s = await resp.Content.ReadAsStringAsync();
Console.WriteLine($"{DateTime.Now.ToString()}:" + s);
}
}
}
注意:
WhenAll
Task类的重要方法
(1)Task
(2)Task<TResult[]>.WhenAll
(3)FromResult() 创建普通数值的Task对象。
示例:
static async Task Main(String[] args) {
Task<string> str1 = File.ReadAllTextAsync(@"E:\Biao\MyProjects\test.txt");
Task<string> str2 = File.ReadAllTextAsync(@"E:\Biao\MyProjects\test2.txt");
Task<string> str3 = File.ReadAllTextAsync(@"E:\Biao\MyProjects\test3.txt");
string[] strs = await Task.WhenAll(str1, str2, str3);
string s1 = strs[0];
string s2 = strs[1];
string s3 = strs[2];
Console.WriteLine(s1);
Console.WriteLine(s2);
Console.WriteLine(s3);
}
案例:统计一个文件夹下,所有文件的char字符个数
static async Task Main(String[] args) {
//求一个文件夹下所有文件的字符数
string path = @"E:\Biao\MyProjects";
string[] fileNames = Directory.GetFiles(path);
Task<int>[] countTasks = new Task<int>[fileNames.Length];
for (int i = 0; i < fileNames.Length; i++) {
string filename = fileNames[i];
Task<int> ct = ReadFileCharCount(filename);
countTasks[i] = ct;
}
int[] count = await Task.WhenAll(countTasks);
int cs = count.Sum();
Console.WriteLine(cs);
}
// 求一个文件的字符个数
static async Task<int> ReadFileCharCount(string filename) {
string content = await File.ReadAllTextAsync(filename);
return content.Length;
}
异步其他问题
接口中的异步方法
async是提示编译器为异步方法中的;
await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async。
异步与yield
复习:yield return不仅能够简化数据的返回,而且可以让数据处理”流水线化“,提升性能。
示例:
Test2()执行顺序:yield return "11"——>Console.WriteLine(i)——>yield return "22"——>Console.WriteLine(i)——>yield return "33"——>Console.WriteLine(i)
static async Task Main(String[] args) {
foreach (var i in Test()) {
Console.WriteLine(i);
}
foreach (var i in Test2()) {
Console.WriteLine(i);
}
}
static IEnumerable<string> Test() {
List<string> list = new List<string>();
list.Add("11");
list.Add("22");
list.Add("33");
return list;
}
static IEnumerable<string> Test2() {
yield return "11";
yield return "22";
yield return "33";
}
注意:
static async Task Main(String[] args) {
await foreach (var i in Test3()) {
Console.WriteLine(i);
}
}
static async IAsyncEnumerable<string> Test3() {
yield return "11";
yield return "22";
yield return "33";
}
其他
ASP.NET Core和控制台项目中没有SynchronizationContext,因此不用管ConfigureAwait(false)等。
不要同步、异步混用。