CodeGym /课程 /C# SELF /异步方法的结构 ( async/

异步方法的结构 ( async/ await)

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

1. 介绍

想象你的代码像商店的队列。如果你运行同步方法,整个线程(你的队列)会等收银员处理完一个顾客才开始下一个。如果方法是异步的,你就把东西放到收银台然后去做别的事,等收银员处理好会叫你回来。

在 .NET 里,异步方法(带 async 的方法)是普通方法,只是在前面有个特殊的“标记”(async),它允许在方法内部使用 await 来对其他异步操作(比如网络或文件操作)进行非阻塞等待而不阻塞主线程。正是这个“标记”把普通的同步商店变成了现代化的无队列服务。

异步方法的基础签名

异步方法必须在签名上声明 async 修饰符。下面是最基础的例子:

public async Task MyAsyncMethod()
{
    // 你的异步代码写这里
}

异步方法的返回类型:

返回类型 说明 示例
Task
方法异步执行,但不返回结果
public async Task SaveFileAsync()
Task<T>
方法异步执行并返回类型为 T 的结果
public async Task<int> GetSumAsync()
void
只用于事件处理。其他情况不推荐!
public async void Button_Click(...)
ValueTask / ValueTask<T>
用于高性能场景,当结果经常同步可用时

重要: 除了事件处理器别使用 async void!否则你可能无法正常处理异常。

2. 简单的异步方法示例

回到我们的练习程序(假设是个普通控制台程序),添加一个从网络获取数据的异步方法。我们将模拟延迟。

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("启动异步方法...");
        await MyFakeDownloadAsync();
        Console.WriteLine("异步操作完成!");
    }

    static async Task MyFakeDownloadAsync()
    {
        Console.WriteLine("开始下载(模拟 2 秒延迟)...");
        await Task.Delay(2000); // 模拟耗时操作
        Console.WriteLine("下载完成!");
    }
}

示例说明:

  • 关键字 async 用在 MainMyFakeDownloadAsync 上。
  • 操作符 await 只能在标记为 async 的方法内部使用。
  • 方法 Task.Delay(2000) 模拟一个耗时操作(比如网络请求)。
  • 在遇到 await 后线程不会被阻塞——Main 的执行会“挂起”直到延迟完成。

3. 异步方法在底层是如何工作的?

当编译器看到带有 async 的方法时,会把它转换成一个“状态机”,记录暂停点、局部变量,并在 awaited 操作完成后继续执行。

下面是一个非常简化的示意:


+-----------------------------------------------------------+
|          调用异步方法(例如 await X)                    |
+-------------------------+---------------------------------+
                          |
                          v
            操作完成了吗?(不是 Task.Completed)
                |                  |
                | 是               | 否
                v                  v
          暂停执行,             立即返回
          记住上下文             结果
                |
      异步操作完成...
                |
                v
        状态机“唤醒”方法,
        从 await 之后的位置继续执行

不需要记住所有实现细节:上下文(暂停点和局部变量)会被保存,并在操作完成时自动恢复。

在哪些地方可以(或不能)使用 asyncawait

  • async 放在方法(或 lambda)声明前面。
  • async 方法内部可以(而且常常需要)使用 await
  • 如果在没有 async 的方法中使用 await——会编译错误。
  • 如果写了 async 但方法里没有任何 await——会有警告;方法会同步执行并返回已完成的任务。

4. 返回值选项及其特点

返回 Task

如果方法执行异步操作但不返回值,使用 Task

public async Task SaveToFileAsync(string path)
{
    await Task.Delay(1000);
    // 保存东西到文件
}

返回 Task<T>

如果需要返回结果,使用 Task<T>

public async Task<int> CalculateAsync()
{
    await Task.Delay(500);
    return 42;
}

调用:

int result = await CalculateAsync();

结果为 async void

仅用于事件!例如按钮点击处理器:

private async void Button_Click(object sender, EventArgs e)
{
    await Task.Delay(1000);
    // 再做点别的
}

为什么不要在普通方法中使用 async void?因为你无法得知该方法何时完成,也无法正确捕获其中的异常。

返回 ValueTaskValueTask<T>

它们为高性能库而生,当结果经常同步可用时可以避免额外的任务分配。刚开始学的时候可以不使用:不那么常见。

5. 示例:返回结果的异步计算器

假设程序“带延迟”计算两个数字的和,好像这是个很复杂的操作:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("请输入第一个数字:");
        int a = int.Parse(Console.ReadLine()!);

        Console.WriteLine("请输入第二个数字:");
        int b = int.Parse(Console.ReadLine()!);

        Console.WriteLine("正在计算...");
        int sum = await CalculateSumAsync(a, b);
        Console.WriteLine($"和: {sum}");
    }

    static async Task<int> CalculateSumAsync(int x, int y)
    {
        await Task.Delay(2000); // “耗时”操作
        return x + y;
    }
}

这里需要注意的:

  • CalculateSumAsync 是异步方法,返回 Task<int>,内部使用了 await
  • Main 中使用了 await 来调用它(提示:从 C# 7.1 起 Main 可以是异步的)。
  • 计算和的时候线程不会阻塞——可以显示进度或做其它工作。

6. 嵌套方法和嵌套 await

异步方法可以调用另一个异步方法,而那个方法又可以调用另一个。整个链会自动继续:

public async Task<int> StartWorkAsync()
{
    int data = await GetDataAsync();
    int result = await ProcessDataAsync(data);
    return result;
}

很简单:等待一个操作完成,然后等待下一个。

7. 陷阱和常见错误

1. 别忘了写 async
如果忘记了 async,编译器会因为在方法内部看到 await 而报错,代码无法编译。

2. 除了事件处理器别用 async void
异步 void 方法(除了事件处理器)是异常的“黑洞”。里面的错误可能会被吃掉,你可能根本不会知道。

3. 异步方法没有 await
可以这样写,但没意义——方法会同步执行并返回一个已完成的任务。

public async Task DoNothingAsync()
{
    // 哎,根本没有 await!
}

4. 从非异步方法返回 Task
这样做是为了统一接口:有些操作真实是异步的,有些是瞬时完成的。

public Task<int> Foo()
{
    // return 42; // 这样不行!
    return Task.FromResult(42); // 可以,但这里没有任何异步性。
}
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION