.NET-异步编程

.NET-异步编程

在 C# 中,异步编程主要通过 asyncawait 关键字来实现

这种方式不仅让代码更加简洁易读,还能够有效提升应用的性能和响应能力


基本概念

  • 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表达式标记即可


底层原理

asyncawait关键字是语法糖,在底层是状态机的调用,编译之后,代码中不会存在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#";
}