Теория языков программирования

CSP: философия конкурентности Go

Цели урока

  • Понять философию CSP: общение вместо разделения памяти
  • Использовать goroutines для лёгкой конкурентности
  • Создавать channels для безопасной коммуникации
  • Применять select для мультиплексирования каналов
  • Знать паттерны: fan-out/fan-in, worker pools, pipelines

Предварительные знания

  • Что такое concurrency и parallelism
  • Проблемы threads: race conditions, deadlocks
  • Базовое понимание любого языка программирования
  • Желательно: опыт с async/await (JavaScript, Python)

10,000 одновременных подключений к серверу. Threads? Это 10GB RAM и context switch hell. Async/await? Callback не так уж и умер. Go предлагает третий путь: goroutines дешевле threads, а channels проще callbacks.

  • **Docker & Kubernetes**: написаны на Go - конкурентность в ДНК
  • **Twitch**: миллионы стримов через Go микросервисы
  • **Uber**: real-time геолокация на Go
  • **Cloudflare**: edge-серверы на Go обрабатывают миллионы RPS
  • **Prometheus & Grafana**: мониторинг инфраструктуры мира на Go

От теории к практике: 1978 → 2009

В 1978 году британский учёный Тони Хоар опубликовал работу "Communicating Sequential Processes" - математическую модель конкурентных вычислений. Идея: вместо разделяемой памяти процессы общаются через каналы. 30 лет спустя Rob Pike (создатель UTF-8 и Plan 9) воплотил эти идеи в Go. Он работал в Bell Labs вместе с Кеном Томпсоном (создатель Unix), и они хотели язык для современных серверов Google. CSP стал идеальным фундаментом.

Проблема: threads - это дорого

Сценарий: веб-сервер. Каждый запрос - отдельный поток. Проблемы:

Async/await (Node.js, Python) решает это через event loop, но код превращается в "callback soup" или "async всё".

Goroutines: лёгкие "потоки"

Go предлагает **goroutines** - функции, которые выполняются конкурентно. Они НЕ являются OS threads!

**Ключевое слово `go`**: одно слово превращает любой вызов функции в конкурентный. Никаких Thread, ExecutorService, async/await. Просто `go f()`.

Channels: безопасное общение

Goroutines должны как-то обмениваться данными. Shared memory? Race conditions! Go предлагает **channels** - типизированные "трубы" для передачи данных.

**Мантра Go**: "Don't communicate by sharing memory; share memory by communicating." Не общайтесь через разделяемую память - делитесь памятью через общение.

Аналогия: обычная почта. Письмо опускается в ящик (`chan <-`), адресат достаёт из своего (`<-chan`). Письмо физически перемещается - нет двух копий, нет race condition.

Buffered vs Unbuffered Channels

Select: мультиплексирование каналов

Что если нужно слушать несколько каналов одновременно? **Select** - как switch для каналов.

Timeout и default

Паттерны CSP

Pipeline: конвейер обработки

Fan-out / Fan-in: параллельная обработка

Worker Pool: ограничение параллелизма

**Частые ошибки с goroutines:** 1. **Забыть про close(ch)** - получатель с `range` будет ждать вечно 2. **Goroutine leak** - запустили goroutine, но не дали ей завершиться 3. **Закрыть канал дважды** - panic! 4. **Записать в закрытый канал** - panic!

Практика

CSP: разделяй коммуникацией, не памятью

В чём главное отличие CSP от традиционного подхода с threads?

Суть CSP: вместо mutex вокруг shared data - goroutines передают данные по channels. Один владеет данными в каждый момент времени → race condition невозможен по конструкции.

Goroutines, channels и select

Что делает `select` в Go?

select ждёт первый готовый case. Если несколько готовы одновременно - выбирает случайный. default case делает select неблокирующим. Это основа fan-in паттерна.

Паттерны конкурентности в CSP

Как предотвратить утечку горутин (goroutine leak)?

Горутина живёт пока не вернётся из функции. Без done-channel горутина может ждать вечно на заблокированном channel. context.WithCancel даёт сигнал всем горутинам в дереве завершиться.

ПроблемаOS ThreadПоследствия
Память~1-8 MB stack на поток10K потоков = 10-80 GB RAM
Создание~1msМедленный старт для short-lived задач
Context switch~1-10µsCPU тратит время на переключение
Лимиты~10K потоков на процессНе масштабируется
OS ThreadGoroutine
Память~1-8 MB~2 KB (в 1000 раз меньше!)
Создание~1 ms~1 µs (в 1000 раз быстрее!)
Количество~10K~1M (легко миллион!)
УправлениеOS SchedulerGo Runtime Scheduler
Context switch~1-10 µs~100 ns

Почему можно создать миллион goroutines, но не миллион OS threads?

Goroutine стартует с ~2KB стека и растёт по необходимости. OS thread требует заранее выделить ~1-8MB. Миллион goroutines = ~2GB RAM, миллион threads = ~1-8TB RAM (невозможно).

ТипКогда использовать
UnbufferedНужна синхронизация, handshake между goroutines
BufferedProducer быстрее consumer, batch processing

Что произойдёт при отправке в unbuffered channel без получателя?

Unbuffered channel требует, чтобы отправитель и получатель встретились. Если получателя нет - отправитель ждёт вечно. Go runtime обнаружит deadlock и завершит программу с ошибкой.

Реализуйте функцию, которая делает HTTP запросы к нескольким URL параллельно и возвращает первый успешный ответ (race pattern). ```go func fetchFirst(urls []string) (string, error) { // Ваш код здесь // Запустите goroutine для каждого URL // Верните первый успешный результат } func main() { urls := []string{ "https://api1.example.com/data", "https://api2.example.com/data", "https://api3.example.com/data", } result, err := fetchFirst(urls) if err != nil { log.Fatal(err) } fmt.Println("Got:", result) } ```

func fetchFirst(urls []string) (string, error) { results := make(chan string, len(urls)) errors := make(chan error, len(urls)) for _, url := range urls { go func(u string) { resp, err := http.Get(u) if err != nil { errors <- err return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) results <- string(body) }(url) } select { case result := <-results: return result, nil case <-time.After(5 * time.Second): return "", fmt.Errorf("all requests timed out") } } // Бонус: собрать все ошибки если все запросы упали func fetchFirstWithErrors(urls []string) (string, error) { type result struct { body string err error } results := make(chan result, len(urls)) for _, url := range urls { go func(u string) { resp, err := http.Get(u) if err != nil { results <- result{err: err} return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) results <- result{body: string(body)} }(url) } var errs []error for i := 0; i < len(urls); i++ { select { case r := <-results: if r.err == nil { return r.body, nil } errs = append(errs, r.err) case <-time.After(5 * time.Second): return "", fmt.Errorf("timeout") } } return "", fmt.Errorf("all failed: %v", errs) }

Реализуйте rate limiter: не более N запросов в секунду. ```go // Rate limiter должен пропускать не более rps запросов в секунду func NewRateLimiter(rps int) *RateLimiter { // Ваш код } func (r *RateLimiter) Wait() { // Блокирует пока не придёт "разрешение" } func main() { limiter := NewRateLimiter(5) // 5 запросов в секунду for i := 0; i < 20; i++ { limiter.Wait() fmt.Printf("Request %d at %v\n", i, time.Now()) } } ```

type RateLimiter struct { tokens chan struct{} stop chan struct{} } func NewRateLimiter(rps int) *RateLimiter { r := &RateLimiter{ tokens: make(chan struct{}, rps), // Burst capacity stop: make(chan struct{}), } // Заполняем начальный буфер for i := 0; i < rps; i++ { r.tokens <- struct{}{} } // Генерируем токены с нужной частотой go func() { ticker := time.NewTicker(time.Second / time.Duration(rps)) defer ticker.Stop() for { select { case <-ticker.C: select { case r.tokens <- struct{}{}: // Добавили токен default: // Буфер полон, пропускаем } case <-r.stop: return } } }() return r } func (r *RateLimiter) Wait() { <-r.tokens // Блокируется пока нет токена } func (r *RateLimiter) Stop() { close(r.stop) }

CSP в контексте

Как Go's CSP соотносится с другими моделями конкурентности

  • Async/Await — JS/Python подход - event loop вместо goroutines
  • Actor Model — Erlang подход - похож на CSP, но с mailboxes
  • Threads & Locks — Традиционный подход - что Go заменяет
  • Rust Ownership — Другой способ предотвращения data races

Итоги

  • **Goroutines** - лёгкие (~2KB) единицы конкурентности, можно создать миллион
  • **Channels** - типизированные "трубы" для безопасной коммуникации
  • **Select** - мультиплексирование нескольких каналов
  • **Философия** - "share memory by communicating", не наоборот
  • **Паттерны** - pipeline, fan-out/fan-in, worker pool
  • **Преимущество** - проще чем threads+locks, эффективнее чем async/await

Вопросы для размышления

  • В каких проектах CSP подошёл бы лучше, чем async/await?
  • Почему Kubernetes и Docker написаны именно на Go?
  • Как объяснить разницу между goroutines и threads джуну?

Связанные уроки

  • os-03-threads
CSP: философия конкурентности Go

0

1

Войти