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

发布于 2024-10-18 13:11:51 字数 2137 浏览 7 评论 0原文

我们目前在单元测试期间遇到一些问题。我们的类使用 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 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 状态,并且当前工作线程正在中断

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

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

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

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

We're currently facing some issues during Unit Testing. Our class is multithreading some function calls on Mocked objects using Rhino Mocks. Here's a example reduced to the minimum:

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

The Interface IFoo is defined as:

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

To reproduce the deadlock, our unittest does the following:
1. create some IFoo Mocks
2. Raise myEvent when DoSomething() gets called.

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

The more Foo's are generated, the more often the deadlock occurs. If the test won't block, run it several times, and it will.
Stopping the debugging testrun shows, that all Tasks are still in TaskStatus.Running and the current worker thread is breaking at

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

The weird thing which confuses us most is the fact, that the signature of the Intercept(...) Method is defined as Synchronized - but several Threads are located here. I've read several postings about Rhino Mocks and Multithreaded, but havn't found warnings (expected setting up the records) or limitations.

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

Are we doing something completely wrong on setting up our Mockobjects or using them in a multithreaded environment? Any help or hint is welcome!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

仲春光 2024-10-25 13:11:51

这是代码中的竞争条件,而不是 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更优雅——并且显然更能容忍多个线程同时在同一实例上引发事件。尽管我不认为这是一个常见的要求 - 就我个人而言,我并不经常发现可以假设事件的订阅者是线程安全的场景。

This is a race condition in your code and not a bug in RhinoMocks. The problem occurs when you are setting up the allTasks task list in the Start() method:

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

You need to pass the foo instance explicitly into the task. The task will execute on a different thread and it's very likely that the foreach loop will replace the value of foo before the task has started.

This means that each foo.DoSomething() is being invoked sometimes never and sometimes more than once. For this reason, some of the tasks will block indefinitely because RhinoMocks can't handle overlapped raising of events on the same instance from different threads and it gets into a deadlock.

Replace this line in your Start method:

allTasks.Add(Task.Factory.StartNew(() => foo.DoSomething())); 

With this:

allTasks.Add(Task.Factory.StartNew(f => ((IFoo)f).DoSomething(), foo));

This is a classic bug that is subtle and very easy to overlook. It is sometimes referred to as "accessing a modified closure".

PS:

Following the comments on this post, I rewrote this test using Moq. In this case it doesn't block - but beware that expectations created on a given instance might not be satisfied unless the original bug is fixed as described. GenerateFoo() using Moq looks like this:

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

It's more elegant than RhinoMocks - and clearly more tolerant of multiple threads raising events on the same instance simultaneously. Although I don't imagine this is a common requirement - personally I don't often find scenarios where you can assume the subscribers to an event are thread-safe.

梨涡 2024-10-25 13:11:51

Maggie,从示例中对我来说并不明显,但如果您有 Visual Studio Ultimate,可能会对您有所帮助...一旦出现死锁,请打破所有进入调试器,然后转到“调试”菜单并选择:

“调试”->“调试”。窗口 ->并行堆栈

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

Maggie, Not obvious to me from the sample but something that might help you if you have Visual studio Ultimate... Once you deadlock, Break all to get into the debugger then go to the Debug menu and select:

Debug -> Windows -> Parallel Stacks

Visual studio builds a nice graph showing the states of all the running threads. From there you usually get some kind of hint as to which locks are in contention.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文