ElG*_*cho 9 c# multithreading unit-testing deadlock 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());
}
}
Run Code Online (Sandbox Code Playgroud)
接口IFoo定义为:
public interface IFoo
{
void DoSomething();
event EventHandler myEvent;
}
Run Code Online (Sandbox Code Playgroud)
为了重现死锁,我们的unittest执行以下操作:1.创建一些IFoo Mocks 2.在调用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;
}
Run Code Online (Sandbox Code Playgroud)
生成的Foo越多,死锁发生的次数就越多.如果测试不会阻塞,请运行几次,它会.停止调试testrun显示,所有任务仍在TaskStatus.Running中并且当前工作线程正在中断
[在睡眠中,等待或加入]
Rhino.Mocks.DLL!Rhino.Mocks.Impl.RhinoInterceptor.Intercept(Castle.Core.Interceptor.IInvocation invocation)+ 0x3d bytes
让我们最困惑的奇怪之处在于,Intercept(...)方法的签名被定义为Synchronized - 但是这里有几个Thread.我已经阅读了几篇关于Rhino Mocks和Multithreaded的帖子,但没有发现警告(预计会设置记录)或限制.
[MethodImpl(MethodImplOptions.Synchronized)]
public void Intercept(IInvocation invocation)
Run Code Online (Sandbox Code Playgroud)
我们在设置Mockobjects或在多线程环境中使用它们时是否完全错误?欢迎任何帮助或暗示!
Jam*_*rld 12
这是代码中的竞争条件,而不是RhinoMocks中的错误.在方法中设置allTasks任务列表时会发生此问题Start():
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());
}
Run Code Online (Sandbox Code Playgroud)
您需要将foo实例明确地传递给任务.该任务将在不同的线程上执行,并且很可能foreach循环将在任务开始之前替换foo的值.
这意味着每个foo.DoSomething()都被调用有时从不,有时不止一次.出于这个原因,一些任务将无限期地阻塞,因为RhinoMocks无法处理来自不同线程的同一实例上的重叠事件,并且它会陷入死锁.
在您的Start方法中替换此行:
allTasks.Add(Task.Factory.StartNew(() => foo.DoSomething()));
Run Code Online (Sandbox Code Playgroud)
有了这个:
allTasks.Add(Task.Factory.StartNew(f => ((IFoo)f).DoSomething(), foo));
Run Code Online (Sandbox Code Playgroud)
这是一个经典的错误,很容易被忽视.它有时被称为"访问修改后的闭包".
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;
}
Run Code Online (Sandbox Code Playgroud)
它比RhinoMocks更优雅 - 显然更能容忍多个线程同时在同一个实例上引发事件.虽然我不认为这是一个常见的要求 - 我个人并不经常发现你可以认为事件的订阅者是线程安全的情况.