C# 异步编程:从 async/await 到 Task 实战指南

1. 引言

C# 异步编程是现代 .NET 开发中不可或缺的核心技能。无论是构建高并发的 Web 服务、流畅的桌面应用,还是高效的移动端程序,合理使用异步编程都能显著提升应用的响应速度和资源利用率。本文将从基础概念出发,逐步深入讲解 async/await、Task、异步模式以及常见实战场景,帮助你系统掌握 C# 异步编程。

2. 为什么需要异步编程

在传统的同步编程模型中,当一个线程执行耗时操作(如文件读取、网络请求、数据库查询)时,该线程会一直阻塞等待操作完成。这会导致以下问题:

  • UI 卡顿:在桌面或移动应用中,UI 线程被阻塞会导致界面无响应,严重影响用户体验。
  • 资源浪费:服务器端大量线程被阻塞在 I/O 等待上,无法处理其他请求,造成线程池资源浪费。
  • 扩展性差:面对高并发场景,同步模型难以支撑大规模请求。

异步编程通过让线程在等待 I/O 操作时主动让出执行权,去处理其他任务,从而大幅提升应用的吞吐量和响应速度。

3. async 和 await 基础

async 和 await 是 C# 5.0 引入的关键字,它们让异步代码的编写变得像同步代码一样直观。下面是一个最简单的异步方法示例:

csharp 复制代码
public async Task<string> FetchDataAsync()
{
    using var httpClient = new HttpClient();
    string result = await httpClient.GetStringAsync("https://api.example.com/data");
    return result;
}

使用 async 关键字修饰方法,表示该方法包含异步操作;await 关键字用于等待一个异步操作完成,同时不会阻塞当前线程。编译器会将 async 方法转换为状态机,自动管理异步操作的恢复和上下文切换。

3.1 async 方法的返回值类型

async 方法可以有以下三种返回类型:

  • Task<TResult>:方法返回一个值。
  • Task:方法不返回值,执行一个异步操作。
  • void:仅用于事件处理器,不推荐在其他场景使用。
csharp 复制代码
// 返回值的异步方法
public async Task<int> CalculateAsync()
{
    await Task.Delay(100);
    return 42;
}

// 无返回值的异步方法
public async Task DoWorkAsync()
{
    await Task.Delay(100);
    Console.WriteLine("完成");
}

// 事件处理器(唯一推荐使用 void 的场景)
public async void OnButtonClick(object sender, EventArgs e)
{
    await LoadDataAsync();
}

4. Task 深入理解

Task 表示一个异步操作的单元,它封装了操作的执行状态、结果和异常信息。理解 Task 的生命周期和常用 API 是掌握异步编程的关键。

4.1 创建 Task

csharp 复制代码
// 方式一:Task.Run 在线程池上执行
Task task = Task.Run(() =>
{
    // 执行耗时计算
    Thread.Sleep(1000);
    Console.WriteLine("任务完成");
});

// 方式二:Task.Factory.StartNew(更底层)
Task<int> task2 = Task.Factory.StartNew(() =>
{
    return 100;
});

// 方式三:Task.FromResult 返回已完成的任务
Task<string> completedTask = Task.FromResult("立即返回");

4.2 等待多个任务

在实际开发中,经常需要同时执行多个异步操作并等待它们全部完成或任一完成。Task 提供了 Task.WhenAll 和 Task.WhenAny 来应对这些场景。

csharp 复制代码
// 等待所有任务完成
var task1 = FetchDataAsync("url1");
var task2 = FetchDataAsync("url2");
var task3 = FetchDataAsync("url3");

await Task.WhenAll(task1, task2, task3);
Console.WriteLine("所有请求完成");

// 等待任一任务完成
Task<string> firstCompleted = await Task.WhenAny(task1, task2, task3);
Console.WriteLine($"最先完成:{firstCompleted.Result}");

5. 异步编程的常见模式

5.1 异步流(IAsyncEnumerable)

C# 8.0 引入了异步流,允许异步地逐个产生和消费数据,非常适合处理分页数据、日志流等场景。

csharp 复制代码
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
    for (int i = 0; i < 10; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

// 消费异步流
await foreach (var number in GenerateNumbersAsync())
{
    Console.WriteLine(number);
}

5.2 取消异步操作

使用 CancellationToken 可以优雅地取消长时间运行的异步操作,避免资源浪费。

csharp 复制代码
public async Task<string> FetchWithCancellationAsync(CancellationToken cancellationToken)
{
    using var httpClient = new HttpClient();
    return await httpClient.GetStringAsync(
        "https://api.example.com/data",
        cancellationToken);
}

// 使用方式
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
    string result = await FetchWithCancellationAsync(cts.Token);
    Console.WriteLine(result);
}
catch (OperationCanceledException)
{
    Console.WriteLine("操作已取消");
}

5.3 异步与并行

当需要同时执行多个 CPU 密集型任务时,可以结合 Task.Run 和 Task.WhenAll 实现并行计算。

csharp 复制代码
var tasks = Enumerable.Range(1, 10).Select(i =>
    Task.Run(() => ComputeHeavyWork(i))
);

int[] results = await Task.WhenAll(tasks);
Console.WriteLine($"结果总和:{results.Sum()}");

static int ComputeHeavyWork(int input)
{
    // 模拟耗时计算
    Thread.Sleep(500);
    return input * input;
}

6. 异步编程的注意事项与最佳实践

6.1 避免 async void

async void 方法无法被调用方捕获异常,一旦发生异常会导致程序崩溃。除事件处理器外,应始终使用 Task 作为返回类型。

6.2 避免阻塞异步代码

在异步方法中应避免使用 .Result 或 .Wait() 等阻塞调用,这会导致死锁和线程池饥饿。

csharp 复制代码
// 错误做法:阻塞等待
string result = FetchDataAsync().Result;

// 正确做法:使用 await
string result = await FetchDataAsync();

6.3 配置上下文(ConfigureAwait)

在类库代码中,使用 ConfigureAwait(false) 可以避免不必要的上下文切换,提升性能。

csharp 复制代码
public async Task<string> ReadFileAsync(string path)
{
    string content = await File.ReadAllTextAsync(path)
        .ConfigureAwait(false);
    return content;
}

6.4 异常处理

异步方法的异常处理与同步方法类似,使用 try-catch 即可捕获。但需要注意,多个任务并行时,Task.WhenAll 会抛出第一个异常,其余异常需要通过 Task.Exception 查看。

csharp 复制代码
try
{
    await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex)
{
    Console.WriteLine($"发生异常:{ex.Message}");
}

7. 实战案例:异步下载器

下面通过一个完整的实战案例,综合运用本文所学的异步编程知识,实现一个简单的异步文件下载器。

csharp 复制代码
public class AsyncDownloader
{
    private readonly HttpClient _httpClient = new HttpClient();

    public async Task DownloadFilesAsync(
        IEnumerable<string> urls,
        string outputDirectory,
        CancellationToken cancellationToken = default)
    {
        Directory.CreateDirectory(outputDirectory);

        var tasks = urls.Select(url =>
            DownloadFileAsync(url, outputDirectory, cancellationToken));

        await Task.WhenAll(tasks);
        Console.WriteLine("所有文件下载完成");
    }

    private async Task DownloadFileAsync(
        string url,
        string outputDirectory,
        CancellationToken cancellationToken)
    {
        try
        {
            string fileName = Path.GetFileName(new Uri(url).LocalPath);
            string filePath = Path.Combine(outputDirectory, fileName);

            byte[] data = await _httpClient.GetByteArrayAsync(url, cancellationToken);
            await File.WriteAllBytesAsync(filePath, data, cancellationToken);

            Console.WriteLine($"已下载:{fileName}");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine($"已取消:{url}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"下载失败:{url},原因:{ex.Message}");
        }
    }
}

// 调用示例
var downloader = new AsyncDownloader();
var urls = new[]
{
    "https://example.com/file1.zip",
    "https://example.com/file2.zip",
    "https://example.com/file3.zip"
};

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await downloader.DownloadFilesAsync(urls, @"C:\Downloads", cts.Token);

8. 总结

C# 异步编程是构建高性能、高响应性应用的核心技能。通过本文的学习,你已经掌握了 async/await 的基本用法、Task 的深入理解、常见异步模式以及最佳实践。在实际开发中,建议遵循以下原则:

  • 始终使用 async/await 而非阻塞调用。
  • 避免 async void,除非是事件处理器。
  • 合理使用 CancellationToken 支持取消操作。
  • 在类库中使用 ConfigureAwait(false) 提升性能。
  • 结合 Task.WhenAll 和 Task.WhenAny 处理并发场景。

异步编程需要在实际项目中不断练习和积累经验,希望本文能为你打下坚实的基础,助你在 .NET 开发之路上更进一步。

相关推荐
羚尔35 分钟前
C语言数组
c语言·开发语言
2601_962074681 小时前
Spring Boot的无缝衔接:深入解析与实践
数据库·spring boot·后端
女神下凡1 小时前
芯参谋(11): 各种存储芯片电路设计
开发语言·嵌入式硬件
武帝为此1 小时前
【InnoDB存储引擎介绍】
数据库·算法
冬夜戏雪1 小时前
笔试的Scanner in Scanner out
java·开发语言
csdn_aspnet1 小时前
mysql 触发器写法及示例
数据库·mysql·触发器·trigger
Zane1994121 小时前
HashSet、TreeSet、LinkedHashSet:底层都是“套壳“
java·开发语言
CJi0NG2 小时前
【自用】MySQL-概述
数据库·mysql
数据技术说2 小时前
数据库慢了怎么查?从监控报警到找到根因的完整路径
数据库·架构