← Назад к вопросам

Синхронизация потоков: чётные и нечётные числа

1.8 Middle🔥 191 комментариев
#Асинхронность и многопоточность

Условие

Необходимо написать программу, где два потока выводят числа:

  • Первый поток выводит нечётные числа: 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)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Анализ задачи

Это классическая задача на синхронизацию потоков, где требуется обеспечить упорядоченный вывод чисел. Сложность заключается в необходимости строгого чередования — нечётное число должно вывести первый поток, затем обязательно второй поток выведет чётное число, и так далее.

Выбор механизма синхронизации

Для этой задачи наиболее подходит 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>();
        }
    }
}

Ключевые выводы

Потокобезопасность достигается:

  • Исключение гонки за данные через примитивы синхронизации
  • Упорядоченный доступ к критической секции
  • Контроль чередования потоков с помощью сигналов

Для выбора механизма рекомендуется:

  1. Простая синхронизация между 2 потоками → AutoResetEvent
  2. Сложные сценарии с переменной состояния → Monitor
  3. Новый код → async/await с TaskCompletionSource или Channels

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

Синхронизация потоков: чётные и нечётные числа | PrepBro