异步编程学习笔记

1 为什么用异步编程

餐馆点菜的方式有:1. 通过服务员点菜。服务员拿着菜单站在桌子旁边,你要什么她给你点什么
2.给你菜单,你自己点,她去服务别的顾客
这两种方式有哪些不同呢?方式1,服务员一次只能服务一桌顾客,其他顾客都得等着;方式2,服务员可以服务多桌顾客。其实这和服务器响应请求的模式很像。

方式1的处理方式就像下图,每次user(顾客)给服务器(服务员)发送一条请求(点菜),服务器再给处理程序发送请求,处理程序处理完成后告诉服务器,服务器再返回给user。

发送请求
发送请求
user
web服务器
web处理程序
方式1
方式2的处理方式就像下图,user一次可以给web服务发送多条请求(一个服务员一次可以服务多桌顾客)
发送请求1
发送请求2
发送请求3
发送请求
user
web服务器
web处理程序

所以,使用异步编程可以增加服务器的响应数量。

2 异步编程的误区

  1. 异步编程可以加快单个请求的处理速度
    还是以点餐为例。方式1和方式2相比,方式2可以减少顾客等待的时间吗?很显然,不能。因为等待时间取决于后厨做菜所花费的时间,虽然和点餐花费时间也有关系,但是并不是由点餐花费时间决定的。所以,异步编程可以加快单个请求的处理速度,它只是能够让web服务器处理更多的请求。

  2. 异步编程是多线程
    在c#中,可以用async await进行调用异步方法进行多线程编程,但是异步编程不是多线程。

3 async await 基本使用

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

  1. 异步方法的返回值一般是Task,T是真正的返回类型,例如Task。异步方法一般以async结尾,例如 GetAsync()
  2. 即使异步方法没有返回值,也最好把返回值类型声明为非范型的Task
public void Get() {} 

public async Task Get(){} // get的异步方法 没有返回值,返回类型为task
  1. 调用异步方法时,一般在方法前加上await关键字,这样拿到的返回值就是范型指定的T类型
  2. 异步方法的传染性:一个方法中如果有await调用,则这个方法必须用async修饰

现在给目录下写一个txt,然后将写入内容再次读出来。

    static void Main(string[] args)
    {
        // 同步方法
        string path = @"~Documents\学习\1.txt";
        File.WriteAllText(path, "hello");
        string content = File.ReadAllText(path);
        Console.WriteLine(content);

        Console.ReadKey();
    }

现在来用异步方法

    static void Main(string[] args)
    {
        // 异步方法
        string path = @"~Documents\学习\1.txt";
        await File.WriteAllTextAsync(path, "hello"); // 用await调用异步方法
        string content = File.ReadAllText(path);
        Console.WriteLine(content);

        Console.ReadKey();
    }

我们的main方法没有加async关键字,会报错。其实在用await时,vs会自动帮我们给方法加上async关键字,这里是为了演示错误,我故意去掉了。
在这里插入图片描述
正确写法

    static async Task Main(string[] args) // 方法中有await时,方法必须加上async关键字;方法没有返回值时,返回值类型用task
    {
        // 异步方法
        string path = @"~Documents\学习\1.txt";
        await File.WriteAllTextAsync(path, "hello"); // 用await调用异步方法
        //ReadAllTextAsync的返回值类型其实是Task<string>,
        //但是因为用await调用异步方法可以直接得到T类型的实际类型,所以这里可以直接用stirng来接收返回值
        string content = await File.ReadAllTextAsync(path); 
        Console.WriteLine(content);

        Console.ReadKey();
    }

当一个方法既有同步方法又有异步方法时,我们推荐使用异步方法,因为异步方法可以提高系统的并发量。.net core中,有些方法就只有异步方法了,微软去掉了它的同步方法。

4. 编写异步方法

1.读取网页内容,并把它写入到文件中

  1. 无返回值
    // 调用自己写的异步方法
    static async Task Main(string[] args) // 方法中有await时,方法必须加上async关键字;方法没有返回值时,返回值类型用task
    {
        // 异步方法
        string path = @"~Documents\学习\1.txt";
        string url = "https://www.baidu.com/";
        await DownloadHtml(url, path);
        Console.ReadKey();
    }

    public static async Task DownloadHtml(string url, string path)
    {
        using (HttpClient httpClient = new HttpClient())
        {
            string html = await httpClient.GetStringAsync(url);
            await File.WriteAllTextAsync(path, html);
        }

    }
  1. 有返回值
    异步方法的返回值类型是Task,但是我们在方法中可以直接返回int类型,因为它会自动转换成Task类型,在program中,我们可以直接打印,因为程序会自动将Task里面的int取出来。
    public static async Task<int> DownloadHtml(string url, string path)
    {
        using (HttpClient httpClient = new HttpClient())
        {
            string html = await httpClient.GetStringAsync(url);
            await File.WriteAllTextAsync(path, html);
            return html.Length;
        }
    }

    static async Task Main(string[] args) // 方法中有await时,方法必须加上async关键字;方法没有返回值时,返回值类型用task
    {
        // 异步方法
        string path = @"~Documents\学习\1.txt";
        string url = "https://www.baidu.com/";
        //await DownloadHtml(url, path);
        Console.WriteLine(await DownloadHtml(url, path));

        Console.ReadKey();
    }

2.如果方法不支持加关键字async,但是又必须调用异步方法,那该怎么办?

  1. 异步方法有返回值 用.Result
    //假如main方法不支持添加async关键字,但是又必须调用异步方法
    static void Main(string[] args)
    {
        // 异步方法
        string path = @"~Documents\学习\1.txt";
        string s = File.ReadAllTextAsync(path).Result; // 不用await关键字 直接调用,然后用.result得到结果
        Console.WriteLine(s);
        Console.ReadKey();
    }
  1. 异步方法没有返回值 用.wait()
    //假如main方法不支持添加async关键字,但是又必须调用异步方法
    // 1. 有返回值 用result
    //2. 无返回值 用wait
    static void Main(string[] args)
    {
        // 异步方法
        string path = @"~Documents\学习\1.txt";
        File.WriteAllTextAsync(path, "hahahhahahahah").Wait(); // 无返回值
        string s = File.ReadAllTextAsync(path).Result; // 不用await关键字 直接调用,然后用.result得到结果
        Console.WriteLine(s);
        Console.ReadKey();
    }

但是尽量不要用这两种方法,因为会有死锁的风险。.netcore中,大部分都支持异步方法,所以不到逼不得已,不要使用result wait。

3. 异步委托

如果委托中用到了异步方法,则需要在lamda表达式前面加上async关键字。

    //异步委托
    static async Task Main(string[] args)
    {
        // 异步方法
        string path = @"~Documents\学习\1.txt";
        ThreadPool.QueueUserWorkItem(async (o) =>
        {
            await File.WriteAllTextAsync(path, "ttttttt");

        });
        Console.ReadKey();
    }

5 await async原理

用ILSpy反编译dll成4.0版本,就能看见荣利利己的的底层IL代码。
await async是“语法糖”,最终变异成“状态机”调用
总结:添加async关键字的方法会被c#编译器编译成一个类,会根据await调用进行切分为多个状态,对async的调用会被拆分为对MoveNext的调用。
用await看似等待,经过反编译后,其实没有等待。

6. ASYNC背后的线程切换

await调用的等待期间,.net会把当前的线程返回给线程池,等异步方法调用执行完成后,框架会从线程池再取出来一个线程执行后续的代码。

    static async Task Main(string[] args)
    {
        Console.WriteLine($"调用异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
        string path = @"~Documents\学习\1.txt";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++)
        {
            sb.Append("hahahhahhhhhhhhhhhhhhaaaaaaaa");
        }
        string content = sb.ToString();

        await File.WriteAllTextAsync(path, content);

        Console.WriteLine($"调用异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");

        Console.ReadKey();
    }

在这里插入图片描述
这里有个小细节,如果写入的非常快,线程有可能是不切换的,例:

    static async Task Main(string[] args)
    {
        Console.WriteLine($"调用异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
        string path = @"~Documents\学习\1.txt";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10; i++)
        {
            sb.Append("hahahhahhhhhhhhhhhhhhaaaaaaaa");
        }
        string content = sb.ToString();

        await File.WriteAllTextAsync(path, content);

        Console.WriteLine($"调用异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");

        Console.ReadKey();
    }

这是CLR的优化。要等待时,如果发现程序已经执行完了,就没必要切换线程了,剩下的代码就在之前的线程上继续执行就可以了。

我们自己来写一个异步方法,看一下背后的线程切换

    static async Task Main(string[] args)
    {
        Console.WriteLine($"调用自己写的异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
        await CalcAsync(5000);
        Console.WriteLine($"调用自己写的异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");

        Console.ReadKey();
    }

    public static async Task<double> CalcAsync(int a)
    {
        double result = 0;
       // 这样写,并不户自动切换线程,之后把代码放到task.run中才会切换线程
        Random random = new Random();
        for (int i = 0; i < a* a; i++)
        {
            result += random.NextDouble();
        }
        return result;
    }

在这里插入图片描述
我们可以看到线程并没有切换。这是因为异步编程并不会自动切换线程,如果想切换线程,必须手动将代码放到Task中去执行。

    public static async Task<double> CalcAsync(int a)
    {
        double result = 0;
        await Task.Run(() =>
         {
             Random random = new Random();
             for (int i = 0; i < a* a; i++)
             {
                 result += random.NextDouble();
             }
         });
        return result;
    }
    static async Task Main(string[] args)
    {
        Console.WriteLine($"调用自己写的异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
        await CalcAsync(5000);
        Console.WriteLine($"调用自己写的异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");

        Console.ReadKey();
    }

在这里插入图片描述
所以才说,异步编程不是多线程。它不会自动切换,而是需要我们手动将代码放到task中才可以。那为什么我们调用其他写好的异步方法时会切换呢?那是因为它内部已经放到了task中。

7 为什么有的异步方法没有标async

如果一个异步方法只是对其他异步方法的调用,没有复杂的逻辑,那么就可以去掉async。以上面自己写的异步方法为例

    // 不写await  async
    public static Task<double> CalcAsync2(int a)
    {
        double result = 0;
        Task.Run(() =>
       {
           Random random = new Random();
           for (int i = 0; i < a * a; i++)
           {
               result += random.NextDouble();
           }
       });
        return Task.FromResult(result);
    }
    
        static async Task Main(string[] args)
       {
	        Console.WriteLine($"调用自己写的异步方法前的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
	        await CalcAsync2(5000);
	        Console.WriteLine($"调用自己写的异步方法后的当前线程id: {Thread.CurrentThread.ManagedThreadId}");
	
	        Console.ReadKey();
       }

8 不要用sleep

如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用await task.delay()。

9 CancellationToken

有时需要提前终止任务,比如:请求超时,用户取消请求
很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号

CancellationToken结构体
None:空
bool IsCancellationRequested 是否取消
(*)Register(Action callback)注册取消监听 ----- 很少用
ThrowIfCancellationRequested() 如果任务被取消,执行到这里就抛异常

我们一般通过CancellationTokenSource来创建CancellationToken对象,不直接new
CancellationTokenSource
CancelAfter() 超时后发出取消信号
Cancel() 发出取消信号
CancellationToken Token

下面来做一个案例。
为“下载一个网址n次”的方法增加取消功能。分别用GetStringAsync + IsCancellationRequested 、GetStringAsync + ThrowIfCancellationRequested 、带CancellationToken的GeAsync()分别实现。取消分别用户超时、用户敲键盘(不能await)实现

先写一个普通下载

    static async Task Main(string[] args)
    {
        await Download1("https://www.youzack.com", 2000);

        Console.Read();
    }

    // 下载一个网站 普通下载
    public static async Task Download1(string url, int n)
    {
        using (HttpClient client = new HttpClient())
        {
            for (int i = 0; i < n; i++)
            {
                string html = await client.GetStringAsync(url);
                Console.WriteLine(html);
            }
        }
    }

再写一个用GetStringAsync + IsCancellationRequested方式响应用户取消请求的:

    static async Task Main(string[] args)
    {
        CancellationTokenSource source = new CancellationTokenSource();
        source.CancelAfter(5000); // 5s后取消请求 
         await Download1("https://www.youzack.com", 2000, source.Token);

        Console.Read();
    }

    // 下载一个网站 响应用户取消请求
    public static async Task Download2(string url, int n,CancellationToken token)
    {
        using (HttpClient client = new HttpClient())
        {
            for (int i = 0; i < n; i++)
            {
                string html = await client.GetStringAsync(url);
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("用户取消请求");
                }
                Console.WriteLine(html);
            }
        }
    }

在这里插入图片描述
这里有个需要注意的点:我们设置5s后停止请求,但是如果5s内下载操作已经完成,那么程序会正常终止,不会再打印“用户取消请求”这条信息。这一点可以通过将2000改为20验证。

再写一个用GetStringAsync + ThrowIfCancellationRequested方式响应用户取消请求的:

    public static async Task Download2(string url, int n, CancellationToken token)
    {
        using (HttpClient client = new HttpClient())
        {
            for (int i = 0; i < n; i++)
            {
                string html = await client.GetStringAsync(url);
                token.ThrowIfCancellationRequested();
                Console.WriteLine(html);
            }
        }
    }

这样写,程序会通过报错的方式来终止操作。
在这里插入图片描述
再写一个通过CancellationToken的GeAsync()方式响应用户取消请求的:

    public static async Task Download3(string url, int n, CancellationToken token)
    {
        using (HttpClient client = new HttpClient())
        {
            for (int i = 0; i < n; i++)
            {
                var responseMessage = await client.GetAsync(url, token);
                string html = await responseMessage.Content.ReadAsStringAsync();
                Console.WriteLine(html);
            }
        }
    }

在这里插入图片描述
这个也是通过报异常的方式终止操作。

ASP.NET Core开发中,一般不需要自己处理CancellationToken、CancellationTokenSource这些,知道做到“能转发CancellationToken就转发”即可。ASP.NET Core会对用户请求中断进行处理。

10 异步编程 WhenAll

task类的重要方法:

  1. TaskWhenAny(IEnumerable tasks) 任何一个task完成,task就完成
  2. Task<TResult[]>WhenAll<TResult[]>(params Task<TResult[]> tasks) 所有task完成,task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。
  3. FromResult() 创建普通数值的Task对象

有3个txt,分别读取它们,然后等全部读取完毕再打印它们的内容。

    static async Task Main(string[] args)
    {
        // 这里没有使用await,所以不会等1读取完再去读取2,等2读取完再去读取3
        Task<string> s1 = File.ReadAllTextAsync(@"1.txt"); 
        Task<string> s2 = File.ReadAllTextAsync(@"2.txt");
        Task<string> s3 = File.ReadAllTextAsync(@"3.txt");

        // 这里用whenall,等3个都读取完毕再打印
        string[] strs = await Task.WhenAll(s1, s2, s3); 
        Console.WriteLine(strs[0] + " " + strs[1] + " " + strs[2]);
        Console.ReadKey();
    }

在这里插入图片描述

11 异步的其他问题

1. 接口中的异步方法

async是为了提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来说没有区别。因此对于接口中的方法或抽象方法不能修饰为async。如果方法需要异步,可以在它的继承类中写async。
如:

给接口加上async关键字会报错

    public  interface AsyncInterface
    {
        async Task<int> GetFileCount();
    }

在这里插入图片描述
给实现类中加async不报错

    public class TestClass : AsyncInterface
    {
        public async Task<int> GetFileCount()
        {
            string path = @"~Documents\学习\1.txt";
            string content = await File.ReadAllTextAsync(path);
            return content.Length;
        }
    }

2. 异步与yield

yield不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提高性能。
在旧版本c#中,async方法中不能用yield。从c#8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值