C# 中的Async 和 Await 的用法详解

async/await 是 C# 中用于编写异步代码的语法糖,它基于 TaskTask<T> 实现,让异步代码看起来更像同步代码,提高了可读性和可维护性。

实例讲解

我们将采用控制台应用程序进行演示。

假设我们分别使用了两种方法,即Method 1和Method 2,这两种方法不相互依赖,而Method 1需要很长时间才能完成它的任务。在同步编程中,它将执行第一个Method 1,并等待该方法的完成,然后执行Method 2。

第一个例子

在这个例子中,我们将采取两个不相互依赖的方法。

cs 复制代码
    class Program
    {
        static void Main(string[] args)
        {
            Method1();
            Method2();
            Console.ReadKey();
        }

        public static async Task Method1()
        {
            await Task.Run(() =>
            {
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine(" Method 1");
                }
            });
        }


        public static void Method2()
        {
            for (int i = 0; i < 25; i++)
            {
                Console.WriteLine(" Method 2");
            }
        }
    }

在上面给出的代码中,Method 1和Method 2不相互依赖,我们是从主方法调用的。

在这里,我们可以清楚地看到,方法1和方法2并不是在等待对方完成。

典型输出
复制代码
主线程 ID: 1
进入 Method1 - 线程 ID: 1
Task.Run 内部 - 线程 ID: 4    // 线程池线程
进入 Method2 - 线程 ID: 1    // 主线程继续执行
 Method 2
 Method 2
 Method 1                   // 两个线程的输出交错
 Method 2
 Method 1
 ...

第二个例子

如果任何第三个方法(如Method 3)都依赖于Method 1,那么它将在Wait关键字的帮助下等待Method 1的完成。我们将创建一个新的方法,作为CallMethod,在这个方法中,我们将调用我们的所有方法,分别为Method 1、Method 2和Method 3。

cs 复制代码
    class Program
    {
        static void Main(string[] args)
        {
            callMethod();
            Console.ReadKey();
        }

        public static async void callMethod()
        {
            Task<int> task = Method1();
            Method2();
            int count = await task;
            Method3(count);
        }

        public static async Task<int> Method1()
        {
            int count = 0;
            await Task.Run(() =>
            {
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine(" Method 1");
                    count += 1;
                }
            });
            return count;
        }

        public static void Method2()
        {
            for (int i = 0; i < 25; i++)
            {
                Console.WriteLine(" Method 2");
            }
        }

        public static void Method3(int count)
        {
            Console.WriteLine("Total count is " + count);
        }
    }

在上面给出的代码中,Method 3需要一个参数,即Method 1的返回类型。在这里,await关键字对于等待Method 1任务的完成起着至关重要的作用。

典型输出
复制代码
callMethod 线程: 1
进入 Method1 线程: 1
Task.Run 线程: 4                // 线程池线程执行循环
启动 Method2 线程: 1            // 主线程继续执行 Method2
 Method 2
 Method 1                      // 两个线程的输出交错
 Method 2
 Method 1
 ...
等待结果的线程: 1
Method1 返回结果线程: 4
调用 Method3 线程: 1
Total count is 100

什么时候开始异步?

当被调用方法内部遇到第一个 await 时。如果方法内部没有await那么当前方法会同步执行。

假设你要开发一个桌面应用,点击按钮后下载文件并显示进度条。以下是关键代码:

cs 复制代码
private async void DownloadButton_Click(object sender, EventArgs e)
{
    // 1. 点击按钮,代码在 UI 线程执行
    progressBar.Value = 0;
    statusLabel.Text = "开始下载...";
    downloadButton.Enabled = false;

    try
    {
        // 2. 调用异步方法,注意这里有 await!
        await DownloadFileAsync("https://example.com/file.zip", 
                               new Progress<int>(percent => {
                                   // 5. 更新进度条的代码在 UI 线程执行
                                   progressBar.Value = percent;
                                   statusLabel.Text = $"下载中: {percent}%";
                               }));

        statusLabel.Text = "下载完成!";
    }
    catch (Exception ex)
    {
        statusLabel.Text = $"错误: {ex.Message}";
    }
    finally
    {
        downloadButton.Enabled = true;
    }
}

private async Task DownloadFileAsync(string url, IProgress<int> progress)
{
    using var client = new HttpClient();
    
    // 3. 发起 HTTP 请求,这里是异步操作
    using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
    response.EnsureSuccessStatusCode();

    var totalBytes = response.Content.Headers.ContentLength;
    using var contentStream = await response.Content.ReadAsStreamAsync();
    
    // 创建本地文件流
    using var fileStream = new FileStream("downloaded_file.zip", FileMode.Create);
    
    var buffer = new byte[8192];
    var bytesRead = 0;
    var totalBytesRead = 0L;

    // 4. 循环读取数据并写入文件,整个过程异步执行
    while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
    {
        await fileStream.WriteAsync(buffer, 0, bytesRead);
        totalBytesRead += bytesRead;
        
        // 计算进度并报告
        if (totalBytes.HasValue && progress != null)
        {
            var percent = (int)(100 * totalBytesRead / totalBytes.Value);
            progress.Report(percent);
        }
    }
}

执行流程详解

1. 初始状态
  • 用户点击按钮,DownloadButton_ClickUI 线程 执行。
  • UI 更新(进度条归零、按钮禁用)。
2. 遇到 await 关键字
复制代码
await DownloadFileAsync(...); // 关键点!
  • 异步执行开始DownloadFileAsync 方法被调用,但遇到第一个 await 时(如 client.GetAsync):
    • 释放当前线程(即 UI 线程),允许 UI 继续响应(如拖动窗口、点击其他按钮)。
    • 返回一个未完成的 Task 给调用者。
3. 后台执行异步操作
  • 网络请求HttpClient.GetAsync线程池线程 中执行(但无需手动管理线程)。
  • 文件写入fileStream.WriteAsynccontentStream.ReadAsync 同样在 线程池线程 中执行。
4. 进度更新如何回到 UI 线程?
复制代码
progress.Report(percent); // 在下载方法中调用
  • 自动上下文恢复Progress<T> 的回调函数会自动在 UI 线程 执行(因为 DownloadButton_Click 最初在 UI 线程启动)。
  • 无需手动同步 :这是 async/await 的魔法之一!
5. 异步操作完成
  • 当所有 await 操作完成后,DownloadButton_Click 从上次暂停的位置继续执行:

    csharp

    复制代码
    statusLabel.Text = "下载完成!"; // 回到 UI 线程执行

流程图:线程切换过程

复制代码
用户点击按钮
↓
UI 线程执行 DownloadButton_Click()
↓
调用 DownloadFileAsync()
↓
遇到第一个 await (client.GetAsync)
├─ 释放 UI 线程,允许 UI 继续响应
└─ 在后台线程执行网络请求
↓
网络请求完成
↓
继续执行 DownloadFileAsync()
↓
循环读取文件数据 (ReadAsync/WriteAsync)
└─ 每次循环都在后台线程执行,不阻塞 UI
↓
进度更新 (progress.Report)
└─ 自动在 UI 线程执行回调
↓
所有 await 完成
↓
UI 线程继续执行 DownloadButton_Click() 的剩余代码
相关推荐
uyeonashi1 小时前
【QT】窗口详解
开发语言·c++·qt·学习
Hello eveybody2 小时前
C++介绍整数二分与实数二分
开发语言·数据结构·c++·算法
jmlinux3 小时前
从 C 语言计算器到串口屏应用
c语言·开发语言
yuren_xia3 小时前
RabbitMQ 知识详解(Java版)
java·rabbitmq·java-rabbitmq
Mallow Flowers4 小时前
Python训练营-Day31-文件的拆分和使用
开发语言·人工智能·python·算法·机器学习
kfyty7254 小时前
轻量级 ioc 框架 loveqq,支持接口上传 jar 格式的 starter 启动器并支持热加载其中的 bean
java·jvm·ioc·jar·热加载
早起鸟儿4 小时前
docker-Dockerfile 配置
java·linux·运维·docker
云边小网安4 小时前
java集合篇(六) ---- ListIterator 接口
java·开发语言·青少年编程·java集合
都叫我大帅哥4 小时前
Spring WebFlux:响应式编程的“未来战士”还是“花架子”?
java·spring·flux
都叫我大帅哥4 小时前
Reactor 深度解析:响应式编程的「核反应堆」是如何工作的?
java·spring