C# 学习笔记四:异步编程(async/await)

老牛浏览 41评论 0发表于

目录

  1. 异步编程基础

  2. async/await 语法详解

  3. 任务操作(Task)

  4. 取消支持(CancellationToken)

  5. 异常处理

  6. 最佳实践与陷阱

  7. 实战场景

  8. 性能优化

  9. 速查表

1. 异步编程基础

1.1 什么是异步编程

异步编程:让程序在等待耗时操作(如I/O、网络请求)时,不阻塞当前线程,从而提高应用程序的响应性和吞吐量。

1.2 同步 vs 异步对比

csharp
// ❌ 同步方式:线程被阻塞
void SyncMethod()
{
    Console.WriteLine("开始");
    Thread.Sleep(3000);  // 主线程被阻塞3秒
    Console.WriteLine("结束");
}
// 输出:开始 →(等待3秒)→ 结束

// ✅ 异步方式:线程不被阻塞
async Task AsyncMethod()
{
    Console.WriteLine("开始");
    await Task.Delay(3000);  // 线程不被阻塞,可以处理其他任务
    Console.WriteLine("结束");
}
// 输出:开始 →(立即返回)→ 3秒后输出结束

1.3 异步编程的适用场景

场景

示例

网络请求

HTTP API调用、下载文件

数据库操作

查询、插入、更新

文件I/O

读写大文件

UI应用

避免界面卡顿

Web服务器

处理并发请求

2. async/await 语法详解

2.1 基本语法

csharp
// 异步方法的完整结构
[访问修饰符] async [Task|Task<T>|void] 方法名(参数列表)
{
    // 方法体
    await 异步操作;
    // 继续执行
}

// 示例
public async Task<string> FetchDataAsync(string url)
{
    using var client = new HttpClient();
    string result = await client.GetStringAsync(url);
    return result;
}

2.2 返回类型

返回类型

说明

使用场景

Task

无返回值

异步操作不返回结果

Task<T>

有返回值(T是返回类型)

异步操作返回结果

void

无返回值(特殊)

仅限事件处理器

ValueTask<T>

有返回值(性能优化)

高频调用、可能同步完成

csharp
// 1. Task:无返回值
async Task DoWorkAsync()
{
    await Task.Delay(1000);
    Console.WriteLine("完成");
}

// 2. Task<T>:有返回值
async Task<int> CalculateAsync()
{
    await Task.Delay(1000);
    return 42;
}

// 3. void:仅限事件处理器
async void Button_Click(object sender, EventArgs e)
{
    await LoadDataAsync();
}

// 4. ValueTask<T>:性能优化
async ValueTask<int> GetValueAsync()
{
    if (cached)
        return cachedValue;  // 同步返回,无堆内存分配
    return await FetchFromDatabaseAsync();
}

2.3 await 的工作原理

csharp
// 异步方法的执行流程
async Task ExampleAsync()
{
    Console.WriteLine("1. 方法开始(同步执行)");
    
    // 遇到await,线程返回调用者
    await Task.Delay(1000);
    
    // 异步操作完成后,继续执行(可能在原线程或新线程)
    Console.WriteLine("2. 异步操作完成后继续");
    
    // 多个await
    await Task.Delay(500);
    Console.WriteLine("3. 第二个异步操作完成");
}

3. 任务操作(Task)

3.1 创建任务

csharp
// 1. 从异步方法获取Task
Task task = DoWorkAsync();
Task<int> taskWithResult = GetValueAsync();

// 2. 手动创建任务
Task manualTask = Task.Run(() =>
{
    // 耗时操作
    Thread.Sleep(1000);
    Console.WriteLine("任务完成");
});

// 3. 从结果创建Task
Task<int> completedTask = Task.FromResult(42);

// 4. 创建已完成的任务
Task completed = Task.CompletedTask;

// 5. 创建异常任务
Task faultyTask = Task.FromException(new Exception("错误"));

// 6. 取消的任务
var cts = new CancellationTokenSource();
Task canceledTask = Task.FromCanceled(cts.Token);

3.2 等待任务完成

csharp
// 等待单个任务
async Task SingleTaskExample()
{
    Task<int> task = GetValueAsync();
    int result = await task;
    Console.WriteLine(result);
}

// 等待所有任务完成(并行执行)
async Task WhenAllExample()
{
    Task task1 = DoWorkAsync(1000);
    Task task2 = DoWorkAsync(2000);
    Task task3 = DoWorkAsync(3000);
    
    await Task.WhenAll(task1, task2, task3);
    Console.WriteLine("所有任务完成!");
}

// 等待任一任务完成
async Task WhenAnyExample()
{
    Task<string> downloadTask = DownloadAsync();
    Task timeoutTask = Task.Delay(5000);
    
    Task completed = await Task.WhenAny(downloadTask, timeoutTask);
    
    if (completed == downloadTask)
    {
        string result = await downloadTask;
        Console.WriteLine($"下载成功:{result}");
    }
    else
    {
        Console.WriteLine("下载超时");
    }
}

// 等待所有任务完成并获取结果
async Task WhenAllWithResultExample()
{
    Task<int>[] tasks = new[]
    {
        CalculateAsync(1, 100),
        CalculateAsync(2, 100),
        CalculateAsync(3, 100)
    };
    
    int[] results = await Task.WhenAll(tasks);
    Console.WriteLine($"结果:{string.Join(", ", results)}");
    Console.WriteLine($"总和:{results.Sum()}");
}

3.3 任务状态

csharp
void CheckTaskStatus(Task task)
{
    Console.WriteLine($"任务状态:{task.Status}");
    Console.WriteLine($"是否完成:{task.IsCompleted}");
    Console.WriteLine($"是否出错:{task.IsFaulted}");
    Console.WriteLine($"是否取消:{task.IsCanceled}");
    Console.WriteLine($"是否完成(含错误和取消):{task.IsCompleted}");
}

// 任务状态枚举
// - Created:已创建未启动
// - WaitingForActivation:等待激活
// - Running:正在运行
// - WaitingForChildrenToComplete:等待子任务完成
// - RanToCompletion:成功完成
// - Faulted:出错
// - Canceled:取消

4. 取消支持(CancellationToken)

4.1 基本用法

csharp
async Task DownloadAsync(CancellationToken token)
{
    // 检查取消
    token.ThrowIfCancellationRequested();
    
    using var client = new HttpClient();
    
    // 传递token给支持取消的API
    var response = await client.GetAsync("https://example.com", token);
    var content = await response.Content.ReadAsStringAsync(token);
    
    return content;
}

4.2 创建取消令牌

csharp
// 1. 手动取消
void ManualCancel()
{
    var cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    
    // 注册取消回调
    token.Register(() => Console.WriteLine("操作已取消"));
    
    // 稍后取消
    cts.Cancel();  // 触发取消
}

// 2. 超时自动取消
void TimeoutCancel()
{
    var cts = new CancellationTokenSource();
    cts.CancelAfter(TimeSpan.FromSeconds(5));
    CancellationToken token = cts.Token;
}

// 3. 组合取消
void CombineCancel()
{
    var cts1 = new CancellationTokenSource();
    var cts2 = new CancellationTokenSource();
    
    // 任一取消都触发取消
    var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        cts1.Token, cts2.Token
    );
    
    CancellationToken token = linkedCts.Token;
}

4.3 在循环中检查取消

csharp
async Task ProcessItemsAsync(List<string> items, CancellationToken token)
{
    for (int i = 0; i < items.Count; i++)
    {
        // 方式1:抛出异常
        token.ThrowIfCancellationRequested();
        
        // 方式2:检查并手动处理
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("操作已取消");
            return;
        }
        
        await ProcessItemAsync(items[i], token);
    }
}

// 支持取消的异步操作
async Task DownloadWithProgressAsync(CancellationToken token)
{
    using var client = new HttpClient();
    
    // 获取响应
    using var response = await client.GetAsync(
        "https://example.com/largefile.zip",
        HttpCompletionOption.ResponseHeadersRead,
        token
    );
    
    var totalBytes = response.Content.Headers.ContentLength ?? -1;
    using var stream = await response.Content.ReadAsStreamAsync(token);
    using var fileStream = File.Create("file.zip");
    
    var buffer = new byte[8192];
    int bytesRead;
    long totalRead = 0;
    
    while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
    {
        await fileStream.WriteAsync(buffer, 0, bytesRead, token);
        totalRead += bytesRead;
        
        if (totalBytes > 0)
        {
            int progress = (int)((double)totalRead / totalBytes * 100);
            Console.WriteLine($"进度:{progress}%");
        }
        
        token.ThrowIfCancellationRequested();
    }
}

5. 异常处理

5.1 基本异常处理

csharp
async Task HandleExceptionsAsync()
{
    try
    {
        await DownloadDataAsync();
        Console.WriteLine("下载成功");
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"网络错误:{ex.Message}");
    }
    catch (OperationCanceledException ex)
    {
        Console.WriteLine($"操作已取消:{ex.Message}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"未知错误:{ex.Message}");
    }
}

5.2 多个任务的异常处理

csharp
async Task MultipleTasksExceptionExample()
{
    Task task1 = ThrowExceptionAsync("任务1");
    Task task2 = ThrowExceptionAsync("任务2");
    
    try
    {
        await Task.WhenAll(task1, task2);
    }
    catch (Exception ex)
    {
        // 只捕获第一个异常
        Console.WriteLine($"捕获异常:{ex.Message}");
        
        // 获取所有异常
        if (task1.IsFaulted)
        {
            foreach (var inner in task1.Exception.InnerExceptions)
                Console.WriteLine($"任务1:{inner.Message}");
        }
        if (task2.IsFaulted)
        {
            foreach (var inner in task2.Exception.InnerExceptions)
                Console.WriteLine($"任务2:{inner.Message}");
        }
    }
}

// 完整的异常信息获取
async Task GetAllExceptionsExample()
{
    try
    {
        await ThrowExceptionAsync("任务1");
        await ThrowExceptionAsync("任务2");
    }
    catch (Exception ex)
    {
        // 获取所有异常(包括内部异常)
        var allExceptions = GetInnerExceptions(ex);
        foreach (var e in allExceptions)
        {
            Console.WriteLine(e.Message);
        }
    }
}

IEnumerable<Exception> GetInnerExceptions(Exception ex)
{
    if (ex is AggregateException agg)
    {
        foreach (var inner in agg.InnerExceptions)
        {
            foreach (var e in GetInnerExceptions(inner))
                yield return e;
        }
    }
    else if (ex.InnerException != null)
    {
        yield return ex;
        foreach (var e in GetInnerExceptions(ex.InnerException))
            yield return e;
    }
    else
    {
        yield return ex;
    }
}

5.3 async void 的异常处理

csharp
// ❌ 危险:async void 中的异常会丢失
async void BadButtonClick(object sender, EventArgs e)
{
    throw new Exception("这个异常会崩溃应用程序!");
}

// ✅ 安全:在事件处理器中捕获异常
async void SafeButtonClick(object sender, EventArgs e)
{
    try
    {
        await LoadDataAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"加载失败:{ex.Message}");
        // 显示给用户
    }
}

6. 最佳实践与陷阱

6.1 避免同步等待异步任务

csharp
// ❌ 危险:可能导致死锁
string BadSyncCall()
{
    // 同步等待异步任务
    var task = GetDataAsync();
    string result = task.Result;  // 可能死锁!
    return result;
}

// ❌ 危险:也可能死锁
string BadSyncCall2()
{
    return GetDataAsync().GetAwaiter().GetResult();
}

// ✅ 正确:一直异步到底
async Task<string> GoodAsyncCall()
{
    return await GetDataAsync();
}

// ✅ 替代:使用 ConfigureAwait(false)(适用于类库)
string SafeSyncCall()
{
    return GetDataAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}

6.2 ConfigureAwait(false) 的使用

csharp
// UI应用(WinForms/WPF)
async void ButtonClick(object sender, EventArgs e)
{
    // 默认捕获UI上下文
    var data = await GetDataAsync();
    // 回到UI线程更新界面
    label.Text = data;
}

// 类库代码(推荐)
async Task<string> FetchDataAsync()
{
    // 不捕获上下文,提升性能
    using var client = new HttpClient();
    string result = await client.GetStringAsync("https://api.example.com")
                               .ConfigureAwait(false);
    return result;
}

// 多个await的情况
async Task<string> ComplexExampleAsync()
{
    // 第一个await不捕获上下文
    var data = await GetDataAsync().ConfigureAwait(false);
    
    // 第二个await也不捕获上下文(明确指定)
    var processed = await ProcessDataAsync(data).ConfigureAwait(false);
    
    return processed;
}

6.3 避免 async void(事件处理器除外)

csharp
// ❌ 不推荐:除了事件处理器
async void BadMethod()
{
    // 异常无法被调用者捕获
    throw new Exception("丢失的异常");
}

// ✅ 推荐:使用Task
async Task GoodMethod()
{
    // 异常可被捕获
    throw new Exception("可捕获的异常");
}

6.4 过度异步

csharp
// ❌ 不必要:方法只有一行async/await
async Task<int> OverAsync()
{
    return await Task.FromResult(42);
}

// ✅ 更简单:同步返回
int Simple()
{
    return 42;
}

// ✅ 如果有条件判断,可以使用 ValueTask
async ValueTask<int> BetterAsync()
{
    if (cached)
        return 42;  // 同步返回,不分配内存
    return await FetchFromDbAsync();
}

6.5 线程池使用

csharp
// ❌ 避免:用Task.Run包装纯异步方法
async Task BadAsync()
{
    // Task.Run会创建线程,但HttpClient已经是异步
    await Task.Run(async () =>
    {
        using var client = new HttpClient();
        await client.GetStringAsync("https://example.com");
    });
}

// ✅ 直接调用异步方法
async Task GoodAsync()
{
    using var client = new HttpClient();
    await client.GetStringAsync("https://example.com");
}

// ✅ Task.Run适用于CPU密集型操作
Task<int> ProcessDataAsync()
{
    return Task.Run(() =>
    {
        // CPU密集型计算
        int result = 0;
        for (int i = 0; i < 1000000; i++)
            result += i;
        return result;
    });
}

6.6 命名规范

csharp
// ✅ 异步方法命名以Async结尾(约定)
async Task<string> FetchDataAsync() { }
async Task<int> CalculateAsync() { }

// ❌ 不推荐:不以Async结尾
async Task<string> FetchData() { }

// ✅ 事件处理器可以不加Async后缀
async void Button_Click(object sender, EventArgs e) { }

7. 实战场景

7.1 Web API 调用

csharp
public class ApiService
{
    private readonly HttpClient _httpClient = new();
    
    // GET请求
    public async Task<T> GetAsync<T>(string url)
    {
        try
        {
            var response = await _httpClient.GetAsync(url);
            response.EnsureSuccessStatusCode();
            
            string json = await response.Content.ReadAsStringAsync();
            return JsonSerializer.Deserialize<T>(json);
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"HTTP请求失败:{ex.Message}");
            throw;
        }
    }
    
    // POST请求
    public async Task<T> PostAsync<T>(string url, object data)
    {
        string json = JsonSerializer.Serialize(data);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        
        var response = await _httpClient.PostAsync(url, content);
        response.EnsureSuccessStatusCode();
        
        string resultJson = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<T>(resultJson);
    }
    
    // 文件下载
    public async Task DownloadFileAsync(string url, string savePath, IProgress<float> progress)
    {
        var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
        response.EnsureSuccessStatusCode();
        
        var totalBytes = response.Content.Headers.ContentLength ?? -1;
        using var contentStream = await response.Content.ReadAsStreamAsync();
        using var fileStream = File.Create(savePath);
        
        byte[] buffer = new byte[8192];
        long totalRead = 0;
        int bytesRead;
        
        while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
        {
            await fileStream.WriteAsync(buffer, 0, bytesRead);
            totalRead += bytesRead;
            
            if (totalBytes > 0)
            {
                progress?.Report((float)totalRead / totalBytes * 100);
            }
        }
    }
}

7.2 数据库操作

csharp
public class UserService
{
    private readonly string _connectionString;
    
    public UserService(string connectionString)
    {
        _connectionString = connectionString;
    }
    
    // 异步查询
    public async Task<List<User>> GetUsersAsync()
    {
        var users = new List<User>();
        
        using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync();
        
        using var command = new SqlCommand("SELECT Id, Name, Email FROM Users", connection);
        using var reader = await command.ExecuteReaderAsync();
        
        while (await reader.ReadAsync())
        {
            users.Add(new User
            {
                Id = reader.GetInt32(0),
                Name = reader.GetString(1),
                Email = reader.GetString(2)
            });
        }
        
        return users;
    }
    
    // 异步插入
    public async Task<int> InsertUserAsync(string name, string email)
    {
        using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync();
        
        const string sql = "INSERT INTO Users (Name, Email) VALUES (@Name, @Email)";
        using var command = new SqlCommand(sql, connection);
        command.Parameters.AddWithValue("@Name", name);
        command.Parameters.AddWithValue("@Email", email);
        
        return await command.ExecuteNonQueryAsync();
    }
}

7.3 文件操作

csharp
public class FileService
{
    // 读取大文件(逐行)
    public async Task ProcessFileAsync(string path, Func<string, Task> lineProcessor)
    {
        using var reader = new StreamReader(path);
        string line;
        int lineNumber = 0;
        
        while ((line = await reader.ReadLineAsync()) != null)
        {
            lineNumber++;
            await lineProcessor(line);
        }
    }
    
    // 异步写入
    public async Task WriteFileAsync(string path, IEnumerable<string> lines)
    {
        using var writer = new StreamWriter(path);
        
        foreach (var line in lines)
        {
            await writer.WriteLineAsync(line);
        }
    }
    
    // 异步复制(带进度)
    public async Task CopyFileAsync(string source, string dest, IProgress<int> progress)
    {
        using var sourceStream = File.OpenRead(source);
        using var destStream = File.Create(dest);
        
        var totalBytes = sourceStream.Length;
        var buffer = new byte[8192];
        long totalRead = 0;
        int bytesRead;
        
        while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
        {
            await destStream.WriteAsync(buffer, 0, bytesRead);
            totalRead += bytesRead;
            
            int percentage = (int)((double)totalRead / totalBytes * 100);
            progress?.Report(percentage);
        }
    }
}

8. 性能优化

8.1 ValueTask<T> 的使用

csharp
// 适用于高频调用、可能同步完成的情况
public class CacheService
{
    private readonly Dictionary<string, string> _cache = new();
    private readonly HttpClient _httpClient = new();
    
    // 使用ValueTask优化缓存场景
    public async ValueTask<string> GetValueAsync(string key)
    {
        // 缓存命中:同步返回(无堆内存分配)
        if (_cache.TryGetValue(key, out string cached))
            return cached;
        
        // 缓存未命中:异步获取
        string result = await FetchFromNetworkAsync(key);
        _cache[key] = result;
        return result;
    }
    
    private async Task<string> FetchFromNetworkAsync(string key)
    {
        var response = await _httpClient.GetStringAsync($"https://api.example.com/{key}");
        return response;
    }
}

8.2 减少不必要的异步

csharp
// ❌ 不必要:包装同步操作
async Task<int> BadAsync(int a, int b)
{
    return await Task.Run(() => a + b);  // 多余!
}

// ✅ 同步即可
int GoodSync(int a, int b) => a + b;

// ✅ 需要等待多个结果时使用WhenAll
async Task<int> ProcessMultipleAsync()
{
    // 错误:依次执行
    // var result1 = await GetData1Async();
    // var result2 = await GetData2Async();
    // var result3 = await GetData3Async();
    
    // 正确:并行执行
    var task1 = GetData1Async();
    var task2 = GetData2Async();
    var task3 = GetData3Async();
    
    await Task.WhenAll(task1, task2, task3);
    return task1.Result + task2.Result + task3.Result;
}

9. 速查表

9.1 常用API速查

方法

说明

Task.Delay(ms)

异步等待(不阻塞线程)

Task.Run(() => { })

在ThreadPool上执行代码

Task.FromResult(value)

创建已完成的任务

Task.CompletedTask

创建已完成的空任务

Task.WhenAll(tasks)

等待所有任务完成

Task.WhenAny(tasks)

等待任一任务完成

CancellationTokenSource

创建取消令牌

ConfigureAwait(false)

避免捕获上下文

9.2 异步方法模板

csharp
// 无返回值
public async Task MethodNameAsync(parameters)
{
    try
    {
        await SomeAsyncOperation();
    }
    catch (Exception ex)
    {
        // 处理异常
        throw;
    }
}

// 有返回值
public async Task<T> MethodNameAsync(parameters)
{
    try
    {
        T result = await SomeAsyncOperation();
        return result;
    }
    catch (Exception ex)
    {
        // 处理异常
        throw;
    }
}

// 带取消支持
public async Task<T> MethodNameAsync(parameters, CancellationToken token)
{
    token.ThrowIfCancellationRequested();
    
    T result = await SomeAsyncOperation(token);
    
    return result;
}

// 带进度报告
public async Task MethodNameAsync(parameters, IProgress<float> progress)
{
    progress?.Report(0);
    
    await SomeAsyncOperation();
    
    progress?.Report(100);
}

9.3 常见陷阱总结

陷阱

说明

解决方案

同步等待异步

task.Result / task.Wait()

使用 await

async void

异常丢失

仅用于事件处理器

死锁

ConfigureAwait(true) + UI线程

使用 ConfigureAwait(false)

过度异步

不必要的async包装

考虑同步实现

忽略取消

不支持取消

传递 CancellationToken

异常丢失

多个任务异常

检查 Task.Exception

点赞
收藏
暂无评论,快来发表评论吧~