Синхронизация потоков: чётные и нечётные числа
Условие
Необходимо написать программу, где два потока выводят числа:
- Первый поток выводит нечётные числа: 1, 3, 5, 7, 9...
- Второй поток выводит чётные числа: 2, 4, 6, 8, 10...
Требуется синхронизировать потоки так, чтобы числа выводились по порядку: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.
Требования:
- Использовать механизмы синхронизации C# (AutoResetEvent, ManualResetEvent, Monitor, Semaphore или другие)
- Объяснить выбор механизма синхронизации
- Код должен быть потокобезопасным
Пример вывода:
1 (Thread 1)
2 (Thread 2)
3 (Thread 1)
4 (Thread 2)
...
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Анализ задачи
Это классическая задача на синхронизацию потоков, где требуется обеспечить упорядоченный вывод чисел. Сложность заключается в необходимости строгого чередования — нечётное число должно вывести первый поток, затем обязательно второй поток выведет чётное число, и так далее.
Выбор механизма синхронизации
Для этой задачи наиболее подходит AutoResetEvent — легковесный примитив синхронизации, который автоматически переходит в несигнализированное состояние после выхода из WaitOne(). Это идеально для синхронизации двух потоков в чередующемся режиме.
Почему именно AutoResetEvent:
- Минимальные накладные расходы (в отличие от Monitor)
- Автоматический сброс сигнала после пробуждения потока
- Нет необходимости в Mutex
- Ясная семантика «один сигнал = пробуждение одного потока»
Реализация
using System;
using System.Threading;
class OddEvenSynchronization
{
private static AutoResetEvent _oddEvent = new AutoResetEvent(true); // Начинает нечётный поток
private static AutoResetEvent _evenEvent = new AutoResetEvent(false); // Блокирует чётный в начале
private const int MaxNumber = 10;
static void Main()
{
Thread oddThread = new Thread(PrintOdd) { Name = "Нечётные" };
Thread evenThread = new Thread(PrintEven) { Name = "Чётные" };
oddThread.Start();
evenThread.Start();
oddThread.Join();
evenThread.Join();
Console.WriteLine("\nВсе числа выведены.");
}
static void PrintOdd()
{
for (int i = 1; i <= MaxNumber; i += 2)
{
_oddEvent.WaitOne(); // Ждём сигнала
Console.WriteLine($"{i} (Thread: {Thread.CurrentThread.Name})");
_evenEvent.Set(); // Сигнализируем чётному потоку
}
}
static void PrintEven()
{
for (int i = 2; i <= MaxNumber; i += 2)
{
_evenEvent.WaitOne(); // Ждём сигнала
Console.WriteLine($"{i} (Thread: {Thread.CurrentThread.Name})");
_oddEvent.Set(); // Сигнализируем нечётному потоку
}
}
}
Альтернативное решение через Monitor
Если требуется более гибкий контроль над состоянием, можно использовать Monitor с обычной переменной-флагом:
using System;
using System.Threading;
class OddEvenWithMonitor
{
private static object _lock = new object();
private static bool _oddTurn = true;
private const int MaxNumber = 10;
static void Main()
{
Thread oddThread = new Thread(PrintOdd);
Thread evenThread = new Thread(PrintEven);
oddThread.Start();
evenThread.Start();
oddThread.Join();
evenThread.Join();
}
static void PrintOdd()
{
for (int i = 1; i <= MaxNumber; i += 2)
{
lock (_lock)
{
while (!_oddTurn)
Monitor.Wait(_lock); // Ждём, пока наступит наша очередь
Console.WriteLine($"{i} (Thread: Odd)");
_oddTurn = false;
Monitor.PulseAll(_lock); // Пробуждаем чётный поток
}
}
}
static void PrintEven()
{
for (int i = 2; i <= MaxNumber; i += 2)
{
lock (_lock)
{
while (_oddTurn)
Monitor.Wait(_lock); // Ждём, пока наступит наша очередь
Console.WriteLine($"{i} (Thread: Even)");
_oddTurn = true;
Monitor.PulseAll(_lock); // Пробуждаем нечётный поток
}
}
}
}
Сравнение подходов
| Подход | Преимущества | Недостатки |
|---|---|---|
| AutoResetEvent | Легковесный, понятный, меньше контекстных переключений | Менее гибкий для сложных сценариев |
| Monitor | Гибкий, можно использовать условные переменные, удобен для множественных потоков | Небольшие накладные расходы на блокировку, может быть сложнее в понимании |
Современный подход: TaskCompletionSource и async/await
Для нового кода рекомендуется использовать асинхронный стиль:
using System;
using System.Threading.Tasks;
class OddEvenAsync
{
private static TaskCompletionSource<bool> _oddTurn = new();
private static TaskCompletionSource<bool> _evenTurn = new();
static async Task Main()
{
_oddTurn.SetResult(true); // Начинаем с нечётного
var oddTask = PrintOddAsync();
var evenTask = PrintEvenAsync();
await Task.WhenAll(oddTask, evenTask);
}
static async Task PrintOddAsync()
{
for (int i = 1; i <= 10; i += 2)
{
await _oddTurn.Task;
Console.WriteLine($"{i} (Odd)");
_evenTurn.TrySetResult(true);
_oddTurn = new TaskCompletionSource<bool>();
}
}
static async Task PrintEvenAsync()
{
for (int i = 2; i <= 10; i += 2)
{
await _evenTurn.Task;
Console.WriteLine($"{i} (Even)");
_oddTurn.TrySetResult(true);
_evenTurn = new TaskCompletionSource<bool>();
}
}
}
Ключевые выводы
Потокобезопасность достигается:
- Исключение гонки за данные через примитивы синхронизации
- Упорядоченный доступ к критической секции
- Контроль чередования потоков с помощью сигналов
Для выбора механизма рекомендуется:
- Простая синхронизация между 2 потоками →
AutoResetEvent - Сложные сценарии с переменной состояния →
Monitor - Новый код →
async/awaitсTaskCompletionSourceилиChannels
Данное решение гарантирует вывод чисел в правильном порядке без потери данных и гонок.