ZetCode

C# async/await

最后修改于 2023 年 7 月 5 日

C# async/await 教程展示了如何在 C# 中使用 async 和 await 关键字。

通过异步编程,我们可以在主程序执行的同时并发地执行任务。asyncawait 关键字简化了 C# 中的异步编程。C# 语言内置了异步编程模型。

并发编程用于两种任务:I/O 密集型任务和 CPU 密集型任务。从网络请求数据、访问数据库或读写文件都属于 I/O 密集型任务。CPU 密集型任务是那些计算量大的任务,例如数学计算或图形处理。

注意: MSDN 文档错误地使用了术语“异步 (asynchronous)”而不是标准使用的“并发 (concurrent)”。

async 修饰符用于方法、lambda 表达式或匿名方法上,以创建异步方法。一个 async 方法会同步运行,直到遇到第一个 await 运算符,此时方法会挂起,直到被等待的任务完成。在此期间,控制权返回给该方法的调用者。

await 运算符会挂起其所在 async 方法的执行,直到异步操作完成。当异步操作完成后,await 运算符会返回操作的结果(如果有的话)。

如果 async 方法不包含 await 运算符,则该方法会同步执行。

在 C# 中,一个 Task 代表一个并发操作。

C# 简单同步示例

在下一个示例中,我们同步执行三个方法。

Program.cs
using System.Diagnostics;

var sw = new Stopwatch();
sw.Start();

f1();
f2();
f3();

sw.Stop();

var elapsed = sw.ElapsedMilliseconds;
Console.WriteLine($"elapsed: {elapsed} ms");

void f1() 
{
    Console.WriteLine("f1 called");
    Thread.Sleep(4000);
}

void f2() 
{
    Console.WriteLine("f2 called");
    Thread.Sleep(7000);
}

void f3() 
{
    Console.WriteLine("f3 called");
    Thread.Sleep(2000);
}

我们使用 Thread.Sleep 来模拟一些耗时较长的计算。

var sw = new Stopwatch();
sw.Start();

我们使用 Stopwatch 来测量方法的执行时间。

f1();
f2();
f3();

这些方法被依次调用。

$ dotnet run
f1 called
f2 called
f3 called
elapsed: 13034 ms

在我们的系统上,执行这三个函数花费了 13 秒。

C# 简单异步示例

现在,这个例子被重写为使用 async/await 关键字。

Program.cs
using System.Diagnostics;


var sw = new Stopwatch();
sw.Start();

Task.WaitAll(f1(), f2(), f3());

sw.Stop();

var elapsed = sw.ElapsedMilliseconds;
Console.WriteLine($"elapsed: {elapsed} ms");

async Task f1()
{
    await Task.Delay(4000);
    Console.WriteLine("f1 finished");
}

async Task f2()
{
    await Task.Delay(7000);
    Console.WriteLine("f2 finished");
}

async Task f3()
{
    await Task.Delay(2000);
    Console.WriteLine("f3 finished");
}

我们测量三个异步方法的执行时间。

Task.WaitAll(f1(), f2(), f3());

Task.WaitAll 等待所有提供的任务完成执行。

async Task f1()
{
    await Task.Delay(4000);
    Console.WriteLine("f1 finished");
}

f1 方法使用了 async 修饰符并返回一个 Task。在方法体内部,我们对 Task.Delay 使用了 await 运算符。

$ dotnet run
f3 finished
f1 finished
f2 finished
elapsed: 7006 ms

现在执行花费了 7 秒。同时请注意,任务完成的顺序是不同的。

C# 异步 Main 方法

当我们在 Main 方法内部使用 await 运算符时,必须用 async 修饰符来标记它。

Program.cs
using System.Diagnostics;


namespace AsyncMain
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var sw = new Stopwatch();
            sw.Start();

            Console.WriteLine("task 1");
            Task task1 = doWork();

            Console.WriteLine("task 2");
            Task task2 = doWork();

            Console.WriteLine("task 3");
            Task task3 = doWork();

            await Task.WhenAll(task1, task2, task3);

            Console.WriteLine("Tasks finished");

            sw.Stop();

            var elapsed = sw.ElapsedMilliseconds;
            Console.WriteLine($"elapsed: {elapsed} ms");
        }

        static async Task doWork()
        {
            await Task.Delay(1500);
        }
    }
}

Main 方法内部,我们调用了三次 doWork

$ dotnet run
task 1
task 2
task 3
Tasks finished
elapsed: 1550 ms

C# 异步读取文件

C# 有许多内置方法可以异步读取文件。例如,File.ReadAllTextAsync 异步地打开一个文本文件,读取文件中的所有文本,然后关闭文件。

Program.cs
using System.Diagnostics;


var task1 = File.ReadAllTextAsync("data1.txt");
var task2 = File.ReadAllTextAsync("data2.txt");
var task3 = File.ReadAllTextAsync("data3.txt");
var task4 = File.ReadAllTextAsync("data4.txt");

Console.WriteLine("doing some work");

var tasks = new Task[] { task1, task2, task3, task4 };

Task.WaitAll(tasks);

var content1 = await task1;
var content2 = await task2;
var content3 = await task3;
var content4 = await task4;

Console.WriteLine(content1.TrimEnd());
Console.WriteLine(content2.TrimEnd());
Console.WriteLine(content3.TrimEnd());
Console.WriteLine(content4.TrimEnd());

在本例中,我们异步读取四个文件。

var content1 = await task1;

我们使用 await 异步地解包任务的结果。

C# CPU 密集型异步任务

在下一个示例中,我们处理 CPU 密集型计算。

Program.cs
var tasks = new List<Task<int>>();

tasks.Add(Task.Run(() => DoWork1()));
tasks.Add(Task.Run(() => DoWork2()));

await Task.WhenAll(tasks);

Console.WriteLine(await tasks[0]);
Console.WriteLine(await tasks[1]);

async Task<int> DoWork1()
{
    var text = string.Empty;

    for (int i = 0; i < 100_000; i++)
    {
        text += "abc";
    }

    Console.WriteLine("concatenation finished");

    return await Task.FromResult(text.Length);
}

async Task<int> DoWork2()
{
    var text = string.Empty;

    for (int i = 0; i < 100_000; i++)
    {
        text = $"{text}abc";
    }

    Console.WriteLine("interpolation finished");

    return await Task.FromResult(text.Length);
}

我们有两个计算密集型的方法,它们会将字符串连接十万次。

tasks.Add(Task.Run(() => DoWork1()));
tasks.Add(Task.Run(() => DoWork2()));

Task.Run 方法将指定的工作排队到线程池上运行,并为该工作返回一个 task 或 Task<TResult> 句柄。

return await Task.FromResult(text.Length);

我们通过 text.Length 返回连接的字符数。Task.FromResult 创建一个已成功完成并带有指定结果的 Task<TResult>。

C# 多个异步请求

HttpClient 类用于从指定资源发送 HTTP 请求和接收 HTTP 响应。

Program.cs
using System.Text.RegularExpressions;

var urls = new string[] { "http://webcode.me", "http://example.com",
    "http://httpbin.org", "https://ifconfig.me", "http://termbin.com",
    "https://github.com"
};

var rx = new Regex(@"<title>\s*(.+?)\s*</title>",
  RegexOptions.Compiled);

using var client = new HttpClient();

var tasks = new List<Task<string>>();

foreach (var url in urls)
{
    tasks.Add(client.GetStringAsync(url));
}

Task.WaitAll(tasks.ToArray());

var data = new List<string>();

foreach (var task in tasks)
{
    data.Add(await task);
}

foreach (var content in data)
{
    var matches = rx.Matches(content);

    foreach (var match in matches)
    {
        Console.WriteLine(match);
    }
}

我们异步下载给定的网页并打印它们的 HTML 标题标签。

tasks.Add(client.GetStringAsync(url));

GetStringAsync 向指定的 URL 发送一个 GET 请求,并在一个异步操作中以字符串形式返回响应体。它返回一个新的 task。

Task.WaitAll(tasks.ToArray());

Task.WaitAll 等待所有提供的任务完成执行。

data.Add(await task);

await 解包操作的结果。

$ dotnet run
<title>My html page</title>
<title>Example Domain</title>
<title>httpbin.org</title>
<title>termbin.com - terminal pastebin</title>
<title>GitHub: Where the world builds software · GitHub</title>

来源

使用 async 和 await 进行异步编程

在本文中,我们使用 async/await 关键字在 C# 中创建了异步程序。

作者

我的名字是 Jan Bodnar,我是一名充满热情的程序员,拥有丰富的编程经验。我从 2007 年开始撰写编程文章。至今,我已经撰写了超过 1400 篇文章和 8 本电子书。我拥有超过十年的编程教学经验。

列出所有 C# 教程