【问题标题】:Multithreaded code makes Rhino Mocks cause a Deadlock多线程代码使 Rhino Mocks 导致死锁
【发布时间】:2011-07-01 08:32:48
【问题描述】:

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

为了重现死锁,我们的单元测试执行以下操作: 1. 创建一些 IFoo 模拟 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 并且当前工作线程正在中断

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

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

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

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

【问题讨论】:

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


【解决方案1】:

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

按照这篇文章中的 cmets,我使用 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 - 是的,这是一个 foreach 闭包问题(临时变量也可以) - 你可能只有一个 foo 调用了 50 次 - 但这会“产生”死锁吗?我对 Rhino 不是很熟悉(尽管 mock 是一个 mock :) 但看起来不像任何块,至少明确地(模拟代码)。 IMO 你会在一个 foo 上触发 50 个事件,但看起来仍然要结束。
  • 死锁在 RhinoInterceptor.Intercept 方法上,该方法用 SynchronizedAttribute 标记,相当于一个锁(this)。一旦您在同一个实例上执行多个线程,就会出现坏消息。我没有一步一步地跟踪调试器中的整个调用链(我不喜欢逆向工程 RhinoMocks 和 Castle - 代码很麻烦!),但我发现对 Intercept 的调用在堆栈中出现多次 - 所以看起来像是由两个线程造成的死锁,这两个线程曾经在不同的对象上通过 Intercept 并且现在正试图获取彼此的锁。
  • 这更有意义:),现在很清楚了 - 但它仍然是 IMO 犀牛设计的错误 - 因为那里没有任何东西可以证明锁是合理的(尽管我可以理解如何到达那里很容易) - 即,如果你手工制作 Mock (手动类),它应该可以正常工作 - 如果我没记错的话,即关闭的错误只是提出了隐藏在引擎盖下的问题......虽然这可能解决锁定问题(所以它是对特定 Q 的回答——但似乎有人又想要清除与 Rhino 相关的问题)——你很快就会再次陷入困境。我也没有调试所以...
  • 谢谢 - 用更多信息更新了我的答案。我添加了等效的起订量 - 效果很好。我更喜欢 Moq 处理事件的方式,但不要认为您在这里对 RhinoMocks 过于苛刻,因为真正需要这种情况是非常不寻常的。
  • 好的,很遗憾,上面的解决方案对我不起作用,因为它需要修改相当多的功能。我们的实时目标 'foo' 是线程安全的,但 rhino 不是。不过感谢您提供的信息,您确实完成了工作,并解释了问题,值得加分。
【解决方案2】:

Maggie,从示例中对我来说不是很明显,但如果您有 Visual Studio Ultimate,可能会对您有所帮助...一旦死锁,Break all 进入调试器,然后转到 Debug 菜单并选择:

调试 -> Windows -> 并行堆栈

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-14
    • 1970-01-01
    • 2018-11-09
    • 2020-08-29
    相关资源
    最近更新 更多