.NET-异步编程
.NET-异步编程
在 C# 中,异步编程主要通过 async
和 await
关键字来实现
这种方式不仅让代码更加简洁易读,还能够有效提升应用的性能和响应能力
基本概念
- async: 用于标记一个方法为异步方法。异步方法可以包含一个或多个
await
表达式。 - await: 用于等待一个异步操作的结果。当遇到
await
时,控制权会返回给调用者,直到异步操作完成。
异步方法的返回类型
- Task: 表示一个没有返回值的异步操作。
- Task<T>: 表示一个有返回值的异步操作,其中
T
是返回值的类型。 - void: 虽然可以使用
void
作为返回类型,但一般只在事件处理程序中这样做,因为这会导致无法捕获异常或取消操作。
异步方法
异步方法具有传染性,也就是说,调用异步方法的方法本身也将会成为异步方法(调用异步方法的方法必须带有async关键字)
基于此特性,我们可以将一些异步方法的调用封装成自定义的异步方法
namespace dotNet
{
internal class Program
{
static async Task Main(string[] args)
{
await DownloadHtmlAsync("https://www.youzack.com", @"D:\Project\1.txt");
}
// 封装好的自定义异步方法
static async Task DownloadHtmlAsync(string url, string filename)
{
using (HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(filename, html);
}
}
}
}
Result&Wait
如果我们封装的方法,或者调用异步方法的方法无法写成异步方法(即无法加上async关键字)
我们可以通过两个方法来调用其异步方法获取结果
需要注意的是,使用这种方法完成异步方法的调用会存在死锁的风险
static void Main(string[] args)
{
// 调用没有返回值的异步方法,在结尾链式调用.wait()方法即可
DownloadHtmlAsync("https://www.youzack.com", @"D:\Project\1.txt").Wait();
// 调用存在返回值的异步方法,在结尾链式调用.Result()方法即可
string str = DownloadHtmlAsync("https://www.youzack.com", @"D:\Project\1.txt").Result();
}
Lambda
在Lambda表达式中调用异步方法,可以用async关键字将Lambda表达式标记即可
底层原理
async
和 await
关键字是语法糖,在底层是状态机的调用,编译之后,代码中不会存在await关键字
await的中文释义是等待的意思,但其实是不等待,当代码执行到await时,将会继续往下执行而不是等待await标记的语句执行结束
先去执行其他代码,当await标记的代码执行完成后再回来
多线程
多线程可通过异步方法来实现,但异步方法并不等于多线程
如果一个异步方法中,没有调用其他封装好的异步方法(具有多线程),亦或者是没有手动的开辟一条线程去执行方法中内容,则不会实现多线程
如果想将完全自定义的异步方法实现多线程,需要调用委托函数Task.Run()将方法执行内容封装其中,则实现多线程
非多线程
namespace dotNet
{
internal class Program
{
static async Task Main(string[] args)
{
var num = await Test();
Console.WriteLine(num);
}
// 虽然这个方法是异步方法,但实际上没有实现多线程
static async Task<double> Test()
{
return 1.1;
}
}
}
多线程
namespace dotNet
{
internal class Program
{
static async Task Main(string[] args)
{
var num = await Test();
Console.WriteLine(num);
}
static async Task<double> Test()
{
// 使用Task.Run()委托,封装执行体,实现多线程
return await Task.Run(() =>
{
return 1.1;
});
}
}
}
没有async
异步方法中,可以没有async关键字,在返回的时候不直接返回泛型中的类型,而是直接将Task类型返回
在调用的时候同样使用await将其内容拆出即可
可以用的场景
如果一个异步方法只是对别的异步方法调用的转发,并没有太多的复杂的逻辑,就可以去掉async关键字
namespace dotNet
{
internal class Program
{
static async Task Main(string[] args)
{
var num = await Test();
}
// 此处去掉了async关键字
static Task<double> Test()
{
// 此处去掉了await的关键字
return Task.Run(() =>
{
return 1.1;
});
}
}
}
await Task.Delay()
在异步调用中,想要在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程
使用await Task.Delay()方法来使异步方法休眠
CancellationToken
在 C# 中,CancellationToken
是一个非常有用的机制,用于在长时间运行的操作中支持取消请求
它通常与 CancellationTokenSource
一起使用,后者负责生成和管理取消令牌
通过这种方式,可以在异步操作或长时间运行的任务中优雅地处理取消请求,避免资源浪费和不必要的计算
在.NET框架提供的很多异步方法中,都提供了带CancellationToken参数的重载方法,我们在这里要遵循一个规则——能转发则转发
在ASP.NET程序中,如果入参有CancellationToken参数传入,我们在调用其他异步方法时也将此参数传递即可,即转发此参数
CancelAfter
使用CancellationTokenSource对象调用CancelAfter设置令牌在多长时间后取消的信息
使用CancellationTokenSource对象调用.token将令牌作为参数传递到方法中
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
// 设置消息为在五秒后取消方法执行
cts.CancelAfter(5000);
// 调用方法,循环请求200次百度网站, 向方法内部发送一个信息——当方法五秒钟后未完成执行则取消执行方法
await DownloadAsync("https://www.baidu.com", 200, cts.Token);
}
Cancel
我们不止可以设置事件来发送取消任务的消息,还可以通过一些条件判断来调用Cancel方法手动取消任务
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
// 调用方法,循环请求200次百度网站, 向方法内部发送一个信息——当方法五秒钟后未完成执行则取消执行方法
DownloadAsync("https://www.baidu.com", 200, cts.Token);
// 当键盘接受到q的时候,跳出循环,执行到cts.Cancel消息
while (Console.ReadLine() != "q")
{
}
cts.Cancel();
Console.ReadLine();
}
.IsCancellationRequested
static async Task DownloadAsync(string url, int n, CancellationToken token)
{
using (HttpClient httpClient = new HttpClient())
{
// 循环发送请求网站html文件
for (int i = 0; i < n; i++)
{
string html = await httpClient.GetStringAsync(url);
// 打印请求到的内容
Console.WriteLine($"{DateTime.Now}:{html}");
// 当token带过来取消的消息时,跳出循环
if (token.IsCancellationRequested)
{
break;
}
}
}
}
.ThrowIfCancellationRequested()
static async Task DownloadAsync(string url, int n, CancellationToken token)
{
using (HttpClient httpClient = new HttpClient())
{
// 循环发送请求网站html文件
for (int i = 0; i < n; i++)
{
string html = await httpClient.GetStringAsync(url);
// 打印请求到的内容
Console.WriteLine($"{DateTime.Now}:{html}");
// 当接收到取消的消息时,直接抛出异常
token.ThrowIfCancellationRequested();
}
}
}
异步编程的问题
接口中的异步方法
在接口中定义的异步方法不需要使用async来标识
yield return
在 C# 中,yield return
是一个非常强大的特性,用于在方法中生成一个迭代器(iterator)
迭代器允许你在方法中逐步返回一系列值,而不需要一次性将所有值存储在内存中
这对于处理大量数据或生成无限序列非常有用
事实上,async方法中不能使用yield关键字进行流式调用,但C# 8.0后提供了一个方法可同时使用异步方法和yield关键字
static async Task Main(string[] args)
{
// 在foreach上使用await关键字标识
await foreach (var s in Test())
{
Console.WriteLine(s);
}
}
// 这里不使用Task,而是使用IAsyncEnumerable作为方法的返回类型
static async IAsyncEnumerable<string> Test()
{
yield return "hello";
yield return "world";
yield return "C#";
}