Skip to main content

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)