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

时间:2011-02-21 15:10:54

标签: c# multithreading unit-testing rhino-mocks deadlock

我们目前在单元测试期间遇到一些问题。我们的类使用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 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;
    }

生成的Foo越多,死锁发生的次数就越多。如果测试不会阻塞,请运行几次,它会。 停止调试testrun显示,所有任务仍然在TaskStatus.Running中并且当前工作线程正在中断

  

[在睡觉,等待或加入]
    Rhino.Mocks.DLL!Rhino.Mocks.Impl.RhinoInterceptor.Intercept(Castle.Core.Interceptor.IInvocation   调用)+ 0x3d字节

让我们最困惑的奇怪之处在于,拦截(...)方法的签名被定义为已同步 - 但是这里有几个线程。我已经阅读了几篇关于Rhino Mocks和Multithreaded的帖子,但没有发现警告(预计会设置记录)或限制。

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

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

2 个答案:

答案 0 :(得分:12)

这是您的代码中的竞争条件,而不是RhinoMocks中的错误。在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));

这是一个经典的错误,很容易被忽视。它有时被称为“访问修改后的闭包”。

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;
}

它比RhinoMocks更优雅 - 显然更能容忍多个线程同时在同一个实例上引发事件。虽然我不认为这是一个常见的要求 - 我个人并不经常发现你可以认为事件的订阅者是线程安全的情况。

答案 1 :(得分:2)

Maggie,从样本中对我来说不是很明显但是如果你有Visual Studio Ultimate可能对你有所帮助......一旦你死锁,打破所有进入调试器然后进入Debug菜单并选择:

调试 - &gt; Windows - &gt;并行堆栈

Visual Studio构建了一个很好的图表,显示了所有正在运行的线程的状态。从那里你通常会得到一些关于哪些锁争用的提示。