第二章: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
提供了更高层次的抽象,能够以类似于同步代码的方式编写异步逻辑,极大地提升了代码的可读性和维护性。