第二章:C#异步编程简介
第二章:异步编程简介 🚀
异步编程(Asynchronous Programming)是现代编程中处理 I/O 密集型任务的关键技术之一。与传统的同步编程不同,异步编程允许程序在等待耗时操作(如网络请求、文件读写、数据库查询等)完成时,继续执行其他任务,从而提高应用程序的响应性和效率。
在本章中,我们将介绍异步编程的基本概念、C# 中的异步编程模型以及常用的 async 和 await 关键字。我们还会探讨异步编程的一些常见应用场景和最佳实践,帮助你理解如何在实际开发中使用异步编程来提升性能。
2.1 异步编程的基本概念 🤔
同步 vs 异步
- 同步编程 🛑:在同步编程中,任务是顺序执行的。如果某个操作是耗时的(如网络请求),程序会阻塞等待操作完成,之后才会继续执行下一行代码。这种方式简单直观,但在处理 I/O 密集型任务时,可能会导致程序变得不响应。
- 异步编程 🔄:异步编程允许程序在执行耗时操作时,不必等待操作完成,而是立即返回继续执行其他任务。耗时操作完成后,会通过回调、事件或
Task通知程序结果。这样,程序不会被阻塞,能更好地利用系统资源。
异步编程的优势 🌟
- 提高性能和吞吐量:异步编程避免了线程的阻塞,使得应用程序可以处理更多的任务。
- 提升用户体验:对于 GUI 应用程序,异步编程能保持界面的响应性,避免“卡顿”现象。
- 更好地利用资源:异步编程通常使用线程池来管理后台任务,避免了频繁创建和销毁线程的开销。
2.2 C# 中的异步编程模型简介 🛠️
C# 提供了多种方式来实现异步编程,其中最常用的是基于 Task 和 async/await 的异步编程模型。在此之前,C# 中还有以下异步编程模型:
异步编程模型(APM)
APM(Asynchronous Programming Model) 是 C# 中最早引入的异步编程模式,通常使用 BeginXXX 和 EndXXX 方法来表示异步操作。BeginXXX 方法启动异步操作,而 EndXXX 方法用于获取操作结果。APM 使用 回调 或 IAsyncResult 来处理异步任务的完成。虽然简单,但代码往往难以阅读和维护,容易形成“回调地狱”。
基于事件的异步模式(EAP)
EAP(Event-based Asynchronous Pattern,EAP)是一种较早使用的异步编程模式。它主要用于处理需要长时间运行的操作(例如 I/O 操作),并通过事件通知调用者操作的完成情况。这种模型曾经在 .NET Framework 2.0 和 3.5 中被广泛使用,随着 async/await 引入后,EAP 逐渐被淘汰。
基于任务的异步模式(TAP)
C# 5.0 引入了 Task 和 async/await,这是一种更加现代和简洁的异步编程方式,以同步编码的方式编写异步代码。
async 和 await 关键字 🔑
async:用于标记一个方法为异步方法。异步方法通常返回Task或Task<T>,表示异步操作的结果。await:用于等待一个异步操作的完成,并在操作完成后继续执行后续代码。await会释放当前线程,让它去处理其他任务,避免了阻塞。
APM和EAP基本已经被淘汰了,在本系列文章后续也不会再过多介绍。详情参考官网
示例代码
基于回调(Callback)异步编程模型(APM) 🧩
回调 是一种简单的异步编程方式,通常通过委托或匿名方法来实现。开发者将一个方法作为参数传递给异步操作,当操作完成时,调用此方法返回结果。
| using System; | |
| using System.Threading; | |
| class Program | |
| { | |
| static void Main(string[] args) | |
| { | |
| Console.WriteLine("开始异步操作..."); | |
| PerformAsyncOperation(ResultCallback); | |
| Console.WriteLine("主线程继续执行..."); | |
| // 防止程序过早结束 | |
| Thread.Sleep(3000); | |
| } | |
| // 模拟异步操作 | |
| static void PerformAsyncOperation(Action<string> callback) | |
| { | |
| new Thread(() => | |
| { | |
| Thread.Sleep(2000); // 模拟耗时操作 | |
| callback("操作完成,结果为:42"); | |
| }).Start(); | |
| } | |
| // 回调方法 | |
| static void ResultCallback(string result) | |
| { | |
| Console.WriteLine(result); | |
| } | |
| } |
输出:
| 开始异步操作... | |
| 主线程继续执行... | |
| 操作完成,结果为:42 |
解释:
PerformAsyncOperation方法启动了一个新线程,并在操作完成后调用回调方法ResultCallback。- 主线程不会被阻塞,能够继续执行其他操作。
- 回调方法用于处理异步操作的结果。
回调地狱(Callback Hell)🔥
回调地狱是指当多个异步操作依次依赖回调时,代码会形成嵌套的结构,导致难以维护和阅读。这种问题在复杂场景中尤为常见。
回调地狱示例
| using System; | |
| using System.Threading; | |
| class Program | |
| { | |
| static void Main(string[] args) | |
| { | |
| Console.WriteLine("开始异步任务链..."); | |
| Step1(result1 => | |
| { | |
| Console.WriteLine(result1); | |
| Step2(result2 => | |
| { | |
| Console.WriteLine(result2); | |
| Step3(result3 => | |
| { | |
| Console.WriteLine(result3); | |
| Console.WriteLine("所有步骤完成!"); | |
| }); | |
| }); | |
| }); | |
| // 防止程序过早结束 | |
| Thread.Sleep(5000); | |
| } | |
| static void Step1(Action<string> callback) | |
| { | |
| new Thread(() => { Thread.Sleep(1000); callback("步骤 1 完成"); }).Start(); | |
| } | |
| static void Step2(Action<string> callback) | |
| { | |
| new Thread(() => { Thread.Sleep(1000); callback("步骤 2 完成"); }).Start(); | |
| } | |
| static void Step3(Action<string> callback) | |
| { | |
| new Thread(() => { Thread.Sleep(1000); callback("步骤 3 完成"); }).Start(); | |
| } | |
| } |
输出:
| 开始异步任务链... | |
| 步骤 1 完成 | |
| 步骤 2 完成 | |
| 步骤 3 完成 | |
| 所有步骤完成! |
问题:
- 嵌套的回调导致代码呈现“金字塔”结构,难以阅读和维护。
- 异常处理复杂,容易遗漏错误处理逻辑。
基于事件的异步模式(EAP) 📅
EAP 是 C# 中较早的一种异步编程模型,它使用 事件 来通知异步操作的完成状态。EAP 模式通常以 BeginXXX 和 EndXXX 命名方法,或者通过事件处理完成通知。
示例代码:WebClient 的 EAP 模式
| using System; | |
| using System.Net; | |
| class Program | |
| { | |
| static void Main(string[] args) | |
| { | |
| using (WebClient client = new WebClient()) | |
| { | |
| client.DownloadStringCompleted += OnDownloadStringCompleted; | |
| Console.WriteLine("开始下载数据..."); | |
| client.DownloadStringAsync(new Uri("https://www.example.com")); | |
| // 防止程序过早结束 | |
| Console.ReadLine(); | |
| } | |
| } | |
| // 下载完成时触发的事件处理程序 | |
| static void OnDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) | |
| { | |
| if (e.Error != null) | |
| { | |
| Console.WriteLine($"下载失败:{e.Error.Message}"); | |
| return; | |
| } | |
| Console.WriteLine("下载成功,数据长度:" + e.Result.Length); | |
| } | |
| } |
输出:
| 开始下载数据... | |
| 下载成功,数据长度:1270 |
解释:
DownloadStringAsync方法启动异步下载操作。- 当下载完成后,会触发
DownloadStringCompleted事件,并调用事件处理程序OnDownloadStringCompleted。 e.Result包含下载的数据结果。
EAP 的问题
- 复杂性:每个异步操作都需要定义事件和处理程序,增加了代码的复杂性。
- 错误处理困难:异常处理需要在事件处理程序中显式检查
Error属性。 - 可维护性差:对于大量异步操作,代码的结构会变得凌乱。
基于任务的异步模式(TAP)
使用 async/await 重写回调示例
| using System; | |
| using System.Threading.Tasks; | |
| class Program | |
| { | |
| static async Task Main(string[] args) | |
| { | |
| Console.WriteLine("开始异步任务链..."); | |
| await Step1(); | |
| await Step2(); | |
| await Step3(); | |
| Console.WriteLine("所有步骤完成!"); | |
| } | |
| static async Task Step1() | |
| { | |
| await Task.Delay(1000); | |
| Console.WriteLine("步骤 1 完成"); | |
| } | |
| static async Task Step2() | |
| { | |
| await Task.Delay(1000); | |
| Console.WriteLine("步骤 2 完成"); | |
| } | |
| static async Task Step3() | |
| { | |
| await Task.Delay(1000); | |
| Console.WriteLine("步骤 3 完成"); | |
| } | |
| } |
优势:
- 代码结构更加清晰,不再有深层嵌套。
- 异常处理可以使用标准的
try/catch块。 - 异步方法的调用和同步方法类似,降低了编写和理解异步代码的难度。
解释:
GetDataAsync方法被标记为async,表示这是一个异步方法。await关键字等待GetStringAsync方法完成,而不会阻塞主线程。- 当网络请求完成后,
response字符串会返回给调用者,程序继续执行。
小结:为什么选择 async/await? 💡
随着 C# 5.0 引入 async/await 关键字,异步编程变得更加简洁和直观。相比于回调和 EAP 模式,async/await 提供了更高层次的抽象,能够以类似于同步代码的方式编写异步逻辑,极大地提升了代码的可读性和维护性。
浙公网安备 33010602011771号