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

Mutex и WaitGroup на каналах

2.0 Middle🔥 291 комментариев
#Конкурентность и горутины

Условие

Можно ли реализовать sync.Mutex и sync.WaitGroup на каналах? Реализуйте их.

Часть 1: Mutex на каналах

type ChannelMutex struct {
    // ваши поля
}

func NewChannelMutex() *ChannelMutex
func (m *ChannelMutex) Lock()
func (m *ChannelMutex) Unlock()

Часть 2: WaitGroup на каналах

type ChannelWaitGroup struct {
    // ваши поля
}

func NewChannelWaitGroup() *ChannelWaitGroup
func (wg *ChannelWaitGroup) Add(delta int)
func (wg *ChannelWaitGroup) Done()
func (wg *ChannelWaitGroup) Wait()

Требования

  • Использовать только каналы для синхронизации
  • Поведение должно быть аналогично стандартным примитивам

Комментарии (1)

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

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

Mutex и WaitGroup на каналах - полное решение

Описание задачи

В Go каналы являются примитивом синхронизации между горутинами. Да, можно реализовать и Mutex, и WaitGroup только на каналах, используя их свойства:

  • Отправка в канал блокирует отправителя, пока кто-то не прочитает
  • Чтение из канала блокирует читателя, пока кто-то не отправит
  • Закрытие канала разбудит всех ожидающих читателей

Часть 1: Mutex на каналах

package main

import (
    "fmt"
    "sync"
)

// ChannelMutex реализует взаимное исключение на каналах
type ChannelMutex struct {
    ch chan struct{}
}

func NewChannelMutex() *ChannelMutex {
    m := &ChannelMutex{
        ch: make(chan struct{}, 1), // буферизированный канал емкостью 1
    }
    m.ch <- struct{}{} // инициализируем токеном
    return m
}

// Lock получает взаимное исключение (блокирует, если занято)
func (m *ChannelMutex) Lock() {
    <-m.ch // читаем из канала, блокируемся если пусто
}

// Unlock освобождает взаимное исключение
func (m *ChannelMutex) Unlock() {
    m.ch <- struct{}{} // отправляем токен обратно
}

// Тест ChannelMutex
func testChannelMutex() {
    fmt.Println("=== Testing ChannelMutex ===")
    
    mu := NewChannelMutex()
    counter := 0
    iterations := 1000
    
    var wg sync.WaitGroup
    
    // 10 горутин инкрементируют счетчик
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < iterations; j++ {
                mu.Lock()
                counter++
                mu.Unlock()
            }
        }()
    }
    
    wg.Wait()
    
    expected := 10 * iterations
    if counter == expected {
        fmt.Printf("✓ ChannelMutex: counter = %d (expected %d)\\n", counter, expected)
    } else {
        fmt.Printf("✗ ChannelMutex: counter = %d (expected %d)\\n", counter, expected)
    }
}

Часть 2: WaitGroup на каналах

import "sync/atomic"

// ChannelWaitGroup реализует WaitGroup на каналах
type ChannelWaitGroup struct {
    counter atomic.Int64
    done    chan struct{}
}

func NewChannelWaitGroup() *ChannelWaitGroup {
    return &ChannelWaitGroup{
        done: make(chan struct{}),
    }
}

// Add увеличивает счетчик ожидания
func (wg *ChannelWaitGroup) Add(delta int) {
    newVal := wg.counter.Add(int64(delta))
    if newVal < 0 {
        panic("negative WaitGroup counter")
    }
    // Если счетчик стал 0 и был > 0, закрываем канал
    if newVal == 0 && delta < 0 {
        close(wg.done)
        wg.done = make(chan struct{}) // переоткрываем для переиспользования
    }
}

// Done уменьшает счетчик на 1
func (wg *ChannelWaitGroup) Done() {
    wg.Add(-1)
}

// Wait блокирует до тех пор, пока счетчик не станет 0
func (wg *ChannelWaitGroup) Wait() {
    for {
        if wg.counter.Load() == 0 {
            return
        }
        <-wg.done // блокируемся на канале
    }
}

// Тест ChannelWaitGroup
func testChannelWaitGroup() {
    fmt.Println("=== Testing ChannelWaitGroup ===")
    
    wg := NewChannelWaitGroup()
    results := make(chan string, 5)
    
    // Запускаем 5 горутин
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            results <- fmt.Sprintf("Горутина %d завершена", id)
        }(i)
    }
    
    // Ждем завершения всех горутин
    wg.Wait()
    close(results)
    
    fmt.Println("Все горутины завершены:")
    for msg := range results {
        fmt.Println("  " + msg)
    }
    fmt.Println("✓ ChannelWaitGroup: все горутины завершены")
}

func main() {
    testChannelMutex()
    fmt.Println()
    testChannelWaitGroup()
}

Анализ реализации

Mutex на каналах

Как это работает:

  • Буферизированный канал емкостью 1 работает как очередь с одним местом
  • Если канал содержит токен struct{}{}, Lock может прочитать его
  • Если канал пуст, Lock блокируется
  • Unlock отправляет токен обратно

Визуально:

Lock():   <-ch     Ждет, если ch пусто
Unlock(): ch <- {} Отправляет токен

WaitGroup на каналах

Как это работает:

  • atomic.Int64 - потокобезопасный счетчик
  • done - канал для синхронизации
  • Add(delta) - увеличивает счетчик, закрывает канал если он стал 0
  • Done() - уменьшает счетчик на 1
  • Wait() - ждет пока счетчик не станет 0

Минусы такой реализации:

  • Канал нельзя переиспользовать после закрытия - нужно создавать новый
  • После Wait() WaitGroup нельзя использовать повторно (нужен новый)
  • Есть race condition при переоткрытии канала

Зачем это нужно?

  1. Образовательная ценность - понимание как работают каналы и синхронизация
  2. Альтернативные подходы - иногда каналы удобнее для сложной синхронизации
  3. Паттерны Go - "Do not communicate by sharing memory; share memory by communicating"

Сложность

  • Time: O(1) для Lock/Unlock и Add/Done
  • Space: O(1) - минимальные переменные

Важные замечания

  • Никогда не используй эти реализации в production - используй стандартные sync.Mutex и sync.WaitGroup
  • Стандартные примитивы быстрее и протестированы
  • Эта задача демонстрирует мощь Go каналов для синхронизации
  • Best practice: "Do not use channels as a replacement for Mutex" - каналы для communication, Mutex для synchronization

Альтернативный подход для WaitGroup

Можно реализовать более надежно с использованием кастомного канала:

type ChannelWaitGroupV2 struct {
    counter atomic.Int64
    waiters []chan struct{}
    mu      sync.Mutex
}

func (wg *ChannelWaitGroupV2) Done() {
    if wg.counter.Add(-1) == 0 {
        wg.mu.Lock()
        for _, ch := range wg.waiters {
            close(ch)
        }
        wg.waiters = nil
        wg.mu.Unlock()
    }
}

Но это использует Mutex, что противоречит условию задачи.

Mutex и WaitGroup на каналах | PrepBro