C# async/await
最后修改于 2023 年 7 月 5 日
C# async/await 教程展示了如何在 C# 中使用 async 和 await 关键字。
通过异步编程,我们可以在主程序执行的同时并发地执行任务。async 和 await 关键字简化了 C# 中的异步编程。C# 语言内置了异步编程模型。
并发编程用于两种任务:I/O 密集型任务和 CPU 密集型任务。从网络请求数据、访问数据库或读写文件都属于 I/O 密集型任务。CPU 密集型任务是那些计算量大的任务,例如数学计算或图形处理。
async 修饰符用于方法、lambda 表达式或匿名方法上,以创建异步方法。一个 async 方法会同步运行,直到遇到第一个 await 运算符,此时方法会挂起,直到被等待的任务完成。在此期间,控制权返回给该方法的调用者。
await 运算符会挂起其所在 async 方法的执行,直到异步操作完成。当异步操作完成后,await 运算符会返回操作的结果(如果有的话)。
如果 async 方法不包含 await 运算符,则该方法会同步执行。
在 C# 中,一个 Task 代表一个并发操作。
C# 简单同步示例
在下一个示例中,我们同步执行三个方法。
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 关键字。
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 修饰符来标记它。
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 异步地打开一个文本文件,读取文件中的所有文本,然后关闭文件。
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 密集型计算。
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 响应。
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 关键字在 C# 中创建了异步程序。
作者
列出所有 C# 教程。