ManualResetEventSlim在异步并发测试中的作用及测试改造方法
关于并发测试中ManualResetEventSlim的作用分析
咱们先来看你写的这段并发测试代码,核心目标是验证异步方法在多线程调用时,每个线程能拿到隔离的结果,不会互相干扰。接下来咱们逐一拆解你的问题:
一、原测试里ManualResetEventSlim到底起啥作用?
你在task1和task2里用的rs1、rs2这两个ManualResetEventSlim,本质是给两个异步任务“定规矩”,强制它们按你想要的顺序执行,精准控制expectedResult的修改时机:
rs2.Wait()让task2必须等task1完成expectedResult = 40的赋值、调用完sut.MyMethod()之后,才允许修改自己的expectedResult = 45rs1.Wait()让task1必须等task2完成sut.MyMethod()和自身断言后,才执行自己的断言- 这么一来,task1用的是40这个值,task2用的是45这个值,两者的变量修改和方法调用完全错开,不会出现“task2提前改了值,导致task1断言出错”的情况(当然现在你的代码可能因为线程调度巧合,不用也能过,但这纯属运气)
二、用和不用ManualResetEventSlim的区别
用了的情况:
- 线程执行顺序是完全可控的:task1先改值→调用方法→通知task2启动→task2改值→调用方法→通知task1断言→最后完成所有校验
- 测试结果是稳定可靠的,不管操作系统怎么调度线程,都能保证每个线程用的是自己设置的
expectedResult,不会出现偶发的断言失败
不用的情况:
- 线程执行顺序是随机无序的,全看操作系统的线程调度:比如可能task2先把
expectedResult改成45,task1才开始调用MyMethod,这时候task1的断言就会拿45去对比,而不是预期的40;但也可能刚好调度顺序符合你的期望,所以测试偶尔能过 - 测试结果是不稳定的,属于“靠运气通过”,本质是测试逻辑有漏洞,没有真正模拟出并发冲突的场景
三、怎么改造测试,让不用ManualResetEventSlim就过不了?
核心思路是放大线程竞争的概率,让expectedResult的修改和MyMethod的调用更容易出现交叉冲突,具体可以这么改:
- 把Mock里的
Thread.Sleep(10)改成更长的时间,比如Thread.Sleep(100),让MyMethod内部的异步操作耗时更久,线程调度的不确定性更高 - 增加更多并发任务,比如5个甚至10个,每个任务设置不同的
expectedResult值 - 去掉ManualResetEventSlim的控制,让所有任务同时启动,并且启动后立刻修改
expectedResult
改造后的示例代码:
public async Task MyMethod_Concurrency_ReturnsIsolatedResultPerThread() { int expectedResult = 20; var theMock = new Mock<IService>(); theMock.Setup(m => m.GetResult(It.IsAny<int>())) .Callback(() => Thread.Sleep(100)) // 延长耗时,放大线程竞争概率 .Returns<int>(t => Task.FromResult(expectedResult)); var sut = new MyClass(30, theMock.Object); var tasks = new List<Task>(); // 创建多个并发任务,每个任务设置不同的目标结果值 for (int i = 0; i < 5; i++) { int targetValue = 30 + i * 5; tasks.Add(Task.Run(async () => { expectedResult = targetValue; await sut.MyMethod(); Assert.AreEqual(targetValue, sut.Result); })); } tasks.Add(Task.Run(() => Assert.AreEqual(0, sut.Amount))); await Task.WhenAll(tasks); }
这个时候,如果你的MyClass.MyMethod没有做到真正的线程隔离,那么去掉ManualResetEventSlim后,多个任务会同时修改expectedResult,导致断言失败——因为某个任务调用MyMethod时,expectedResult已经被另一个任务改成别的值了,而如果MyMethod没有在调用瞬间捕获当前的expectedResult值,就会出现结果不一致的情况。
内容的提问来源于stack exchange,提问作者user5032790
相关产品推荐
相关产品推荐

