CodeGym /课程 /C# SELF /关键字 async

关键字 asyncawait

C# SELF
第 59 级 , 课程 2
可用

1. 介绍

在写代码之前,先搞清楚动机,想象一个典型场景。假设你的程序从互联网下载文件:

// 伪代码
var data = DownloadFile("https://example.com/file");
ProcessData(data);

问题是:在下载的时候,程序“卡住”了。其它操作都不能进行 — 用户既不能移动鼠标,也不能点击按钮,甚至只能面对一个死掉的界面无奈等待。

以前(以及在其他语言里)为了解决这个问题需要使用线程 (Thread)、任务 (Task)、委托、计时器 —— 这些会让代码堆成一团,很难读也难维护。C# 团队想把事情简单化:引入了用关键字 asyncawait 的异步。现在可以像写普通代码一样简单地写异步代码。

经典的“异步痛点”没有 async/await

为了对比,看看如果用线程来做耗时操作以避免阻塞界面,会是什么样子:

// 无 async/await 的例子,手工实现
var thread = new Thread(() =>
{
    var data = DownloadFile("https://example.com/file");
    Console.WriteLine("文件已下载!");
});
thread.Start();

这种方式挺粗糙的:你得手动管理线程,没办法方便地“等待”结果,错误处理也很麻烦。

2. C# 异步:语法

定义:什么是 asyncawait

async — 是一个修饰符,它让方法不再普通并把它变成异步。这样的方法通常返回 Task(或 Task<T>),或者 ValueTask。它是一种承诺,结果会在稍后到来(像网购下单 —— 下单然后等着收货)。

await — 是一个操作符,它的意思是:“到这行时暂停。等操作完成,但不要阻塞线程!其余的事情可以继续做。”

异步方法长什么样?

public async Task MyAsyncMethod()
{
    Console.WriteLine("正在下载文件...");
    var data = await DownloadFileAsync("https://example.com/file"); // 异步等待结果!
    Console.WriteLine("完成!");
}

注意:

  • 方法上加了修饰符 async
  • 方法内部使用了 await 来等待异步操作。
  • 方法返回 Task(如果有返回值则返回 Task<T>)。

可视化:调用 async 方法时发生了什么?

graph LR
A[调用 MyAsyncMethod] --> B[执行到 await 前]
B --> C[调用 DownloadFileAsync]
C --> D{等待}
D --> |文件未加载| E[释放线程]
D --> |文件已加载| F[await 后继续执行]
F --> G[方法完成]
  • 在第一个 await 之前,方法是同步执行的。
  • await 时方法会被挂起,控制权返回给调用者。
  • 当异步操作完成后,执行会在 await 之后继续 —— 就像什么都没发生一样。

3. 示例:使用 async/await 的异步下载

假设我们的教学用程序从网络下载文本并打印它的长度,同时不阻塞其它代码。

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

class Program
{
    // 异步方法(返回 Task)
    public static async Task DownloadAndPrintLengthAsync(string url)
    {
        Console.WriteLine("开始下载...");

        // 使用 HttpClient — 它支持异步方法
        using (var client = new HttpClient())
        {
            string data = await client.GetStringAsync(url);
            Console.WriteLine($"下载完成!文本长度:{data.Length} 个字符。");
        }

        Console.WriteLine("方法执行完毕。");
    }

    static void Main()
    {
        // 启动异步操作并等待其完成
        var task = DownloadAndPrintLengthAsync("https://www.example.com");
        task.Wait(); // 在简单的控制台例子里可以这样做。在真实的 UI 或 web 应用里,这个调用会导致阻塞并可能引起死锁。
    }
}

说明:

  • DownloadAndPrintLengthAsync — 完全是异步的,得益于 asyncawait
  • 在方法内部我们通过 await 等待异步下载字符串的完成。
  • Main() 中我们启动任务并通过 Wait() 显式等待。在现代 C# 中你也可以让 Main 本身是异步的(async Task Main),然后直接用 await

4. 它是怎么工作的?

同步代码 vs 异步代码的区别

同步示例

Console.WriteLine("开始");
string data = client.GetStringAsync(url).Result; // .Result 会阻塞线程!
Console.WriteLine("操作完成");

异步示例

Console.WriteLine("开始");
string data = await client.GetStringAsync(url); // 不会阻塞线程
Console.WriteLine("操作完成");

await 在“引擎”里是怎么工作的?

当你使用 await 时,C# 会自动把你的方法“拆成”两部分(或更多):await 之前的部分和之后的部分。当被调用的异步方法返回 Task 时,你的方法会先返回给调用者;当该 Task 完成时,执行会在 await 之后继续。所有这些都是自动完成的;你不需要手动管理线程和切换。

有趣的事实: C# 会把你的异步方法变成一个状态机,每个 await 都是一个新的“恢复点”。

在实际应用里使用 async/await

static async Task Main(string[] args)
{
    var downloadTask = DownloadAndPrintLengthAsync("https://www.example.com");
    // 在下载进行时,做点别的事情
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"正在工作... 迭代 {i}");
        await Task.Delay(500); // 暂停 0.5 秒,模拟工作
    }
    await downloadTask; // 等待下载完成
}

现在程序既在下载,又不会被阻塞:是真正的“活”多任务,就像一只既睡着又盯着饭碗的猫。

异步 ≠ 多线程

一个常被初学者混淆的重要区别:异步代码 可以在同一个线程上执行!异步的本质是不要阻塞线程,而不是必须创建新线程。操作系统的线程是昂贵的资源!异步允许“释放”线程去做别的事,而不是等待耗时操作(网络、文件、定时器等)。

比较表:什么时候选择哪个?

场景 Thread/Task async/await
CPU 密集型任务 是(通过 Task.Run
I/O 密集型任务 效率低 理想
大量并行 复杂 简单
代码简洁性 很难 易读

5. 有用的细节

异步 Main

在现代 C# 中,Main 也可以是异步的!

static async Task Main(string[] args)
{
    // 你可以在 Main 里直接 await 所有异步代码
    await DownloadAndAnalyzeFileAsync("https://example.com/file");
}

典型错误:忘记 await — 任务“泄漏”

SomeAsyncFunction(); // 没有 await,没人等它完成!

结果这个任务会在“无人治理”的情况下继续执行 —— 如果出错,你可能根本不会知道!

什么时候不应该写 async 方法

  • 如果方法内部没有任何异步操作(没有任何 await),就不要把它标记为 async
  • 不要写 async void 的方法(除非你是在写事件处理器)。

简要规则和常见问题

  • 能在 async 方法外使用 await 吗? 不能!必须在标记为 async 的方法内部使用 await
  • 一个方法里可以有多个 await 吗? 可以,数量不限——每个都是一个等待点。
  • 想返回结果怎么办? 使用 Task<T> 并用 return 返回。
  • 能组合多个异步任务吗? 当然可以!可以并行启动多个任务并通过 Task.WhenAll 等待它们。

6. 常见错误 使用 async/await

错误 №1:忘记等待 — 在调用异步任务后没用 await

DownloadAndPrintLengthAsync("https://www.example.com");
Console.WriteLine("全部完成!"); // 实际上下载还在进行中!

代码不会等待异步操作完成。所有异步任务要么被 await,要么被显式地用 Wait() 等待(但后者危险,可能导致阻塞)。

错误 №2:在同步和异步代码之间混用 .Result.Wait()

这是个反模式,会把异步的好处全部抹掉。在 UI 或 ASP.NET 应用中几乎肯定会导致死锁(异步任务等着线程释放,而线程被等待任务阻塞)。记住口头禅: "async all the way"(从底到顶都异步)。

Ошибка №3: 使用 async void.

带有 async void 的方法无法被等待(不能用 await),而且从它们抛出的异常不能被常规的 try-catch 捕获,通常会导致应用崩溃。唯一合理的场景是事件处理器(例如 async void Button_Click(...)),因为事件签名要求如此。其他场合请使用 async Task

Ошибка №4: 多余的 async 在没有 await 的方法里。

如果你把方法标记为 async,但内部没有使用任何 await,编译器会给出警告。这样的代码会完全同步执行,但会多出不必要的状态机开销,既令人误解又降低性能。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION