← Назад к вопросам
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)- увеличивает счетчик, закрывает канал если он стал 0Done()- уменьшает счетчик на 1Wait()- ждет пока счетчик не станет 0
Минусы такой реализации:
- Канал нельзя переиспользовать после закрытия - нужно создавать новый
- После Wait() WaitGroup нельзя использовать повторно (нужен новый)
- Есть race condition при переоткрытии канала
Зачем это нужно?
- Образовательная ценность - понимание как работают каналы и синхронизация
- Альтернативные подходы - иногда каналы удобнее для сложной синхронизации
- Паттерны 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, что противоречит условию задачи.