C# · 12月 25, 2021

c# – 多线程代码使Rhino Mocks导致死锁

我们目前在单元测试中面临一些问题.我们的类是使用Rhino Mocks对Mocked对象的一些函数调用进行多线程处理.这是一个减少到最小值的例子: public class Bar{ private readonly List<IFoo> _fooList; public Bar(List<IFoo> fooList) { _fooList = fooList; } public void Start() { var allTasks = new List<Task>(); foreach (var foo in _fooList) allTasks.Add(Task.Factory.StartNew(() => foo.DoSomething())); Task.WaitAll(allTasks.ToArray()); }}

接口IFoo定义为:

public interface IFoo{ void DoSomething(); event EventHandler myEvent;}

为了重现僵局,我们的unittest将执行以下操作:
1.创建一些IFoo模拟器
当DoSomething()被调用时,提高myEvent.

[TestMethod] public void Foo_RaiseBar() { var fooList = GenerateFooList(50); var target = new Bar(fooList); target.Start(); } private List<IFoo> GenerateFooList(int max) { var mocks = new MockRepository(); var fooList = new List<IFoo>(); for (int i = 0; i < max; i++) fooList.Add(GenerateFoo(mocks)); mocks.ReplayAll(); return fooList; } private IFoo GenerateFoo(MockRepository mocks) { var foo = mocks.StrictMock<IFoo>(); foo.myEvent += null; var eventRaiser = LastCall.On(foo).IgnoreArguments().GetEventRaiser(); foo.DoSomething(); LastCall.On(foo).WhenCalled(i => eventRaiser.Raise(foo,EventArgs.Empty)); return foo; }

产生的Foo越多,死锁越频繁.如果测试不会阻止,运行它几次,它会的.
停止调试testrun显示,所有Tasks仍然在TaskStatus.Running和当前工作线程中断

[In a sleep,wait,or join]
Rhino.Mocks.DLL!Rhino.Mocks.Impl.RhinoInterceptor.Intercept(Castle.Core.Interceptor.IInvocation
invocation) + 0x3d bytes

令我们最困惑的奇怪的事情是,拦截(…)方法的签名被定义为同步 – 但是几个线程位于这里.我读过几篇关于Rhino Mocks和Multithreaded的帖子,但没有发现警告(预期设置记录)或限制.

[MethodImpl(MethodImplOptions.Synchronized)] public void Intercept(IInvocation invocation)

我们在设置我们的Mockobjects或在多线程环境中使用它们是否完全错误?欢迎任何帮助或暗示!

解决方法 这是您的代码中的竞争条件,而不是RhinoMock中的错误.当您在Start()方法中设置allTask​​s任务列表时,会出现此问题: public void Start() { var allTasks = new List<Task>(); foreach (var foo in _fooList) // the next line has a bug allTasks.Add(Task.Factory.StartNew(() => foo.DoSomething())); Task.WaitAll(allTasks.ToArray()); }

您需要将foo实例显式传递到任务中.该任务将在不同的线程上执行,并且很可能foreach循环将在任务开始之前替换foo的值.

这意味着每个foo.DoSomething()被调用有时从来没有,有时不止一次.因此,一些任务将无限期地阻止,因为RhinoMocks不能处理来自不同线程的同一实例上的事件的重叠提升,并且它陷入死锁.

在Start方法中替换此行:

allTasks.Add(Task.Factory.StartNew(() => foo.DoSomething()));

有了这个:

allTasks.Add(Task.Factory.StartNew(f => ((IFoo)f).DoSomething(),foo));

这是一个经典的bug,它是微妙的,非常容易忽视的.它有时被称为“访问修改的关闭”.

PS:

根据对这篇文章的评论,我用Moq重写了这个测试.在这种情况下,它不会阻止 – 但请注意,在给定实例上创建的期望可能不会被满足,除非原始错误是按照所述进行修复的.使用Moq的GenerateFoo()如下所示:

private List<IFoo> GenerateFooList(int max){ var fooList = new List<IFoo>(); for (int i = 0; i < max; i++) fooList.Add(GenerateFoo()); return fooList;}private IFoo GenerateFoo(){ var foo = new Mock<IFoo>(); foo.Setup(f => f.DoSomething()).Raises(f => f.myEvent += null,EventArgs.Empty); return foo.Object;}

它比RhinoMock更优雅,并且显然更容忍同时在同一个实例上提升事件的多个线程.虽然我并没有想到这是一个常见的要求 – 我个人并不常常会发现你可以假设一个事件的订阅者是线程安全的.