线程数对并行同步/异步方法执行时间的影响及相关疑问
测试背景
我编写了一个测试,验证可用线程数对并行化同步、异步函数执行时间的影响,过程中产生了诸多疑问,恳请解答。
测试结果
默认线程设置下的测试结果
(展示内容:不同迭代次数(100/1000/5000/15000)下,四个测试方法的执行耗时对比)
自定义线程池设置下的测试结果
配置:<ThreadPoolMinThreads>1000</ThreadPoolMinThreads> 和 <ThreadPoolMaxThreads>1000</ThreadPoolMaxThreads>
(展示内容:该线程配置下,不同迭代次数下四个测试方法的执行耗时对比)
疑问解答
1. 为何将线程数固定为1000后,异步操作的性能出现下降?
异步操作(如Task.Delay)依赖IO完成端口(IOCP)实现无阻塞等待,不需要持续占用线程池线程。强制将线程池线程数拉到1000时,会产生额外的线程调度开销:每个线程需要占用内存(线程栈)、CPU时间片切换成本。异步操作本身不需要这么多线程,过多线程会让CPU在调度上消耗更多资源,反而拖慢整体执行速度。
2. GetSyncExecutionTimeThread与GetSyncExecutionTimeDelay的结果为何存在差异?GetSyncExecutionTimeDelay异步执行却未使用await,这是为何?
两个方法的核心差异在于底层等待逻辑:
GetSyncExecutionTimeThread调用的Delay方法用Thread.Sleep,会阻塞当前线程直到超时结束。当任务数量超过线程池默认容量时,线程池需要缓慢扩容(默认每秒新增2个线程),导致大量任务排队等待线程,耗时更长。GetSyncExecutionTimeDelay调用的DelayAsync方法用Task.Delay,是异步无阻塞逻辑:遇到await时会释放当前线程回线程池,线程可继续处理其他任务,并行效率更高,耗时更短。
关于“未使用await”的误解:DelayAsync内部已经用await Task.Delay实现异步等待,GetSyncExecutionTimeDelay外层通过await Task.WhenAll等待所有任务完成,整个流程是完整的异步等待逻辑,只是Task.Run的委托直接返回了DelayAsync的Task,不需要额外加async/await包装。
3. 为何GetSyncExecutionTimeDelay比GetAsyncExecutionTimeInTasks更快?GetSyncExecutionTimeDelay异步执行却未使用await,这是为何?
两者的差异在于异步委托的包装开销:
GetSyncExecutionTimeDelay中,Task.Run(() => DelayAsync(...))直接调用异步方法DelayAsync,Task.Run会自动处理内部Task的等待,不需要额外生成异步状态机,开销极小。GetAsyncExecutionTimeInTasks中,Task.Run(async () => await ...)多了一层async委托的包装,会生成额外的异步状态机,带来微小但可感知的性能开销。
重复的“未使用await”疑问解答同问题2:DelayAsync内部已实现异步等待,外层Task.WhenAll完成了整体等待,不存在未等待的情况。
4. GetAsyncExecutionTime与GetAsyncExecutionTimeInTasks有何区别?是否GetAsyncExecutionTime由Task自主决定处理线程数量?
核心区别在于是否强制使用线程池线程:
GetAsyncExecutionTime:直接调用DelayAsync,生成的是异步IO任务,不需要主动占用线程池线程(仅在await恢复时可能用到线程池,因使用了ConfigureAwait(false))。任务调度完全由.NET异步运行时管理,仅在必要时使用线程,线程数量使用效率极高。GetAsyncExecutionTimeInTasks:用Task.Run包装DelayAsync调用,会强制将异步任务放到线程池线程上执行。虽然DelayAsync本身无阻塞,但Task.Run会先占用线程池线程直到await释放,带来额外的线程调度开销,线程池的线程数量会直接影响该方法的执行效率。
是的,GetAsyncExecutionTime的线程使用由.NET异步运行时自主决定,无需手动通过Task.Run分配线程,更适合IO密集型任务。
5. 为何单独运行各方法时结果最优,而连续调用时后续方法的结果不如单独运行时理想?
主要原因有两点:
- 线程池状态残留:前面的方法(尤其是调用
Thread.Sleep的GetSyncExecutionTimeThread)会导致线程池线程数量被扩容。后续方法运行时,线程池内可能存在大量闲置线程,带来额外的内存开销和调度成本;同时线程池的饥饿检测、扩容逻辑可能仍处于调整状态,影响任务执行效率。 - 系统资源占用:前面的方法运行时已消耗部分系统资源(CPU、内存),比如大量并行任务会导致CPU缓存命中率下降、内存占用升高,后续方法无法获得最优的资源状态,耗时增加。
测试代码
using System.Diagnostics; var upperLimitsOfRanges = new List<int>() { 100, 1000, 5000, 15000 }; var millisecondsTimeout = 100; foreach (var upperLimitOfRange in upperLimitsOfRanges) { var collection = Enumerable.Range(1, upperLimitOfRange); var syncExecutionTimeThread = await GetSyncExecutionTimeThread(collection, millisecondsTimeout); var syncExecutionTimeDelay = await GetSyncExecutionTimeDelay(collection, millisecondsTimeout); var asyncExecutionTime = await GetAsyncExecutionTime(collection, millisecondsTimeout); var asyncExecutionTimeInTasks = await GetAsyncExecutionTimeInTasks(collection, millisecondsTimeout); Console.WriteLine($"Iterations: {upperLimitOfRange}; " + $"Sync thread: {syncExecutionTimeThread} ms; " + $"Sync delay: {syncExecutionTimeDelay} ms; " + $"Async: {asyncExecutionTime} ms; " + $"Async in tasks: {asyncExecutionTimeInTasks} ms"); } Console.Read(); async Task<long> GetSyncExecutionTimeThread(IEnumerable<int> collection, int millisecondsTimeout) { var executionTimeWithoutAsync = Stopwatch.StartNew(); await Task.WhenAll(collection.Select(_ => Task.Run(() => Delay(millisecondsTimeout)))); executionTimeWithoutAsync.Stop(); return executionTimeWithoutAsync.ElapsedMilliseconds; } async Task<long> GetSyncExecutionTimeDelay(IEnumerable<int> collection, int millisecondsTimeout) { var executionTimeWithoutAsync = Stopwatch.StartNew(); await Task.WhenAll(collection.Select(_ => Task.Run(() => DelayAsync(millisecondsTimeout)))); executionTimeWithoutAsync.Stop(); return executionTimeWithoutAsync.ElapsedMilliseconds; } async Task<long> GetAsyncExecutionTime(IEnumerable<int> collection, int millisecondsTimeout) { var executionTimeWithAsync = Stopwatch.StartNew(); await Task.WhenAll(collection.Select(async _ => await DelayAsync(millisecondsTimeout).ConfigureAwait(false))); executionTimeWithAsync.Stop(); return executionTimeWithAsync.ElapsedMilliseconds; } async Task<long> GetAsyncExecutionTimeInTasks(IEnumerable<int> collection, int millisecondsTimeout) { var executionTimeWithAsync = Stopwatch.StartNew(); await Task.WhenAll(collection.Select(_ => Task.Run(async () => await DelayAsync(millisecondsTimeout).ConfigureAwait(false)))); executionTimeWithAsync.Stop(); return executionTimeWithAsync.ElapsedMilliseconds; } bool Delay(int millisecondsTimeout) { Thread.Sleep(millisecondsTimeout); return true; } async Task<bool> DelayAsync(int millisecondsTimeout) { await Task.Delay(millisecondsTimeout); return true; }
内容的提问来源于stack exchange,提问作者Web FR

