zoukankan      html  css  js  c++  java
  • asp.net core 关于同步等待异步坑及解决办法

    .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况

    原文链接 : https://blog.walterlv.com/post/task-wait-may-cause-long-time-waiting

    一个简单的 Task 不会消耗多少时间,但如果你不合适地将 Task 转为同步等待,那么也可能很快耗尽线程池的所有资源,出现类似死锁的情况。

    本文将以一个最简单的例子说明如何出现以及避免这样的问题。


    耗时的 Task.Run

    谁都不会认为 Task.Run(() => 1) 这个异步任务执行会消耗多少时间。

    但实际上,如果你的代码写得不清真,它真的能消耗大量的时间,这种时间消耗有点像死锁。

    下图分别是 7 个这样的任务、8 个这样的任务和 16 个这样的任务的耗时:

    简单异步任务的耗时

    可以发现,8 个任务和 16 个任务的耗时很不正常。

    在实际的测试当中,1~7 个任务的耗时几乎相同,而到后面每增加一个任务会增加大量时间。

    任务个数耗时 (ms)
    1 39
    2 54
    3 58
    4 50
    5 49
    6 45
    7 54
    8 1027
    9 2030
    10 3027
    11 4027
    12 5032
    13 6027
    14 7029
    15 8025
    16 9025

    任务计时采用的是 Stopwatch,关于为什么要使用这种计时方式,可以阅读 .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)

    统计图表

    从图中,我们可以很直观地观察到,每多一个任务,就会多花 1 秒的事件。这可以认为默认情况下线程池在增加线程的时候,发现如果线程不够,会等待 1 秒之后才会创建新的线程。

    最简复现代码

    class Program
    {
        static async Task Main(string[] args)
        {
            Console.Title = "walterlv task demo";
    
            var stopwatch = Stopwatch.StartNew();
    
            var task = Enumerable.Range(0, 8).Select(i => Task.Run(() => DoAsync(i).Result)).ToList();
            await Task.WhenAll(task);
    
            Console.WriteLine($"耗时: {stopwatch.Elapsed}");
            Console.Read();
        }
    
        private static async Task<int> DoAsync(int index)
        {
            return await Task.Run(() => 1);
        }
    }
    

    原因

    你可以阅读 .NET 默认的 TaskScheduler 和线程池(ThreadPool)设置 了解线程池创建新工作线程的规则。这里其实真的是类似于死锁的一个例子。

    1. 一开始,我们创建了 n 个 Task,然后分别安排在线程池中执行,并在每个 Task 中等待任务执行完毕;
    2. 随后这 n 个 Task 分别再创建了 n 个子 Task,并继续安排在线程池中执行;
    3. 这时问题来了,由于前面 n 个 Task 在等待中,所以占用了线程池的线程资源:
      • 如果 n < 线程池最小线程数,那么当前线程池中还有剩余工作线程帮助完成子 Task;
      • 但如果 n >= 线程池最小线程数,那么当前线程池中便没有新的工作线程来完成子 Task;于是一开始的等待也不会完成;必须等线程池开启新的工作线程后,任务才可以继续。

    带线程池开启新的线程之前,以上那些线程就是处于死锁的状态!由于线程池开启新的工作线程需要等待一段时间(例如每秒最多开启一个新的线程),所以每增加一个这样的任务,那么消耗的时间便会持续增加。

    解决

    去掉这里本来多余的 Task.Run 问题便可以解决。或者一直 async/await 中间不要转换为同步代码,那么问题也能解决。

    我会遇到以上代码,是因为在库中写了类似 DoAsync 那样的方法。同时为了方便使用,封装了一个同步等待的属性。在业务使用方,觉得获取此属性可能比较耗时,于是用了 Task.Run 在后台线程调用。同时由于这是一个可能大量并发的操作,于是造成了以上悲剧。

    我们遇到的问题

    我们线上的代码逻辑: 一个请求中会根据会员的子账号数量去开启task.run 发送http请求 获取一些数据 然后汇总 . 但是一些历史原因, http请求使用的是webrequest或者httpclent的getresult同步代码

    就像上文一样, task.run中如果跑的是同步代码, 没有await  这个task会等待它的做完后task才会释放回task池, 这里还是一些http请求等耗时操作, 也只能干等着

    一个会员N个子账号 => 需要N个task去执行 耗时操作,  100个会员在线就是100*N 个task去做

    从上文的实验代码可以看出, task池中应该默认有8个task的 , 线程池发现task数量不够用 会生成task补充到池中 但是速度很慢, 上文代码也可看出 大概1秒产生1个task补充到task池 , 因为16个循环中执行了8秒

    这样一来, 大量的请求只能在task池外面排队  用户就迟迟得不到响应

    解决办法上面也说了  改代码吧 我看有篇博客也说了这个问题 总结如下

    全面放弃在同步方法中调用异步方法,并将“千万千万不要在同步方法中调用异步方法”作为一条 .NET Core 开发准则。

        class Program
        {
            static HttpClient httpClient = new HttpClient();
            static async Task Main(string[] args)
            {
                var stopwatch = Stopwatch.StartNew();
    
                //var task = Enumerable.Range(0, 30).Select(i => Task.Run(async () => await sendReq1(i))).ToList();
                var task = Enumerable.Range(0, 15).Select(i => Task.Run(() => sendReq2(i))).ToList();
                await Task.WhenAll(task);
    
                Console.WriteLine($"耗时: {stopwatch.Elapsed}");
                Console.Read();
            }
    
    
            /// <summary>
            /// 目前代码2
            /// </summary>
            /// <param name="i"></param>
            /// <returns></returns>
            private static string sendReq2(int i)
            {
                Console.WriteLine("进入" + i + "当前线程;" + Thread.CurrentThread.ManagedThreadId);
    
                var request = (HttpWebRequest)WebRequest.Create("https://baidu.com");
    
                request.Method = "GET";
                request.ContentType = "application/x-www-form-urlencoded";
                request.Referer = "";
                request.Accept = "*/*";
                request.KeepAlive = true;
                request.CookieContainer = new CookieContainer();
                request.Timeout = 30 * 1000;
    
                var wr = request.GetResponse() as HttpWebResponse;
                Stream ReceiveStream = wr.GetResponseStream();
    
                string receiveString = "";
                using (StreamReader reader = new StreamReader(ReceiveStream, Encoding.UTF8))
                {
                    receiveString = reader.ReadToEnd();
                }
    
                Console.WriteLine(DateTime.Now + "完成" + i + "当前线程;" + Thread.CurrentThread.ManagedThreadId);
                return receiveString;
            }
    
            /// <summary>
            /// 目前的代码
            /// </summary>
            /// <param name="i"></param>
            /// <returns></returns>
            private static string sendReq(int i)
            {
                Console.WriteLine("进入" + i + "当前线程;" + Thread.CurrentThread.ManagedThreadId);
                var resp = httpClient.GetAsync("https://ip.cn").GetAwaiter().GetResult();
                var stream = resp.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
                string receiveString = "";
                using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
                {
                    receiveString = reader.ReadToEnd();
                }
    
                Console.WriteLine(DateTime.Now + "完成" + i + "当前线程;" + Thread.CurrentThread.ManagedThreadId);
                return receiveString;
            }
    
            /// <summary>
            /// 理论该这么写,但是有历史原因改不了全部异步
            /// </summary>
            /// <param name="i"></param>
            /// <returns></returns>
            private static async Task<string> sendReq1(int i)
            {
                Console.WriteLine("进入" + i + "当前线程;" + Thread.CurrentThread.ManagedThreadId);
                var resp = await httpClient.GetAsync("https://ip.cn");
                var stream = await resp.Content.ReadAsStreamAsync();
                string receiveString = "";
                using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
                {
                    receiveString = reader.ReadToEnd();
                }
    
                Console.WriteLine(DateTime.Now + " 完成" + i + "当前线程;" + Thread.CurrentThread.ManagedThreadId);
                return receiveString;
            }
    
    
        }
  • 相关阅读:
    每日算法
    每日算法
    搜索算法入门详解
    NLP
    每日算法
    每日算法
    Elasticsearch地理位置总结
    elasticsearch Geo Bounding Box Query
    elasticsearch Geo Distance Query
    Elasticsearch java API (23)查询 DSL Geo查询
  • 原文地址:https://www.cnblogs.com/xtxtx/p/13408496.html
Copyright © 2011-2022 走看看