C#的异步编程模式是什么?如何实现?

煙雲
发布: 2025-09-02 08:08:01
原创
952人浏览过
答案是基于async和await的TAP模式是C#推荐的异步编程方式,它通过非阻塞I/O提升响应性和吞吐量,适用于I/O密集型操作,结合Task.Run可处理CPU密集型任务,相比传统多线程更简洁高效,避免回调地狱,需注意async void、ConfigureAwait和异常处理等最佳实践。

c#的异步编程模式是什么?如何实现?

C#的异步编程模式,说到底,我们现在最常用也最推荐的,就是基于

async
登录后复制
await
登录后复制
关键字的Task-based Asynchronous Pattern (TAP)。它提供了一种非常优雅的方式来编写非阻塞代码,尤其是在处理那些耗时且需要等待结果的操作时,比如网络请求、文件读写或者长时间的计算。核心思想就是,当一个操作需要等待时,程序不会傻傻地卡在那里,而是可以去做其他有意义的事情,等操作完成再回来继续。这大大提升了应用程序的响应性和资源利用率。

解决方案

实现C#的异步编程,核心就是围绕着

async
登录后复制
await
登录后复制
这两个关键字,以及
Task
登录后复制
Task<TResult>
登录后复制
类型展开。

首先,你需要将一个方法标记为

async
登录后复制
。这告诉编译器,这个方法里面可能会有
await
登录后复制
表达式。
async
登录后复制
关键字本身并不会让方法异步执行,它只是一个修饰符,允许你在方法体内部使用
await
登录后复制

public async Task<string> FetchDataAsync()
{
    // ...
}
登录后复制

接着,在

async
登录后复制
方法内部,当你调用一个同样返回
Task
登录后复制
Task<TResult>
登录后复制
的异步操作时,你可以使用
await
登录后复制
关键字。
await
登录后复制
的作用是暂停当前方法的执行,并将控制权返回给调用者。当被
await
登录后复制
的操作完成时,控制权会回到
await
登录后复制
表达式之后的那一行,继续执行。

举个例子,假设我们想从一个API获取数据:

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class DataService
{
    private readonly HttpClient _httpClient = new HttpClient();

    public async Task<string> GetExternalDataAsync(string url)
    {
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 开始请求数据...");
        // 当执行到这里时,方法会暂停,并释放当前线程。
        // 网络请求在后台进行,线程可以去做其他事情。
        string data = await _httpClient.GetStringAsync(url);
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 数据请求完成。");
        return data;
    }

    public async Task ProcessDataAsync()
    {
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 开始处理流程...");
        string result = await GetExternalDataAsync("https://jsonplaceholder.typicode.com/todos/1");
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 接收到的数据长度: {result.Length}");
        // 这里可以继续处理result
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 流程处理完毕。");
    }

    // 主调用方,通常是一个UI事件处理程序或控制台应用的Main方法
    public static async Task Main(string[] args)
    {
        DataService service = new DataService();
        // 调用异步方法
        await service.ProcessDataAsync();

        Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 主程序继续执行其他操作...");
        // 确保异步操作有足够时间完成,对于控制台应用,通常需要保持主线程活跃
        // Console.ReadLine(); // 如果没有其他异步操作,可以用这个保持程序运行
    }
}
登录后复制

在这个例子里,当

GetExternalDataAsync
登录后复制
方法调用
_httpClient.GetStringAsync(url)
登录后复制
await
登录后复制
它时,当前线程并没有被阻塞。它会回到调用栈,允许UI线程响应用户输入,或者允许服务器处理其他请求。当网络请求完成后,一个合适的线程(通常是原来的上下文线程,或者线程池中的一个线程)会继续执行
GetStringAsync
登录后复制
之后的代码。这种模式对于I/O密集型操作尤其有效,因为它能极大地提高应用程序的吞吐量和响应速度。

对于CPU密集型操作,如果你想让它不阻塞调用线程,可以结合

Task.Run
登录后复制
来使用:

public async Task<long> CalculateFactorialAsync(int n)
{
    // 将CPU密集型计算放到线程池线程中执行,避免阻塞UI线程或请求处理线程。
    long result = await Task.Run(() =>
    {
        long factorial = 1;
        for (int i = 1; i <= n; i++)
        {
            factorial *= i;
        }
        return factorial;
    });
    return result;
}
登录后复制

这样,

Task.Run
登录后复制
会将计算任务调度到线程池中的一个线程上,而
await
登录后复制
则等待这个计算完成,同样不会阻塞调用
CalculateFactorialAsync
登录后复制
的线程。

为什么选择
async/await
登录后复制
而不是传统的线程或回调?

在我看来,

async/await
登录后复制
模式之所以成为C#异步编程的首选,关键在于它极大地简化了异步代码的编写和理解。回想一下以前,我们可能需要使用回调函数(Callback Hell)、事件驱动异步模式(EAP)或者异步编程模型(APM),那些代码写起来复杂,可读性差,错误处理也相当麻烦。当你的逻辑变得稍微复杂一点,多个异步操作需要串联或并行执行时,代码很快就会变得难以维护。

async/await
登录后复制
让异步代码看起来几乎和同步代码一样,它以一种“顺序执行”的错觉,优雅地处理了线程切换、上下文捕获和结果传递等底层细节。这意味着开发者可以把更多精力放在业务逻辑上,而不是陷在复杂的线程管理和同步机制中。对于现代应用程序,尤其是那些需要高响应性(比如桌面应用、移动应用)或者高吞吐量(比如Web服务器)的场景,
async/await
登录后复制
是提升用户体验和系统性能的利器。它能确保你的应用在等待外部资源时依然保持流畅,不会给用户带来卡顿感。

async/await
登录后复制
实践中,有哪些常见的“坑”和最佳实践?

虽然

async/await
登录后复制
用起来很爽,但它也不是没有自己的脾气。有些地方如果处理不好,可能会让你掉进“坑”里。

一个常见的误区是使用

async void
登录后复制
async void
登录后复制
方法主要用于事件处理程序,因为它允许事件订阅者异步执行而不需要返回一个
Task
登录后复制
。但除此之外,几乎所有其他场景都应该返回
async Task
登录后复制
async Task<TResult>
登录后复制
。为什么呢?因为
async void
登录后复制
方法无法被
await
登录后复制
,这意味着你无法知道它何时完成,也无法捕获它内部抛出的异常。一旦
async void
登录后复制
方法内部抛出未处理的异常,它会直接回到应用程序的SynchronizationContext,如果没有合适的处理,就可能导致应用程序崩溃。这就像你放了一个风筝,线断了,你却不知道它飞到哪里去了。

豆包AI编程
豆包AI编程

豆包推出的AI编程助手

豆包AI编程 483
查看详情 豆包AI编程
// 避免:难以追踪完成和异常
public async void BadAsyncVoidMethod()
{
    await Task.Delay(1000);
    throw new InvalidOperationException("Oops!"); // 这个异常很难被捕获
}

// 推荐:返回Task,可以被await,异常可捕获
public async Task GoodAsyncTaskMethod()
{
    await Task.Delay(1000);
    // throw new InvalidOperationException("Oops!");
}
登录后复制

另一个需要注意的点是

ConfigureAwait(false)
登录后复制
。当你
await
登录后复制
一个
Task
登录后复制
时,默认情况下,运行时会尝试捕获当前的“同步上下文”(SynchronizationContext)或“任务调度器”(TaskScheduler)。当异步操作完成后,它会尝试回到这个捕获的上下文继续执行
await
登录后复制
之后的代码。这对于UI应用非常有用,因为UI元素只能在UI线程上更新。但对于库代码或Web API的后端代码,这种上下文切换通常是不必要的,甚至会带来性能开销,或者在某些情况下导致死锁(特别是当你在同步代码中混合使用
await
登录后复制
.Result
登录后复制
.Wait()
登录后复制
时)。

所以,在库代码或不需要特定上下文的后端服务中,我通常会建议在

await
登录后复制
调用后加上
.ConfigureAwait(false)
登录后复制

// 库方法或后端服务中,通常不需要回到原始上下文
public async Task<string> FetchAndProcessDataAsync(string url)
{
    // 不回到原始上下文,提高性能,避免死锁
    string data = await _httpClient.GetStringAsync(url).ConfigureAwait(false);
    // 这里可以继续处理data
    return data.ToUpperInvariant();
}
登录后复制

这样告诉运行时,异步操作完成后,不需要强制回到原始上下文,可以在任何可用的线程池线程上继续执行,这有助于减少开销并避免潜在的死锁问题。但请记住,一旦你使用了

ConfigureAwait(false)
登录后复制
await
登录后复制
之后你就不能再访问任何依赖于原始上下文的状态了,比如UI控件。

最后,异常处理。异步方法中的异常处理和同步方法类似,可以使用

try-catch
登录后复制
块。但要注意,如果一个
Task
登录后复制
await
登录后复制
了,它的异常会在
await
登录后复制
点重新抛出。如果
Task
登录后复制
没有被
await
登录后复制
,它的异常会在
Task
登录后复制
被垃圾回收时(或者在
.Result
登录后复制
.Wait()
登录后复制
被调用时)抛出,这可能会导致程序崩溃,而且很难调试。所以,确保所有
Task
登录后复制
都被妥善地
await
登录后复制
或处理。

public async Task RobustOperationAsync()
{
    try
    {
        string data = await _httpClient.GetStringAsync("http://invalid.url").ConfigureAwait(false);
        Console.WriteLine(data);
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"网络请求失败: {ex.Message}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"发生未知错误: {ex.Message}");
    }
}
登录后复制

async/await
登录后复制
与传统多线程编程有何不同?

这是一个经常被问到的问题,也常常被混淆。在我看来,理解

async/await
登录后复制
与传统多线程(比如直接使用
Thread
登录后复制
类或
ThreadPool
登录后复制
)以及并行编程(比如
Parallel.For
登录后复制
或PLINQ)的区别至关重要。

最核心的区别在于:

async/await
登录后复制
主要是关于非阻塞I/O,它关注的是如何让一个操作在等待某个外部资源(如网络响应、磁盘读写)时,不占用或阻塞当前的执行线程,从而提高应用程序的响应性和吞吐量。它本身并不会创建新的线程来并行执行代码。当一个方法
await
登录后复制
一个
Task
登录后复制
时,当前线程会被释放,可以去做其他事情。当被
await
登录后复制
Task
登录后复制
完成时,
await
登录后复制
之后的代码会在一个可用的线程上(可能是原始线程,也可能是线程池中的一个线程)继续执行。所以,你可以把
async/await
登录后复制
看作是一种“协作式多任务”或者“事件驱动”的编程模型,它更多是关于并发(concurrency)而不是并行(parallelism)。

而传统的多线程编程,比如直接创建

new Thread()
登录后复制
或者使用
ThreadPool.QueueUserWorkItem
登录后复制
,以及并行编程,比如
Task.Run
登录后复制
(不带
await
登录后复制
),
Parallel.For
登录后复制
/
ForEach
登录后复制
,它们的目标是并行执行代码。它们会利用多个CPU核心或多个线程来同时执行不同的计算任务,以减少总的执行时间。这种模式更适合于CPU密集型任务,因为这些任务需要大量的计算资源。

简单来说:

  • async/await
    登录后复制
    适合I/O密集型操作。它让你的程序在等待外部资源时“空闲”下来,去做其他事情,而不是阻塞等待。它不一定增加CPU的利用率,但能显著提高响应性和吞吐量。它通常使用较少的线程,通过上下文切换来模拟并发。
  • 传统多线程/并行编程: 适合CPU密集型操作。它通过分配多个线程或利用多核处理器来同时执行计算任务,以缩短总的计算时间。这会增加CPU的利用率。

当然,两者并非完全独立。在某些情况下,你会希望将一个CPU密集型任务转换为异步操作,以便不阻塞调用线程。这时,你就可以结合使用

Task.Run
登录后复制
await
登录后复制

// CPU密集型任务,但希望通过async/await使其不阻塞调用线程
public async Task<int> PerformCpuIntensiveWorkAsync()
{
    // Task.Run 将计算任务调度到线程池,await 等待结果
    int result = await Task.Run(() =>
    {
        // 模拟一个耗时的CPU计算
        int sum = 0;
        for (int i = 0; i < 1_000_000_000; i++)
        {
            sum += i;
        }
        return sum;
    });
    return result;
}
登录后复制

这里,

Task.Run
登录后复制
负责将CPU密集型计算放到一个单独的线程池线程上执行,而
await
登录后复制
则让
PerformCpuIntensiveWorkAsync
登录后复制
方法在等待计算完成时是非阻塞的。这是一种将并行计算融入异步流程的常见模式。理解这些差异,能够帮助我们根据具体的场景,选择最合适的编程模型。

以上就是C#的异步编程模式是什么?如何实现?的详细内容,更多请关注php中文网其它相关文章!

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号