You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程单元测试中Thread.Sleep使用问题及跨平台差异排查

多线程单元测试疑难问题

问题背景

我正在编写单元测试验证Web服务器的多线程处理能力(需同时处理大量请求),但在PerformTask中用Thread.Sleep模拟数据库等后台任务时测试失败,注释掉该语句则测试通过。

核心复现代码

using NUnit.Framework;

namespace MultiThreadedUnitTest
{
    [TestFixture]
    [Parallelizable(ParallelScope.None)]
    public class Tests
    {
        private readonly int NumberOfForLoops = 100;
        private int Counter { get; set; } = 0;
        private object CounterLock = new object();

        private void addToCounter(int value)
        {
            lock (CounterLock)
            {
                Counter += value;
            }
        }

        [Test, Timeout(5000)] // 必须在5秒内完成
        [Parallelizable(ParallelScope.None)]
        public void RunTest()
        {
            var threads = new Thread[NumberOfForLoops];
            for (int i = 0; i < threads.Length; i++)
            {
                threads[i] = new Thread(() => PerformTask());
            }

            foreach (var thread in threads)
            {
                thread.Start();
            }

            foreach (var thread in threads)
            {
                thread.Join();
            }

            Assert.AreEqual(NumberOfForLoops, Counter);

            var task = AsyncTest();
            task.Wait();

            Assert.AreEqual(NumberOfForLoops, task.Result);

            Assert.AreEqual(NumberOfForLoops * 3, Counter);

            // "performTask"被调用30次,但测试在5秒内完成,说明部分Thread.Sleep是并行执行的
        }

        private async Task<int> AsyncTest()
        {
            var tasks = new List<Task<int>>();
            for (var i = 0; i < NumberOfForLoops; i++)
            {
                tasks.Add(Task.Run(() => Test()));
                /* 
                 * tasks.Add(Test());  --> 这样会让任务按顺序执行
                 * */
            }
            await Task.WhenAll(tasks);

            await Task.Run(() => Parallel.For(0, NumberOfForLoops, s =>
            {
                var res = Test();
            }));

            return tasks.Sum(x => x.Result);
        }

        private async Task<int> Test()
        {
            PerformTask();
            return 1;
        }

        private object Lock = new object();
        private void PerformTask()
        {
            //Thread.Sleep(1000); // 问题2
            try
            {
                lock (Lock)
                {
                    //TODO: 模拟数据库查询
                    var rnd = new Random();
                    var number = rnd.Next(1, 50) * 100;
                    
                    //Task.Delay(number).Wait();
                    var x = "Thread id = " + Thread.CurrentThread.ManagedThreadId;
                    Console.WriteLine(x);

                    Console.WriteLine("Sleeping for : " + number);

                    /* 下面这行导致测试失败 */
                    //Thread.Sleep(number);

                    addToCounter(1);
                    
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}

待解决问题

1. 异常捕获问题

启用Thread.Sleep后测试失败,但无明确错误提示,如何捕获并查看具体异常?

2. 跨平台差异问题

当循环次数较少(如10次)且启用标注为“问题2”的Thread.Sleep(1000)时,Windows环境下测试全部通过,但Ubuntu环境下始终失败,且无错误行号提示,只能靠Console.WriteLine排查,请问跨平台差异的原因是什么?


问题解答

针对问题1:捕获测试中的异常

测试无明确错误提示,核心原因大概率是超时导致的静默失败(测试标注了Timeout(5000)),也可能是线程内异常未传递到主测试线程。可通过以下方式优化:

  • 调整或取消测试超时:启用Thread.Sleep(number)后,每个PerformTask在锁内最多睡眠5000ms,100次串行执行总耗时必然超过5秒,直接触发超时失败。NUnit默认超时仅标记测试失败,不会输出细节,可先注释Timeout(5000)确认是否为超时问题。
  • 全局捕获未处理异常:在测试方法开头添加全局异常监听,捕获线程或Task的未处理异常:
    // 监听AppDomain未处理异常
    AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
    {
        TestContext.WriteLine($"未捕获全局异常: {e.ExceptionObject}");
    };
    // 监听Task未处理异常
    TaskScheduler.UnobservedTaskException += (sender, e) =>
    {
        TestContext.WriteLine($"Task未捕获异常: {e.Exception}");
        e.SetObserved();
    };
    
  • 用TestContext输出异常:PerformTask中的try-catch仅输出到Console,NUnit不会收录该信息,改为用TestContext.WriteLine输出,异常会显示在测试报告中:
    catch (Exception ex)
    {
        TestContext.WriteLine($"PerformTask异常详情: {ex.ToString()}");
    }
    

针对问题2:跨平台差异原因

Windows与Ubuntu测试结果不同,核心源于**.NET在不同操作系统上的线程调度、线程池行为及类实现细节差异**:

  • 线程调度策略差异:Windows和Linux的线程调度算法不同,Linux下线程切换开销、优先级处理逻辑与Windows有区别。当循环10次+Thread.Sleep(1000)时,Windows可能在5秒内完成所有任务,而Linux下调度延迟导致总耗时超过超时阈值,触发测试失败。
  • Random类种子问题:代码在lock(Lock)内每次创建new Random(),而Random默认以系统时钟为种子。Linux系统时钟精度低于Windows,导致多次创建的Random生成相同的睡眠时长,总耗时被拉长,超出5秒限制。建议将Random声明为类级别变量,避免重复初始化:
    private readonly Random _rnd = new Random(); // 类级别仅初始化一次
    
    private void PerformTask()
    {
        try
        {
            lock (Lock)
            {
                var number = _rnd.Next(1, 50) * 100;
                // ... 其余代码
            }
        }
        // ...
    }
    
  • 锁的底层实现差异:.NET在Windows和Linux下的lock(即Monitor类)底层实现不同,Linux下锁的开销略高,累加后导致总耗时增加。
  • NUnit超时检测差异:NUnit在跨平台环境下的超时判断逻辑有细微差别,Linux下的超时检测更严格,线程睡眠的实际时长更接近设定值,容易触发超时。

内容的提问来源于stack exchange,提问作者Quintonn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 15:29:58