Многопоточный код заставляет Rhino Mocks вызывать тупик - PullRequest
9 голосов
/ 21 февраля 2011

В настоящее время мы сталкиваемся с некоторыми проблемами во время модульного тестирования. Наш класс является многопоточным вызовом некоторых функций для объектов Mocked с использованием Moh Rhino. Вот пример, приведенный к минимуму:

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. Поднимите myEvent, когда вызывается DoSomething ().

[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 и Multithreaded, но не нашел предупреждений (ожидание настройки записей) или ограничений.

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

Мы что-то делаем неправильно при настройке наших Mockobjects или использовании их в многопоточной среде? Любая помощь или подсказка приветствуется!

Ответы [ 2 ]

12 голосов
/ 21 марта 2012

Это условие гонки в вашем коде, а не ошибка в RhinoMocks.Проблема возникает при настройке списка задач allTasks в методе Start():

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.В этом случае он не блокируется - но имейте в виду, что ожидания, созданные для данного экземпляра, могут быть не удовлетворены, если исходная ошибка не будет исправлена, как описано.GenerateFoo () с использованием Moq выглядит следующим образом:

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, и явно более терпим к нескольким потокам, вызывающим события в одном и том же экземпляре одновременно.Хотя я не думаю, что это является общим требованием - лично я не часто нахожу сценарии, в которых можно предположить, что подписчики на событие поточнобезопасны.

2 голосов
/ 22 апреля 2011

Мэгги, не очевидно для меня из примера, но кое-что, что может помочь вам, если у вас Visual Studio Ultimate ... После того, как вы зашли в тупик, сломайте все, чтобы попасть в отладчик, затем перейдите в меню отладки и выберите:

Отладка -> Windows -> Параллельные стеки

Visual Studio создает хороший график, показывающий состояния всех запущенных потоков.Оттуда вы обычно получаете некоторый намек на то, какие блокировки находятся в споре.

...