Rx TestScheduler выдает исключение NullReference, когда фоновый поток планирует действие

1

В свете некоторых комментариев я должен четко указать, что этот вопрос связан с тем, почему TestScheduler выбрасывает исключение с использованием NULL-ссылки, а не как пройти тест. В более раннем примере предполагалось, что причиной возникновения проблемы является взаимодействие с TPL, но теперь я обнаружил, что это не требуется для запуска поведения, поэтому я заменил код более простым тестовым примером

У меня возникли проблемы с попыткой комбинировать тестовое тестирование rx-test "Virtual Time" с фоновым потоком. Самый простой способ, который я нашел, чтобы продемонстрировать проблему, показан в нижней части сообщения.

Код просто запускает фоновый поток, который подписывается на наблюдаемую последовательность с таймаутом.

Тайм-аут запускается из TestScheduler, и по мере того, как я продвигаю это по основному потоку, генерируется исключение с нулевой ссылкой:

Ошибка Assert.Fail. Виртуальное время 00: 00: 00.1394720, exception System.NullReferenceException: Ссылка на объект не установлена в экземпляр объекта. в System.Reactive.Concurrency.VirtualTimeScheduler 2.GetNext() at System.Reactive.Concurrency.VirtualTimeSchedulerBase 2.AdvanceTo(TAbsolute time) в System.Reactive.Concurrency.VirtualTimeSchedulerBase'2.AdvanceBy (время траления времени) в UnitTestProject1.UnitTest1.d__6.MoveNext() в....... \UnitTest1.cs: строка

Ошибка, как представляется, нечувствительна к точному типу используемого IObservable, но, похоже, зависит от наличия селектора тайм-аута. Интересно, однако, что тест обычно терпит неудачу в виртуальное время задолго до истечения таймаута из-за пожара.

Выполнение того же кода, что и консольное приложение, также работает, хотя проблема, похоже, довольно легко встречается (возможно, состояние гонки), поэтому это может быть красно-селедка.

Дальнейшие исследования и комментарии сильно указывают на то, что поведение связано с состоянием гонки, которое возникает, когда планировщик продвигается, когда фоновый поток назначает свое действие таймаута

Спасибо заранее за любой свет, который вы можете пролить на это...

using Microsoft.Reactive.Testing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Reactive.Linq;
using System.Threading;

namespace UnitTestProject1
{
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestBackgroundThreadWithTestScheduler()
    {
        var scheduler = new TestScheduler();
        var seq = Observable.Never<string>();

        bool subscribed = false;
        ThreadPool.QueueUserWorkItem(_ =>
        {
            Thread.Sleep(100); //wait a bit to give main thread chance to start advancing scheduler
            seq.Timeout(TimeSpan.FromSeconds(10), scheduler)
               .Subscribe(s => {/*never called*/});
            subscribed = true; //signal we're subscribed
        });

        //----  Uncommenting this line to avoid the race condition appears to fix the test ----
        //while (!subscribed) Thread.Yield(); 

        //Advance the scheduer in small increments to maximise our chances of hitting the race
        var watch = scheduler.StartStopwatch();
        try
        {
            while (watch.Elapsed < TimeSpan.FromSeconds(20)) scheduler.AdvanceBy(10);
        }
        //NullReference is thrown unexpectedly
        catch (NullReferenceException ex)
        {
            Assert.Fail("Virtual time {0}, exception {1}", watch.Elapsed, ex);
        }
        catch (TimeoutException)
        {
            //desired result is a TimeoutException so this is a test pass
        }
    }
}
}
  • 0
    Дальнейший комментарий - мне было интересно, может ли планирование тайм-аута не быть потокобезопасным (нет никакой гарантии, что фоновый поток запустится к тому времени, когда мы начнем продвигать планировщик), но вставка 2-секундного сна после task.factory.startnew не является достаточно, чтобы избежать проблемы.
  • 1
    Я не уверен, что происходит с NRE - но это кстати. Что именно вы пытаетесь проверить здесь? Это очень необычный код. Вы понимаете, что Task в переменной task никогда не потерпит неудачу - ее задача - просто запустить другую задачу асинхронно? Кроме того, вы хотели продвигать планировщик испытаний на 10 тиков за раз? Если вы просто хотите проверить тайм-аут задачи, это достаточно просто - но я буду ждать разъяснений.
Показать ещё 4 комментария
Теги:
system.reactive

1 ответ

1
Лучший ответ

Эта строка в stacktrace System.Reactive.Concurrency.VirtualTimeScheduler2.GetNext() - это ключ к тому, что ошибка действительно заключается в том, что вы получаете доступ к планировщику из 2 потоков (TestScheduler не является потокобезопасным, в настоящее время существует ошибка в TestScheduler при доступе к это из нескольких потоков).

Ваш первый комментарий, скорее всего, правильный: фоновая задача добавляет планировщик так же, как ваша другая задача продвигает планировщик.

Попробуйте добавить блокировку вокруг операторов, которые обращаются к планировщику, и посмотреть, разрешает ли это вашу проблему.

Реальное решение, конечно же, состоит в том, чтобы иметь фоновый сигнал задачи, как только он настроился, и перед тем, как продолжить, перед тем, как продолжить тест, подождите. Потому что, даже если у TestScheduler не было своей ошибки, в вашем тесте есть условие гонки, которое тестовый поток может завершить до того, как фоновый поток когда-либо будет подписываться на наблюдаемый.

  • 1
    TestScheduler должен быть потокобезопасным; NRE - это ошибка . Кроме того, SynchronizationContext не имеет к этому никакого отношения; MSTest не предоставляет SynchronizationContext .
  • 0
    Спасибо за оба комментария. Прочитав описание ошибки, я опубликовал гораздо более простой случай воспроизведения, который полностью избегает TPL и просто использует фоновый поток. Вчера я думал, что у меня есть некоторые результаты, которые показали проблему, даже если тайм-аут был запланирован до того, как планировщик был продвинут, но я не могу воспроизвести это сейчас. Блокировка планировщика, кажется, обходит проблему, но уродлива; постановка задачи сигнализации нецелесообразна в реальном коде (смысл использования RX состоит в том, чтобы избежать знания количества абонентов).
Показать ещё 4 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню