第二章:C#异步编程简介

第二章:异步编程简介 🚀


异步编程(Asynchronous Programming)是现代编程中处理 I/O 密集型任务的关键技术之一。与传统的同步编程不同,异步编程允许程序在等待耗时操作(如网络请求、文件读写、数据库查询等)完成时,继续执行其他任务,从而提高应用程序的响应性和效率。

在本章中,我们将介绍异步编程的基本概念、C# 中的异步编程模型以及常用的 asyncawait 关键字。我们还会探讨异步编程的一些常见应用场景和最佳实践,帮助你理解如何在实际开发中使用异步编程来提升性能。

2.1 异步编程的基本概念 🤔

同步 vs 异步

  • 同步编程 🛑:在同步编程中,任务是顺序执行的。如果某个操作是耗时的(如网络请求),程序会阻塞等待操作完成,之后才会继续执行下一行代码。这种方式简单直观,但在处理 I/O 密集型任务时,可能会导致程序变得不响应。
  • 异步编程 🔄:异步编程允许程序在执行耗时操作时,不必等待操作完成,而是立即返回继续执行其他任务。耗时操作完成后,会通过回调、事件或 Task 通知程序结果。这样,程序不会被阻塞,能更好地利用系统资源。

异步编程的优势 🌟

  1. 提高性能和吞吐量:异步编程避免了线程的阻塞,使得应用程序可以处理更多的任务。
  2. 提升用户体验:对于 GUI 应用程序,异步编程能保持界面的响应性,避免“卡顿”现象。
  3. 更好地利用资源:异步编程通常使用线程池来管理后台任务,避免了频繁创建和销毁线程的开销。

2.2 C# 中的异步编程模型简介 🛠️

C# 提供了多种方式来实现异步编程,其中最常用的是基于 Taskasync/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 引入了 Taskasync/await,这是一种更加现代和简洁的异步编程方式,以同步编码的方式编写异步代码。

asyncawait 关键字 🔑

  • async:用于标记一个方法为异步方法。异步方法通常返回 TaskTask<T>,表示异步操作的结果。
  • await:用于等待一个异步操作的完成,并在操作完成后继续执行后续代码。await 会释放当前线程,让它去处理其他任务,避免了阻塞。

APMEAP基本已经被淘汰了,在本系列文章后续也不会再过多介绍。详情参考官网

示例代码

基于回调(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 模式通常以 BeginXXXEndXXX 命名方法,或者通过事件处理完成通知。

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

posted @ 2024-12-08 23:50  平元兄  阅读(24)  评论(0编辑  收藏  举报