.Net5学习笔记(三):异步编程

一、异步方法

异步方法:用Async关键字修饰的方法

  1. 异步方法的返回值一般是Task,T是真正的返回值类型,Task, 异步方法名称通常以async结尾
  2. 即使异步方法没有返回值,也最好把返回值声明成非泛型的Task
  3. 调用异步方法时 通常在前面叫上await关键字,这样拿到的返回值就是泛型指定的类型
  4. 异步方法具有"传染性", 一个方法中如果有await调用,则这个方法也要修饰成async方法
public static async Task<int> SumAsync(int a, int b)
{
    return await Task.Run(() => a + b);
}

二、异步方法的调用

1. 正常调用,使用await关键字

static async Task Main(string[] args)
{
  var a = 10;
  var b = 20;
  int c = await SumAsync(a, b);
  Console.WriteLine(c);
}
  1. 因为这里使用了await,所以程序运行时会直接当方法返回值Task中的int类型的值赋给c
  2. Main方法本身为void,但是当内部使用了await时,如果返回值依然用void,运行时就会报错 “程序不包含适合于入口点的静态 "Main" 方法 ”,所以需要将Main方法的返回值设为Task。

2. 不使用await调用

static async Task Main(string[] args)
{
    var a = 10;
    var b = 20;
    Task<int> d = SumAsync(a, b);
    Console.WriteLine(d.Result);
}
  1. 如果不使用await 我们就需要使用Task类型的值来接受方法返回值,调用Result属性去获取原本的返回值
  2. await关键字,实际上就是封装了这种获取返回值的方式。

3.其他情况

正常来说,如果使用了await,那么包含该异步方法的方法也要用async修饰。如果说,当前方法不允许使用async修饰,那么就是用以下方式解决

3.1. 有返回值 调用Result方法
static async Task Main(string[] args)
{
    var a = 10;
    var b = 20;
    // 1. 有返回值 调用Result方法
    c = SumAsync(a, b).Result;
    Console.WriteLine(c);
}

3.2. 没有返回值,调用Wait方法

首先添加一下简单的没有返回值的方法

public static async Task ConsoleString(string str)
{
    await Task.Run(() => Console.WriteLine(str));
}

下面是调用方式

static async Task Main(string[] args)
{
    ConsoleString("sdad").Wait();
}

需要注意的是使用这两种方式都有死锁的风险

三、 异步方法的原理

将编译器生成的dll放到反编译器中,可以得出以下结论

详细查看杨中科老师的视频

await、async是语法糖,最终编译成状态机调用,async方法会被C#编译器编译成一个类,会根据await调用进行切分成多个状态,对async方法的调用会被拆分为对多个MoveNext的调用,用await看似是“等待”,经过编译后,其实并没有wait

四、异步编程与多线程之间的管理

  1. await调用的等待期间,.Net会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。到要等待的时候,如果异步方法已经执行完毕,那么就继续在当前线程执行,没有必要在切换线程。验证代码:
public static async Task RelationShip1()
{
    try
    {
        Console.WriteLine($"当前线程id:{Thread.CurrentThread.ManagedThreadId}");
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 10000; i++)
        {
            stringBuilder.Append("sssssssssssssssssssssssssssssss");
        }

        await File.WriteAllTextAsync("RelationShip.txt", stringBuilder.ToString());

        Console.WriteLine($"当前线程id:{Thread.CurrentThread.ManagedThreadId}");
    }
    catch (Exception)
    {
        throw;
    }
}

方法运行后的输出结果:
当前线程id:1
当前线程id:4

  1. 异步方法并不会自动到一个新的线程中执行,除非手动切换,上个方法出现了线程切换是因为File.WriteAllTextAsync内部存在着线程操作(猜测)。测试代码:
public static async Task RelationShip2()
{
    Console.WriteLine($"运行前:{Thread.CurrentThread.ManagedThreadId}");
    var result = await CalcAsync(5000);
    Console.WriteLine($"Result: {result}");
    Console.WriteLine($"运行后:{Thread.CurrentThread.ManagedThreadId}");
}

public static async Task<decimal> CalcAsync(int n)
{
    Console.WriteLine($"当前线程id:{Thread.CurrentThread.ManagedThreadId}");
    decimal result = 0;
    Random random = new Random();
    for (int i = 0; i < n; i++)
    {
        result += (decimal)random.NextDouble();
    }

    return result;
}

方法运行后的输出结果:
运行前:9
当前线程id:9
Result: 2520.063400485160281679
运行后:9

五、没有async方法标识的异步方法

public static Task<string> ReadAsync(int num)
{
    switch (num)
    {
        case 0:
            return File.ReadAllTextAsync("RelationShip.txt");
        case 1:
            return File.ReadAllTextAsync("text.txt");
        default:
            throw new ArgumentOutOfRangeException(nameof(num));
    }
}

上面的方法内部虽然进行了异步操作,但是方法并没有使用async修饰,这是因为这个方法只是对File.ReadAllTextAsync的调用的转发,不包含其他的逻辑,这个时候是可以不适用async关键字修饰的

  1. 使用async修饰方法的缺点
  1. 异步方法会生成一个类,运行效率没有普通方法高
  2. 可能会占用更多的线程
  1. 没有使用async修饰的方法的优点

只返回Task 与普通方法相同,运行效率高,不会造成线程的浪费

六、CancellationToken

CancellationToken: 结构体,用来传递异步方法提前结束的信息

IsCancellationRequested:获取是否已请求取消此标记。
ThrowIfCancellationRequested(): 如果已请求取消此标记,则引发 OperationCanceledException。
None:返回一个空 CancellationToken 值。

6.1. 使用实例1 获取网页内容n次,5秒后超时停止

public static async Task Test1()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    cts.CancelAfter(5000);
    await ReadHtml(10000, cts.Token);
}
public static async Task ReadHtml(int n, CancellationToken token)
{
    using (HttpClient client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {
            string html = await client.GetStringAsync("https://www.bilibili.com/");
            Console.WriteLine(html);
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("超时停止");
                break;
            }
        }
    }
}

CancellationToken一般不直接创建对象,而是使用CancellationTokenSource

6.2. 使用实例2 获取网页内容n次,用户输入回车后停止

public static async Task Test1()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    // 此处如果添加await的话 程序不会往下执行
    _ = ReadHtml(10000, cts.Token);
    while (Console.ReadKey().Key == ConsoleKey.Enter)
    {
        // 手动停止
        cts.Cancel();
    }
}
public static async Task ReadHtml(int n, CancellationToken token)
{
    using (HttpClient client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {
            string html = await client.GetStringAsync("https://www.bilibili.com/");
            Console.WriteLine(html);
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("超时停止");
                break;
            }
        }
    }
}

6.3. 使用实例3 有调用的异步方法自行处理异步停止逻辑

public static async Task Test1()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    cts.CancelAfter(5000);
    await ReadHtml(10000, cts.Token);
}
public static async Task ReadHtml(int n, CancellationToken token)
{
    using (HttpClient client = new HttpClient())
    {
        for (int i = 0; i < n; i++)
        {
            string html = await client.GetStringAsync("https://www.bilibili.com/", token);
            Console.WriteLine(html);
        }
    }
}

超时5秒后,获取网页的一步逻辑自行停止
使用这种方式处理一步停止逻辑的话 可以更加及时的停止异步逻辑

6.4. 在Asp.Net项目中使用

在Asp.Net项目中使用时只需要在action方法中添加CancellationToken token参数,然后自行处理停止逻辑,.Net会自动注入数据,并且在用户停止操作后调用CancellationToken。

posted @ 2021-12-19 17:51  zero_night  阅读(228)  评论(0)    收藏  举报