Ответ
Тестирование многопоточного кода сложно, так как нужно выявлять состояния гонки (race conditions), взаимные блокировки (deadlocks) и проблемы с видимостью памяти. Вот практические подходы для C#/.NET.
1. Стресс-тестирование с параллельным выполнением Запускаем тестируемый метод множество раз из разных потоков, чтобы выявить недетерминированные ошибки.
[Test]
public void Counter_ShouldBeThreadSafe_UnderConcurrentAccess()
{
const int iterations = 100000;
int counter = 0;
var options = new ParallelOptions { MaxDegreeOfParallelism = 10 };
// Многократно инкрементируем счетчик из параллельных потоков
Parallel.For(0, iterations, options, i =>
{
// Если Increment() не потокобезопасен, итоговое значение будет меньше iterations
Increment(ref counter);
});
// Утверждение: если код потокобезопасен, счетчик должен быть равен iterations
Assert.That(counter, Is.EqualTo(iterations));
}
// НЕПОТОКОБЕЗОПАСНАЯ реализация для демонстрации
private static void Increment(ref int value) => value = value + 1;
2. Использование примитивов синхронизации в тестах
ManualResetEventSlim, CountdownEvent, Barrier помогают координировать потоки в предсказуемой последовательности для воспроизведения специфических сценариев (например, deadlock).
[Test]
public void ResourceAccess_ShouldNotDeadlock()
{
var lockA = new object();
var lockB = new object();
bool deadlockDetected = false;
var testCompleted = new ManualResetEventSlim();
Task task1 = Task.Run(() =>
{
lock (lockA)
{
Thread.Sleep(10); // Имитация работы
if (Monitor.TryEnter(lockB, 1000)) // Пытаемся захватить с таймаутом
{
Monitor.Exit(lockB);
}
else { deadlockDetected = true; }
lock (lockA) { }
}
});
// ... аналогичный код для task2, захватывающего lockB, затем lockA
Task.WaitAll(task1, task2);
Assert.IsFalse(deadlockDetected, "Обнаружен взаимный блокировка (deadlock)");
}
3. Специализированные библиотеки и инструменты
- Microsoft.VisualStudio.Threading.Analyzers: Статический анализ кода на предмет проблем с deadlock.
- Concurrency Visualizer (в Performance Profiler): Визуализация работы потоков для анализа.
- Тестовые фреймворки: Некоторые (например, xUnit) позволяют запускать один тест параллельно много раз для выявления "хлопающих" (flaky) тестов.
Главный совет: Проектируйте код так, чтобы минимизировать общее изменяемое состояние. Используйте неизменяемые (immutable) структуры данных и потокобезопасные коллекции (ConcurrentBag, ConcurrentDictionary) из пространства имен System.Collections.Concurrent.