11-C#

C#.Net-多线程-Async-Await篇-学习笔记

一、async/await 基础

1.1 什么是async/await

定义

  • C# 5.0 (.NET 4.5) 引入的语法糖
  • C# 7.1 开始,Main入口也可以使用
  • C# 8.0 支持异步流(await foreach)和异步释放(await using)

什么是语法糖

  • 由编译器提供的便捷功能
  • 底层实现不变,但写代码更简洁
  • 类似的语法糖:var、表达式树、属性的get/set、字符串插值$""

1.2 基本用法

规则

csharp 复制代码
// 1. async单独使用会警告,没有实际作用
public async void Method1() // 警告
{
    Console.WriteLine("没有await");
}

// 2. await必须在async方法内使用
public void Method2()
{
    await Task.Delay(1000); // 编译错误
}

// 3. async + await 配套使用
public async Task Method3()
{
    await Task.Delay(1000); // 正确
}

返回值规则

csharp 复制代码
// 无返回值:返回Task(推荐)或void
public async Task NoReturnAsync()
{
    await Task.Delay(1000);
    // 默认返回Task
}

public async void NoReturnVoid()
{
    await Task.Delay(1000);
    // 不推荐,无法await或Wait
}

// 有返回值:返回Task<T>
public async Task<int> GetValueAsync()
{
    await Task.Delay(1000);
    return 123; // 实际返回Task<int>
}

为什么推荐Task而不是void

  • Task可以使用await、Wait、WhenAny、WhenAll等方法组合
  • void无法组合使用
  • Task可以捕获异常,void不行

二、async/await的核心价值

2.1 解决的问题

问题:既要有顺序,又要不阻塞

csharp 复制代码
// 同步方法:有顺序,但阻塞
public void SyncMethod()
{
    Console.WriteLine("步骤1");
    Thread.Sleep(1000); // 阻塞
    Console.WriteLine("步骤2");
    Thread.Sleep(1000); // 阻塞
    Console.WriteLine("步骤3");
}

// 多线程:不阻塞,但无顺序
public void TaskMethod()
{
    Task.Run(() =>
    {
        Console.WriteLine("步骤1");
        Thread.Sleep(1000);
    });
    Task.Run(() =>
    {
        Console.WriteLine("步骤2");
        Thread.Sleep(1000);
    });
    Task.Run(() =>
    {
        Console.WriteLine("步骤3");
        Thread.Sleep(1000);
    });
    // 输出顺序不确定
}

// async/await:有顺序,不阻塞
public async Task AsyncMethod()
{
    await Task.Run(() =>
    {
        Console.WriteLine("步骤1");
        Thread.Sleep(1000);
    });
    await Task.Run(() =>
    {
        Console.WriteLine("步骤2");
        Thread.Sleep(1000);
    });
    await Task.Run(() =>
    {
        Console.WriteLine("步骤3");
        Thread.Sleep(1000);
    });
    // 按顺序输出:步骤1 -> 步骤2 -> 步骤3
}

2.2 核心理念

以同步编程的方式来写异步代码

  • 代码看起来像同步,实际是异步执行
  • 降低编程难度
  • 保持代码可读性

三、await的执行机制

3.1 执行流程

csharp 复制代码
public async Task TestAsync()
{
    Console.WriteLine($"主线程ID: {Thread.CurrentThread.ManagedThreadId}");
    
    // 1. 遇到await,主线程立即返回(不阻塞)
    await Task.Run(() =>
    {
        Console.WriteLine($"子线程ID: {Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(3000);
        Console.WriteLine("子线程完成");
    });
    
    // 2. await后的代码,由线程池的线程执行
    Console.WriteLine($"await后线程ID: {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine("继续执行");
}

关键点

  1. 主线程遇到 await 就返回,不等待(非阻塞)
  2. await 后的代码会在 Task 完成后继续执行
  3. await 后的代码可能由子线程、主线程或其他线程执行

一个典型的执行顺序示例,能直观说明 await 的非阻塞特性:

csharp 复制代码
public static void Show()
{
    Console.WriteLine($"start  线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ① 主线程
    Async();  // 调用异步方法
    Console.WriteLine($"aaa    线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ③ 主线程继续,不等待
}

public static async void Async()
{
    Console.WriteLine($"ddd    线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ② 还是主线程
    await Task.Run(() =>
    {
        Thread.Sleep(500);
        Console.WriteLine($"bbb    线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ④ 子线程
    });
    Console.WriteLine($"ccc    线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ⑤ await后,线程池线程
}

实际输出顺序:start → ddd → aaa → bbb → ccc

⚠️ 注意 aaabbb 之前输出------主线程遇到 await 后立即返回继续执行,子线程的工作和主线程是并发的。

3.2 与ContinueWith的对比

async/await 出现之前,控制多个异步操作的顺序只能靠 ContinueWith 链式回调,代码可读性很差:

csharp 复制代码
// 老写法:ContinueWith 链式调用控制顺序,嵌套深、难维护
Task<int> task = taskFactory.StartNew<int>(() =>
{
    Thread.Sleep(3000);
    return 123;
}).ContinueWith(c =>
{
    Thread.Sleep(3000);
    return 234;
}).ContinueWith(t =>
{
    Thread.Sleep(3000);
    return 345;
});

用 await 改写后,代码结构和同步方法一样直观:

csharp 复制代码
// 新写法:await 顺序执行,清晰易读
public async Task UseAwait()
{
    await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine("步骤1"); });
    await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine("步骤2"); });
    await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine("步骤3"); });
}

await 的优势

  • 代码更简洁,可读性更好
  • 写法像同步代码,实际是异步执行
  • 自动处理异常传播,不需要手动在每个 ContinueWith 里 try-catch

四、带返回值的async方法

4.1 基本用法

csharp 复制代码
public async Task<long> SumAsync()
{
    Console.WriteLine($"开始计算,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    
    long result = 0;
    
    // 第一个计算
    await Task.Run(() =>
    {
        Console.WriteLine($"计算1,线程ID: {Thread.CurrentThread.ManagedThreadId}");
        for (long i = 0; i < 999_999_999; i++)
        {
            result += i;
        }
    });
    
    Console.WriteLine($"计算1完成,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    
    // 第二个计算
    await Task.Run(() =>
    {
        Console.WriteLine($"计算2,线程ID: {Thread.CurrentThread.ManagedThreadId}");
        for (long i = 0; i < 999_999_999; i++)
        {
            result += i;
        }
    });
    
    Console.WriteLine($"计算2完成,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    
    return result; // 自动包装成Task<long>
}

4.2 调用方式

csharp 复制代码
// 方式1:await(推荐,不阻塞)
public async Task CallAsync()
{
    long result = await SumAsync();
    Console.WriteLine($"结果: {result}");
}

// 方式2:Result(不推荐,阻塞)
public void CallSync()
{
    long result = SumAsync().Result; // 阻塞,相当于同步
    Console.WriteLine($"结果: {result}");
}

// 方式3:Wait(不推荐,阻塞)
public void CallWait()
{
    Task<long> task = SumAsync();
    task.Wait(); // 阻塞
    long result = task.Result;
    Console.WriteLine($"结果: {result}");
}

注意

  • 访问Result或Wait会阻塞,失去async/await的意义
  • 推荐使用await,保持异步特性

五、async/await的底层实现

5.1 状态机

IL代码分析

csharp 复制代码
// C#代码
public async Task SimpleMethod()
{
    await Task.Delay(1000);
    Console.WriteLine("完成");
}

// 编译后生成状态机(简化版)
private struct StateMachine
{
    public int state;
    public AsyncTaskMethodBuilder builder;
    
    public void MoveNext()
    {
        try
        {
            switch (state)
            {
                case 0:
                    // 执行await之前的代码
                    var awaiter = Task.Delay(1000).GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        state = 1;
                        // 等待完成
                        return;
                    }
                    goto case 1;
                    
                case 1:
                    // 执行await之后的代码
                    Console.WriteLine("完成");
                    state = -2;
                    break;
            }
        }
        catch (Exception ex)
        {
            state = -2;
            builder.SetException(ex);
        }
        
        builder.SetResult();
    }
}

状态机执行流程

  1. 实例化状态机
  2. 将状态机交给builder执行
  3. 整理线程上下文
  4. 调用MoveNext()方法
  5. 根据状态执行不同分支
  6. 异常时状态重置为-2
  7. 完成时调用SetResult()

5.2 状态机的价值

类比:红绿灯

  • 红灯:停止
  • 绿灯:行驶
  • 黄灯:减速

状态机

  • 一个对象在不同状态下执行不同行为
  • await将方法分割成多个状态
  • 每个状态对应一个await

六、性能对比

6.1 文件读取对比

csharp 复制代码
// 1. Async版本(推荐)
private async Task<byte[]> ReadAsync(string path)
{
    Console.WriteLine($"ReadAsync开始,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    
    var result = await File.ReadAllBytesAsync(path);
    // 读取文件时,没有开启新线程
    // 主线程告诉系统要做什么,然后就返回了
    // 降低了线程开启数量,降低了CPU负荷
    
    Console.WriteLine($"ReadAsync结束,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    return result;
}

// 2. Task版本
private Task<byte[]> ReadTask(string path)
{
    Console.WriteLine($"ReadTask开始,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    
    var result = Task.Run(() =>
    {
        Console.WriteLine($"ReadTask执行,线程ID: {Thread.CurrentThread.ManagedThreadId}");
        return File.ReadAllBytes(path);
    });
    // 铁定开启新线程
    
    Console.WriteLine($"ReadTask结束,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    return result;
}

// 3. Sync版本
private byte[] ReadSync(string path)
{
    Console.WriteLine($"ReadSync开始,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    var result = File.ReadAllBytes(path);
    Console.WriteLine($"ReadSync结束,线程ID: {Thread.CurrentThread.ManagedThreadId}");
    return result;
}

性能测试结果

复制代码
20次读取大文件:
- Async: 约3000ms
- Task:  约3500ms
- Sync:  约6000ms

6.2 Web请求对比

csharp 复制代码
// 1. Async版本(推荐)
private async Task<string> WebAsync(string url)
{
    HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
    
    // 使用异步版本的API
    using (HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse)
    {
        StreamReader sr = new StreamReader(response.GetResponseStream());
        return await sr.ReadToEndAsync(); // 异步读取
    }
}

// 2. Task版本
private Task<string> WebTask(string url)
{
    return Task.Run(() =>
    {
        return InvokeWebRequest(url); // 同步方法包装
    });
}

// 3. Sync版本
private string WebSync(string url)
{
    return InvokeWebRequest(url);
}

性能测试结果

复制代码
10次Web请求(每次5秒):
- Async: 约5秒(并发)
- Task:  约5秒(并发)
- Sync:  约50秒(串行)

6.3 CPU密集型计算对比

csharp 复制代码
// 1. Async版本
private async Task<long> CalculationAsync(long total)
{
    return await Task.Run(() =>
    {
        long result = 0;
        for (long i = 0; i < total; i++)
        {
            result += i;
        }
        return result;
    });
}

// 2. Task版本
private Task<long> CalculationTask(long total)
{
    return Task.Run(() =>
    {
        long result = 0;
        for (long i = 0; i < total; i++)
        {
            result += i;
        }
        return result;
    });
}

// 3. Sync版本
private long CalculationSync(long total)
{
    long result = 0;
    for (long i = 0; i < total; i++)
    {
        result += i;
    }
    return result;
}

性能测试结果

复制代码
10次计算(每次10亿次循环):
- Task:  约8秒
- Async: 约8秒
- Sync:  约80秒

结论

  • CPU密集型:Async和Task性能相同,都需要开启线程
  • IO密集型:Async性能更好,不需要额外线程

七、适用场景

7.1 适合使用async/await的场景

1. IO密集型操作(推荐)

csharp 复制代码
// 文件操作
await File.ReadAllBytesAsync(path);
await File.WriteAllTextAsync(path, content);

// 数据库操作
await connection.OpenAsync();
await command.ExecuteNonQueryAsync();

// Web请求
await httpClient.GetStringAsync(url);

// Redis操作
await redis.StringGetAsync(key);

2. 与第三方交互(非托管资源)

  • 数据库查询
  • Redis缓存
  • Web API调用
  • 文件读写

7.2 不适合使用async/await的场景

1. CPU密集型计算(不推荐)

csharp 复制代码
// 不推荐:async/await对CPU密集型无优势
public async Task<long> CalculateAsync()
{
    return await Task.Run(() =>
    {
        // 大量计算
        long result = 0;
        for (long i = 0; i < 1_000_000_000; i++)
        {
            result += i;
        }
        return result;
    });
}

// 推荐:直接使用Task
public Task<long> CalculateTask()
{
    return Task.Run(() =>
    {
        long result = 0;
        for (long i = 0; i < 1_000_000_000; i++)
        {
            result += i;
        }
        return result;
    });
}

2. 本地计算(托管资源)

  • 内存中的数据处理
  • 算法计算
  • 数据转换

八、不同框架中的应用

8.1 ASP.NET Core(推荐使用)

csharp 复制代码
[HttpGet]
public async Task<IActionResult> GetDataAsync()
{
    var data = await _service.GetDataAsync();
    return Ok(data);
}

优势

  • 不阻塞线程池线程
  • 提高服务器并发能力
  • 可以处理更多请求

8.2 控制台应用(可以使用)

csharp 复制代码
// C# 7.1+
static async Task Main(string[] args)
{
    await DoWorkAsync();
}

8.3 WinForms(需要注意)

csharp 复制代码
private async void btnClick_Click(object sender, EventArgs e)
{
    // 不阻塞UI线程
    var result = await GetDataAsync();
    
    // await后的代码在UI线程执行,可以直接更新UI
    lblResult.Text = result;
}

注意事项

  • 事件处理器可以使用async void
  • await后的代码会回到UI线程
  • 可以直接更新UI控件

8.4 WPF(类似WinForms)

csharp 复制代码
private async void Button_Click(object sender, RoutedEventArgs e)
{
    var result = await GetDataAsync();
    TextBlock.Text = result;
}

九、C# 8.0 新特性

9.1 异步流(await foreach)

csharp 复制代码
// 生成异步序列
public async IAsyncEnumerable<int> GenerateSequence()
{
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

// 消费异步序列
private async Task ConsumeAsync()
{
    await foreach (var i in GenerateSequence())
    {
        Console.WriteLine($"接收到: {i}");
    }
}

9.2 异步释放(await using)

csharp 复制代码
await using (var resource = new AsyncDisposableResource())
{
    // 使用资源
}
// 自动调用DisposeAsync()

十、最佳实践

10.1 命名规范

csharp 复制代码
// 异步方法以Async结尾
public async Task<string> GetDataAsync()
{
    return await _repository.QueryAsync();
}

10.2 避免async void

csharp 复制代码
// 错误:无法捕获异常,无法await
public async void BadMethodAsync()
{
    await Task.Delay(1000);
}

// 正确:返回Task
public async Task GoodMethodAsync()
{
    await Task.Delay(1000);
}

// 例外:事件处理器可以使用async void
private async void Button_Click(object sender, EventArgs e)
{
    await DoWorkAsync();
}

10.3 避免阻塞

csharp 复制代码
// 错误:阻塞
var result = GetDataAsync().Result;

// 错误:阻塞
GetDataAsync().Wait();

// 正确:异步等待
var result = await GetDataAsync();

10.4 ConfigureAwait

csharp 复制代码
// 库代码中使用,避免捕获上下文
var result = await GetDataAsync().ConfigureAwait(false);

// UI代码中不使用,需要回到UI线程
var result = await GetDataAsync();

十一、小结

  • async/await 是语法糖:底层是状态机实现
  • 核心价值:以同步方式写异步,降低编程难度
  • 适用场景:IO 密集型操作(文件、网络、数据库)
  • 不适用场景:CPU 密集型计算
  • 性能提升:提高吞吐量,不是降低单个请求时间
  • 避免阻塞:不要使用 Result 或 Wait
  • 命名规范:异步方法以 Async 结尾
  • 返回类型:优先使用 Task,避免 void
相关推荐
xushichao19892 小时前
C++中的享元模式
开发语言·c++·算法
fareast_mzh2 小时前
Mistral AI本地部署 C++无需Nvidiad独立显卡也能运行(CPU推理)
开发语言·c++·人工智能
Jackey_Song_Odd2 小时前
Part 1:Python语言核心 - Control Flow 控制流
开发语言·windows·python
m0_716667072 小时前
C++中的访问者模式高级应用
开发语言·c++·算法
大鹏说大话2 小时前
构建高并发缓存系统:架构设计、Redis策略与灾难防御
开发语言
Oueii2 小时前
C++中的访问者模式变体
开发语言·c++·算法
2401_838683372 小时前
单元测试在C++项目中的实践
开发语言·c++·算法
guygg882 小时前
基于Kaimal谱的风速时间序列生成MATLAB程序
开发语言·matlab
执行部之龙2 小时前
js手写——防抖
开发语言·前端·javascript