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或在多线程环境中使用它们是否完全错误?欢迎任何帮助或暗示!
解决方法
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更优雅,并且显然更容忍同时在同一个实例上提升事件的多个线程.虽然我并没有想到这是一个常见的要求 – 我个人并不常常会发现你可以假设一个事件的订阅者是线程安全的.