1 异步编程
async/await 语法糖
写完代码编译后, 使用 ILSpy/DotPeek 查看 dll 反编译后的代码, 查看这几行代码微软实际是如何展开的(会生成一个类, 使用了状态机的设计模式)
using (HttpClient client = new HttpClient())
{
string html = await client.GetStringAsync("http://www.baidu.com");
Console.WriteLine(html);
}
var txt = "HELLO WORLD";
var fileName = @"D:\1.txt";
await File.WriteAllTextAsync(fileName, txt);
Console.WriteLine("写入成功");
string s = await File.ReadAllTextAsync(fileName);
Console.WriteLine($"文件内容: {s}");
线程切换
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
var sb = new StringBuilder();
for (int i = 0; i < 10; i++)
{
sb.Append("XXXXXXXXXXXXXXXXXXXX");
}
await File.WriteAllTextAsync("D:\\test.txt", sb.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await 调用的等待期间, .NET 会把当前线程返回给线程池, 等异步方法调用执行完成后, 框架会从线程池中再取一个线程执行后续的代码, 有时 await 等待时间过短, 还是会使用相同线程来继续执行后续代码
线程切换是不好的, 优化时, 需要想办法来避免线程切换的问题
异步 ≠ 多线程
异步方法的代码并不会自动在新线程中执行, 除非把代码放到新线程中执行(如: Task.Run()方法)
Console.WriteLine($"ThreadId开始: {Thread.CurrentThread.ManagedThreadId}");
var a = await CalcAsync(50000);
Console.WriteLine($"a={a}");
Console.WriteLine($"ThreadId结束: {Thread.CurrentThread.ManagedThreadId}");
async Task<double> CalcAsync(int n)
{
/*
// 这样子是不会产生新线程的
Console.WriteLine($"CalcAsync-ThreadId: {Thread.CurrentThread.ManagedThreadId}");
double result = 0;
var rand = new Random();
for (int i = 0; i < n * n; i++)
{
result += rand.NextDouble();
}
return result;
*/
// 通过 Task.Run 启用一个新线程来执行代码
return await Task.Run(() =>
{
Console.WriteLine($"CalcAsync-ThreadId: {Thread.CurrentThread.ManagedThreadId}");
double result = 0;
var rand = new Random();
for (int i = 0; i < n * n; i++)
{
result += rand.NextDouble();
}
return result;
});
}
为什么有的异步没有 async?
var res = await ReadAsync(0);
Console.WriteLine(res);
// 正常的异步写法
/*async Task<string> ReadAsync(int num)
{
if (num == 0)
{
return await File.ReadAllTextAsync("D:\\i.txt");
}
return "NONE";
}*/
// 直接返回 Task<string> 类型, 当作普通方法调用, 运行效率更高, 不会造成线程浪费
Task<string> ReadAsync(int num)
{
if (num == 0)
{
return File.ReadAllTextAsync("D:\\i.txt");
}
return Task.Run(() => "NONE");
}
async 方法缺点
- 异步方法会生成一个类, 运行效率没有普通方法高
- 可能会占用非常多的线程, 涉及到线程调度和切换
async 的相关优化
- 可以直接返回 Task<string> 类型, 当作普通方法调用(反编译后的代码也不会生成一个类, 而是一个方法调用), 运行效率更高, 不会造成线程浪费
async 细节
- 方法返回 Task<T> 不一定都要标 async, 标注 async 只是让我们更方便使用 await 而已
- 如果异步方法只是对别的异步方法调用的转发, 并没有太多复杂的逻辑, 那么就可以去掉 async 关键字, 直接返回别的异步方法调用即可
不要用 Thread.Sleep(), 而是用 await Task.Delay()
await File.ReadAllTextAsync("D:\\i.txt");
// 会阻塞线程
// Thread.Sleep(3000);
// 不会阻塞线程
await Task.Delay(3000);
await File.ReadAllTextAsync("D:\\i.txt");
CancellationToken的使用
// 没有 CancellationToken 的异步方法, 该怎么处理还是怎么处理, 没法中途中断并处理相应的逻辑
async Task Download1Async(string url, int n)
{
using var client = new HttpClient();
for(var i = 0; i < n; i++)
{
var html = await client.GetAsync(url);
var htmlString = await html.Content.ReadAsStringAsync();
Console.WriteLine($"{DateTime.Now}: {htmlString[..50]}");
}
}
await Download1Async("http://www.baidu.com", 100);
// 带有 CancellationToken 的异步方法
async Task Download2Async(string url, int n, CancellationToken cancellationToken)
{
using var client = new HttpClient();
for(var i = 0; i < n; i++)
{
// 将 cancellationToken 传到相关方法参数中, 在出现取消时, 会抛出相关的异常, 需要相关的捕获异常处理
var html = await client.GetAsync(url, cancellationToken);
var htmlString = await html.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"{DateTime.Now}: {htmlString[..50]}");
// 1. 正常的处理中止处理
// if (cancellationToken.IsCancellationRequested)
// {
// Console.WriteLine($"请求被取消了!");
// break;
// }
// 2. 使用抛异常的方式
// cancellationToken.ThrowIfCancellationRequested();
}
}
var cts = new CancellationTokenSource();
// 1. 设置几秒后中止请求
// cts.CancelAfter(1000 * 5);
//await Download2Async("http://www.baidu.com", 100, cts.Token);
// 2. 也可以设置通过输入来处理中止请求
Download2Async("http://www.baidu.com", 100, cts.Token);
while (Console.ReadLine() != "q")
{
}
await cts.CancelAsync();
Console.ReadLine();
可以用于ASP.NET接口
当请求从客户端取消时, 通过 cancellationToken(通过方法注入来得到这个token) 来实现中止业务逻辑的处理, 以避免资源浪费
app.MapGet("/testCancellation", async (CancellationToken cancellationToken) =>
{
try
{
await Download2Async("http://www.baidu.com", 10000, cancellationToken);
}
catch (TaskCanceledException e)
{
Console.WriteLine("操作被取消了");
}
return Results.Ok();
});
WhenAll()
// 从指定目录获取所有文件, 读取文件中字符数量, 统计总和
var files = Directory.GetFiles("D:\");
var tasks = new List<Task<int>>();
foreach (var file in files)
{
tasks.Add(ReadCharsCount(file));
}
// 使用 WhenAll 等待所有 Task 完成后返回
var counts = await Task.WhenAll(tasks);
var sum = counts.Sum();
Console.WriteLine($"文件总字符个数: {sum}");
async Task<int> ReadCharsCount(string fileName)
{
return (await File.ReadAllTextAsync(fileName)).Length;
}
异步的其它问题
async 和 yield return
async IAsyncEnumerable<string> TestYield()
{
yield return "1";
yield return "2";
yield return "3";
}
await foreach (var str in TestYield())
{
Console.WriteLine(str);
}
返回值不需要加 Task, 直接使用 IAsyncEnumerable<T> 作为返回值
ConfigureAwait(false)
ASP.NET Core 和控制台项目中没有 SynchronizationContext 了, 所以不需要使用 ConfigureAwait(false)
No Comments