• 一个例子形象地理解同步与异步


    请看一个示例:

    同步方式请求接口

    请求一次接口耗时大约100多毫秒

    代码

    一个for循环,循环500次,调用方法Request,Request方法中一个while(true)无限循环,同步方式请求url获取数据。
    代码点评:要是写一个while(true)没问题,这是想运行500个while(true),这代码是错误的,行不通。应该使用Thread或者Task.Run加TaskCreationOptions.LongRunning参数。
    这当然是有问题的代码,请看下面运行截图,只有第一个while(true)在执行,其它的499个while(true)根本没有执行机会。

    static int num = 0;
    static ConcurrentDictionary<int, object> dict = new ConcurrentDictionary<int, object>();
    
    static void Main(string[] args)
    {
        CalcSpeed();
    
        for (int i = 0; i < 500; i++)
        {
            Request(i);
        }
    
        Console.WriteLine($"Main函数结束");
        Console.ReadLine();
    }
    
    static void Request(int index)
    {
        dict.TryAdd(index, null);
    
        while (true)
        {
            string url = "http://localhost:5028/Test/TestGet";
            string result = HttpUtil.HttpGet(url);
            Interlocked.Increment(ref num);
        }
    }
    
    static void CalcSpeed()
    {
        _ = Task.Factory.StartNew(() =>
        {
            Stopwatch sw = Stopwatch.StartNew();
            while (true)
            {
                Thread.Sleep(2000);
                double speed = num / sw.Elapsed.TotalSeconds;
                ThreadPool.GetMaxThreads(out int w1, out int c1);
                ThreadPool.GetAvailableThreads(out int w2, out int c2);
                Console.WriteLine($"有 {dict.Count.ToString().PadLeft(3)} 个 while(true) 在执行,线程池活动线程数:{(w1 - w2).ToString().PadRight(3)}  速度:{speed:#### ####.0} 次/秒");
            }
        }, TaskCreationOptions.LongRunning);
    }
    

    运行截图

    说明

    代码中没有创建线程,也没有使用Task.Run,请求一次接口耗时大约100多毫秒,while(true)在主线程中执行,平均1秒请求接口不到10次。
    注意:只有第一个while(true)在执行。

    修改1:在Request函数中添加一行代码Thread.Sleep(1);

    代码

    static void Request(int index)
    {
        dict.TryAdd(index, null);
    
        while (true)
        {
            string url = "http://localhost:5028/Test/TestGet";
            string result = HttpUtil.HttpGet(url);
            Interlocked.Increment(ref num);
    
            Thread.Sleep(1);
        }
    }
    

    运行截图

    说明

    没什么用,速度还变慢了一点。
    依然是有问题的代码。
    依然只有第一个while(true)在执行。

    修改2:在Request函数中添加一行代码await Task.Delay(1);

    VS自动在void Request前面添加了async关键字

    代码

    static async void Request(int index)
    {
        dict.TryAdd(index, null);
    
        while (true)
        {
            string url = "http://localhost:5028/Test/TestGet";
            string result = HttpUtil.HttpGet(url);
            Interlocked.Increment(ref num);
    
            await Task.Delay(1);
        }
    }
    

    运行截图

    说明

    速度快多了,并且越来越快。
    有多个while(true)在执行,并且在执行的while(true)数量越来越多,最终会达到500个。
    这是比较神奇的地方,仅仅加了一行await Task.Delay(1);同步方法Request就变成了异步方法。
    在执行await Task.Delay(1);这一行时,其它while(true)得到了执行机会,你们可以验证一下。
    同步请求分别在不同的线程中执行,你们可以打印线程ID验证一下。

    修改3:前面使用的是HttpUtil.HttpGet同步请求,修改为异步请求,await Task.Delay(1);这一行也不需要了

    代码

    static async void Request(int index)
    {
        dict.TryAdd(index, null);
    
        while (true)
        {
            string url = "http://localhost:5028/Test/TestGet";
            var httpClient = HttpClientFactory.GetClient();
            string result = await (await httpClient.GetAsync(url)).Content.ReadAsStringAsync();
            Interlocked.Increment(ref num);
        }
    }
    

    运行截图

    说明

    速度非常快。
    异步的优势体现出来了。

    修改4:有没有人会认为修改2,把同步代码用Task.Run包一下,速度会更快?

    代码

    static async void Request(int index)
    {
        dict.TryAdd(index, null);
    
        while (true)
        {
            await Task.Run(() =>
            {
                string url = "http://localhost:5028/Test/TestGet";
                string result = HttpUtil.HttpGet(url);
                Interlocked.Increment(ref num);
            });
    
            await Task.Delay(1);
        }
    }
    

    运行截图

    说明

    线程饥饿,全部阻塞,没有返回结果,速度是0。

    总结

    通过这个例子形象地体会一下同步与异步,以及为什么要使用异步。
    如果你写的代码是异步的,但是调用的IO接口又是同步的,这比真正的异步效率要差很多,但比同步代码有所提升。
    针对修改2,有人会说,这代码有问题,后面的while(true)会延迟好久才会执行。但是如果for循环的数量是少量的,程序启动时的一点延迟是允许的,就没有问题,
    修改代码如下:

    for (int i = 0; i < 20; i++)
    {
        Request(i);
    }
    

    运行截图:

    说明:
    20个while(true)都在运行,比一个while(true)要快很多。
    当然,没必要这么写了,直接new 20个Thread就可以。
    但如果for循环就是500次,而且需要调用的IO接口又是同步的,那么就老老实实写500个new Thread。
    如果非要用异步,设置一下线程池的大小,大于500,避免线程饥饿。

    ThreadPool.SetMinThreads(800, 800);
    ThreadPool.SetMinThreads(600, 600);
    

    你会发现,不能想当然,依然有问题,这时强行用异步就很容易写出BUG了。
    最后,再好好体会一下上面的例子。

    补充

    对于修改2的修改:没必要启动500个while(true),可以只启动一个,代码如下:

    static void Main(string[] args)
    {
        // 设置一下线程池数量可以大大提升批量请求速度
        // ThreadPool.SetMinThreads(650, 650);
        // ThreadPool.SetMinThreads(600, 600); 
    
        CalcSpeed();
    
        Task.Factory.StartNew(() =>
        {
            while (true)
            {
                // 批量请求,第1个参数是一个集合,第2个参数是最大并行度
                Parallel.ForEach(Enumerable.Range(0, 500), new ParallelOptions() { MaxDegreeOfParallelism = 500 }, (i, c) =>
                {
                    Request(i);
                });
    
                Thread.Sleep(1);
            }
        }, TaskCreationOptions.LongRunning);
    
        Console.WriteLine($"Main函数结束");
        Console.ReadLine();
    }
    
    static void Request(int index)
    {
        dict.TryAdd(index, null);
    
        string url = "http://localhost:5028/Test/TestGet";
        string result = HttpUtil.HttpGet(url);
        Interlocked.Increment(ref num);
    }
    
  • 相关阅读:
    docker 部署tig监控服务
    springboot 反射调用ServiceImpl,报错:java.lang.NullPointerException,mapper为null
    企业电子招投标采购系统源码之电子招投标的组成
    Google Earth Engine APP——全球内陆水分布的APP展示
    面经——C++语言1
    基于IDEA集成环境---Nacos安装
    web渗透部分笔记(一)
    基于WebGL、Cesium技术的三维空间可视化
    opencv c++ 高斯模糊,高斯双边模糊(28)
    f-stack的源码编译安装
  • 原文地址:https://www.cnblogs.com/s0611163/p/17979998