如果我使用异步,不应该使用更少的线程数吗?

Posted

技术标签:

【中文标题】如果我使用异步,不应该使用更少的线程数吗?【英文标题】:Shouldn't lesser number of threads be used if I use async? 【发布时间】:2014-06-09 04:46:39 【问题描述】:

我的理解是,如果我使用异步,线程会发出 Web 请求并继续前进。当响应返回时,另一个线程会从那里获取它。因此,闲置的绑定线程数量较少。 这不是意味着最大活动线程数会下降吗?但在下面的示例中,不使用异步的代码最终会使用较少数量的线程。谁能解释一下原因?

没有异步的代码(使用较少的线程):

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;

namespace NoAsync

    internal class Program
    
        private const int totalCalls = 100;

        private static void Main(string[] args)
        
            for (int i = 1; i <= totalCalls; i++)
            
                ThreadPool.QueueUserWorkItem(GoogleSearch, i);
            

            Thread.Sleep(100000);
        

        private static void GoogleSearch(object searchTerm)
        
            Thread.CurrentThread.IsBackground = false;
            string url = @"https://www.google.com/search?q=" + searchTerm;
            Console.WriteLine("Total number of threads in use=0", Process.GetCurrentProcess().Threads.Count);
            WebRequest wr = WebRequest.Create(url);
            var httpWebResponse = (HttpWebResponse) wr.GetResponse();
            var reader = new StreamReader(httpWebResponse.GetResponseStream());
            string responseFromServer = reader.ReadToEnd();
            //Console.WriteLine(responseFromServer); // Display the content.
            httpWebResponse.Close();
            Console.WriteLine("Total number of threads in use=0", Process.GetCurrentProcess().Threads.Count);
        
    

异步代码(使用更多线程)

using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace AsyncAwait

    internal class Program
    
        private const int totalCalls = 100;
        private static DateTime start = System.DateTime.Now;

    private static void Main(string[] args)
    
        var tasks = new List<Task>();

        for (int i = 1; i <= totalCalls; i++)
        
            var searchTerm = i;
            var t = GoogleSearch(searchTerm);
            tasks.Add(t);
        

        Task.WaitAll(tasks.ToArray());
        Console.WriteLine("Hit Enter to exit");
        Console.ReadLine();
    

    private static async Task GoogleSearch(object searchTerm)
    
        Thread.CurrentThread.IsBackground = false;
        string url = @"https://www.google.com/search?q=" + searchTerm;
        Console.WriteLine("Total number of threads in use=0", Process.GetCurrentProcess().Threads.Count);
        using (var client = new HttpClient())
        
            using (HttpResponseMessage response = await client.GetAsync(url))
            
                HttpContent content = response.Content;
                content.Dispose();
                Console.WriteLine("Total number of threads in use=0", Process.GetCurrentProcess().Threads.Count);
                Console.WriteLine("TimeSpan consumed 0", System.DateTime.Now.Subtract(start));
            
        
    

我明白我的结果包括非托管线程。但是总线程数不应该还少吗?

更新:我用 Noseratio 提供的代码更新了异步调用

【问题讨论】:

我认为通过调用Task.Start(),您正在启动新线程。相反,将GoogleSearch() 更改为返回Task 而不是void... 在Main() 中,只需等待它而不是启动另一个无用的任务。 【参考方案1】:

ThreadPool 实际上维护了两个子池,一个用于工作线程,另一个用于 IOCP 线程。当GetAsync 的结果可用时,会从池中分配一个随机 IOCP 线程(I/O 完成端口)来处理异步 HTTP 请求的完成。这就是await 之后的代码被执行的地方。您可以使用ThreadPool.SetMinThreads/SetMaxThreads 控制每个子池的大小,下面将对此进行详细介绍。

事实上,您的非异步代码很难与异步版本相提并论。为了更公平的比较,你应该坚持WebRequest这两种情况,例如:

非异步:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace NoAsync

    internal class Program
    
        private const int totalCalls = 100;

        private static void Main(string[] args)
        
            int maxWorkers, maxIOCPs;
            ThreadPool.GetMaxThreads(out maxWorkers, out maxIOCPs);
            int minWorkers, minIOCPs;
            ThreadPool.GetMinThreads(out minWorkers, out minIOCPs);
            Console.WriteLine(new  maxWorkers, maxIOCPs, minWorkers, minIOCPs );

            ThreadPool.SetMinThreads(100, 100);

            var tasks = new List<Task>();

            for (int i = 1; i <= totalCalls; i++)
                tasks.Add(Task.Run(() => GoogleSearch(i)));

            Task.WaitAll(tasks.ToArray());
        

        private static void GoogleSearch(object searchTerm)
        
            string url = @"https://www.google.com/search?q=" + searchTerm;
            Console.WriteLine("Total number of threads in use=0", Process.GetCurrentProcess().Threads.Count);
            WebRequest wr = WebRequest.Create(url);
            var httpWebResponse = (HttpWebResponse)wr.GetResponse();
            var reader = new StreamReader(httpWebResponse.GetResponseStream());
            string responseFromServer = reader.ReadToEnd();
            //Console.WriteLine(responseFromServer); // Display the content.
            reader.Close();
            httpWebResponse.Close();
            Console.WriteLine("Total number of threads in use=0", Process.GetCurrentProcess().Threads.Count);
        
    

异步

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace Async

    internal class Program
    
        private const int totalCalls = 100;

        private static void Main(string[] args)
        
            int maxWorkers, maxIOCPs;
            ThreadPool.GetMaxThreads(out maxWorkers, out maxIOCPs);
            int minWorkers, minIOCPs;
            ThreadPool.GetMinThreads(out minWorkers, out minIOCPs);
            Console.WriteLine(new  maxWorkers, maxIOCPs, minWorkers, minIOCPs );

            ThreadPool.SetMinThreads(100, 100);

            var tasks = new List<Task>();

            for (int i = 1; i <= totalCalls; i++)
                tasks.Add(GoogleSearch(i));

            Task.WaitAll(tasks.ToArray());
        

        private static async Task GoogleSearch(object searchTerm)
        
            string url = @"https://www.google.com/search?q=" + searchTerm;
            Console.WriteLine("Total number of threads in use=0", Process.GetCurrentProcess().Threads.Count);
            WebRequest wr = WebRequest.Create(url);
            var httpWebResponse = (HttpWebResponse) await wr.GetResponseAsync();
            var reader = new StreamReader(httpWebResponse.GetResponseStream());
            string responseFromServer = await reader.ReadToEndAsync();
            //Console.WriteLine(responseFromServer); // Display the content.
            reader.Close();
            httpWebResponse.Close();
            Console.WriteLine("Total number of threads in use=0", Process.GetCurrentProcess().Threads.Count);
        
    

默认情况下,我在系统上看到的线程池如下图:

maxWorkers = 32767, maxIOCPs = 1000, minWorkers = 4, minIOCPs = 4

线程池在增长线程时惰性。新线程的创建最多可以延迟 500 毫秒(有关更多详细信息,请查看 Joe Duffy 的"CLR thread pool injection, stuttering problems")。

要解决此问题,请使用 ThreadPool.SetMinThreads。对于SetMinThreads(100, 100),我看到 sync 版本的峰值 ~111 个线程,async 版本的峰值约为 20 个线程(发布构建,在没有调试器的情况下运行)。代表异步版本,这是一个相当大的指示性差异。

【讨论】:

如果你能提供一个可行的例子,它将对所有未来的读者非常有用。 @developer747,我发布了你的代码的更新版本。 不使用异步:最大线程=22 消耗时间=8 秒,使用异步:最大线程=39 消耗时间=5 秒。异步速度更快,但使用的线程更多。 当我用不同的调用次数重复实验时,很多时候线程池使用较少的线程并且速度也更快。 在发布模式下构建得到了预期的结果!

以上是关于如果我使用异步,不应该使用更少的线程数吗?的主要内容,如果未能解决你的问题,请参考以下文章

使用 omp_set_num_threads 后,我可以让 OpenMP 恢复到理想的线程数吗?

你能以编程方式知道 GPU 中每个块的最大块数和线程数吗?

Gatling性能测试工具入门

用了这么久线程池,你真的知道如何合理配置线程数吗?

一文读懂Java线程池

Python37 协程阻塞IO非阻塞IO同步IO异步IO