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

相关文章

在要实现单例模式的类当中添加如下代码:实例化的时候:frmC...
1、如果制作圆角窗体,窗体先继承DOTNETBAR的:public parti...
根据网上资料,自己很粗略的实现了一个winform搜索提示,但是...
近期在做DSOFramer这个控件,打算自己弄一个自定义控件来封装...
今天玩了一把WMI,查询了一下电脑的硬件信息,感觉很多代码都...
最近在研究WinWordControl这个控件,因为上级要求在系统里,...