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

标签 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());
    }
}

接口(interface) IFoo 定义为:

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

为了重现死锁,我们的单元测试执行以下操作: 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 越多,死锁发生的频率就越高。如果测试不会阻塞,运行几次,它就会。 停止调试测试运行显示,所有任务仍在 TaskStatus.Running 中,当前工作线程正在中断

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

最让我们困惑的是,Intercept(...) 方法的签名被定义为 Synchronized - 但这里有几个线程。我已经阅读了几篇关于 Rhino Mocks 和多线程的帖子,但没有发现警告(预期设置记录)或限制。

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

我们在设置模拟对象或在多线程环境中使用它们时是否做错了什么?欢迎任何帮助或提示!

最佳答案

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

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

附言:

根据这篇文章的评论,我使用 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 更优雅 - 并且显然更能容忍多个线程同时在同一实例上引发事件。虽然我不认为这是一个常见的要求 - 就我个人而言,我并不经常发现可以假设事件订阅者是线程安全的场景。

关于c# - 多线程代码使 Rhino Mocks 导致死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5067600/

相关文章:

c# - 集成使用 SSL 连接机器的 API

c - 如何使用 OpenMP 并行化在矩阵上进行迭代的 while 循环?

javascript - 带参数的单元测试 AngularJs $resource 查询

python - 包括 Django FileUpload 的 Content-disposition header

unit-testing - 跳过单元测试或至少显示它们的警告

c# - 为什么我的圈子的值为零?

c# - 当我在 Visual Studio 中创建 .net core 项目时,我没有获得 TreeView

c# - 如何以编程方式随机生成游戏对象?

创建两个具有抢占和优先级的任务

ios - UISegmentedControl 具有两个 UICollectionViews 来显示图像。多线程