Определение безопасности резьбы в модульных тестах

Я пишу многопоточное приложение и пытаюсь выяснить, как писать для него единичные тесты. Я думаю, что, возможно, еще один вопрос о том, как это сделать. Во всяком случае, у меня есть класс вроде ниже, зная его небезопасным потоком и хочу доказать его в unit test, но не могу понять, как это сделать:

public class MyClass
{
    private List<string> MyList = new List<string>();

    public void Add(string Data)
    {
        MyList.Add(Data);  //This is not thread safe!!
    }
}

Ответ 1

Доказательство того, что что-то является потокобезопасным, является сложным - возможно, проблема с остановкой. Вы можете показать, что состояние гонки легко производить, или что его трудно производить. Но не производить условие гонки не означает, что его нет.

Но: мой обычный подход здесь (если у меня есть причина подумать о некотором коде, который должен быть потокобезопасным, это не так), чтобы развернуть много потоков, ожидающих одного ManualResetEvent. Последний поток для доступа к воротам (с использованием блокировки для подсчета) отвечает за открытие ворот, чтобы все потоки попали в систему в одно и то же время (и уже существуют). Затем они выполняют работу и проверяют нормальные условия выхода. Затем я повторяю этот процесс много раз. Обычно этого достаточно, чтобы воспроизвести подозрительную нить-расы и показать, что она движется от "явно разбитого" до "не разбитого явным образом" (что принципиально отличается от "не нарушено" ).

Также обратите внимание: большинство кода не должны быть потокобезопасными.

Ответ 2

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

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

Упреждающее модульное тестирование того, что код является потокобезопасным, немного сложнее. Не потому, что unit test сложнее писать, а потому, что вам нужно провести тщательный анализ, чтобы определить (предположим, действительно), что может быть потоком небезопасным. Если ваш анализ верен, вы должны иметь возможность написать тест, который завершится неудачно, пока вы не создадите безопасный поток кода.

При тестировании состояния гонки нитей мои тесты почти всегда следуют одному и тому же шаблону: (это псевдокод)

boolean failed = false;
int iterations = 100;

// threads interact with some object - either 
Thread thread1 = new Thread(new ThreadStart(delegate() {
   for (int i=0; i<iterations; i++) {
     doSomething(); // call unsafe code
     // check that object is not out of synch due to other thread
     if (bad()) {
       failed = true;
     }
   }
});
Thread thread2 = new Thread(new ThreadStart(delegate() {
   for (int i=0; i<iterations; i++) {
     doSomething(); // call unsafe code
     // check that object is not out of synch due to other thread
     if (bad()) {
       failed = true;
     }
   }
});

thread1.start();
thread2.start();
thread1.join();
thread2.join();
Assert.IsFalse(failed, "code was thread safe");

Ответ 3

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

Ответ 4

Я отказался от модульных тестов, чтобы попытаться обнаружить проблемы с потоками. Я просто использую тест загрузки системы с полным набором аппаратных средств (если есть аппаратное обеспечение - часто с моими заданиями есть, uControllers on networks) и необоснованно большое количество автоматизированных клиентов, выполняющих плавность. Я оставляю его работать в течение недели, пока я делаю другие вещи. В конце недели я снимаю груз и проверяю, что осталось. Если он все еще работает, ни один объект не просочился, и память не заметно увеличилась, я отправляю его.

Это столько качества, сколько я могу себе позволить.

Ответ 5

У меня была похожая проблема, когда мы нашли ошибки безопасности потоков. Чтобы исправить это, мы должны были доказать это и затем исправить это. Этот квест привел меня на эту страницу, но я не смог найти никакого реального ответа. Как многие из приведенных выше ответов объяснили почему. Но тем не менее я нашел способ, который мог бы помочь другим:

public static async Task<(bool IsSuccess, Exception Error)> RunTaskInParallel(Func<Task> task, int numberOfParallelExecutions = 2)
    {
        var cancellationTokenSource = new CancellationTokenSource();
        Exception error = null;
        int tasksCompletedCount = 0;
        var result = Parallel.For(0, numberOfParallelExecutions, GetParallelLoopOptions(cancellationTokenSource),
                      async index =>
                      {
                          try
                          {
                              await task();
                          }
                          catch (Exception ex)
                          {
                              error = ex;
                              cancellationTokenSource.Cancel();
                          }
                          finally
                          {
                              tasksCompletedCount++;
                          }

                      });

        int spinWaitCount = 0;
        int maxSpinWaitCount = 100;
        while (numberOfParallelExecutions > tasksCompletedCount && error is null && spinWaitCount < maxSpinWaitCount))
        {
            await Task.Delay(TimeSpan.FromMilliseconds(100));
            spinWaitCount++;
        }

        return (error == null, error);
    }

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

Вот как мы это использовали:

int numberOfParallelExecutions = 2;
RunTaskInParallel(() => doSomeThingAsync(), numberOfParallelExecutions);

Надеюсь, это кому-нибудь поможет.