Операционные системы
Потоки
1999 год. Дэн Кегель публикует эссе «The C10K problem»: как обслужить 10 000 одновременных соединений на одном сервере. Apache с моделью thread-per-request упирается в память: каждый поток требует 1-8MB стека, 10K потоков съедают десятки гигабайт. В 2002 Игорь Сысоев в Рамблере начинает писать Nginx с асинхронной event-loop архитектурой, в октябре 2004 выпускает первую публичную версию. К 2026 Nginx обслуживает около трети всех активных сайтов в интернете. Архитектура потоков не академика, это деньги.
- **Веб-серверы** (Apache, Nginx) - тысячи одновременных HTTP-запросов через thread pools
- **Game engines** - рендеринг в одном потоке, физика во втором, AI в третьем, звук в четвёртом
- **Базы данных** (PostgreSQL, MySQL) - каждое клиентское соединение обрабатывается отдельным потоком или процессом
Цели урока
- Различать процесс и поток: общий address space, отдельные stack/registers/PC
- Сравнить threading models: 1:1 (Linux NPTL), N:1 (green threads), M:N (Go goroutines)
- Знать thread pool и почему пул бьёт create-on-demand при high RPS
- Оценить context switch: ~1µs user, ~5-10µs kernel, ~100µs cross-NUMA
- Применять примитивы: pthread_create, join, detach, thread-local storage
Dijkstra и рождение синхронизации
В 1965 году Дейкстра описал проблему взаимного исключения (mutual exclusion) и придумал семафоры - первый механизм синхронизации потоков. Статья называлась 'Cooperating Sequential Processes'. До неё программисты боролись с race conditions интуитивно и проигрывали. Дейкстра показал: это математическая проблема с формальным решением. Современные mutex, condition variable, monitor - всё это потомки его семафора 1965 года.
Thread Concept
**Поток (thread)** - минимальная единица исполнения в процессе. Процесс имеет собственное адресное пространство. Потоки внутри него делят это пространство, но у каждого - свой стек и регистры.
Потоки одного процесса работают в одном адресном пространстве - коммуникация быстрее, чем IPC. Но без синхронизации возникают race conditions.
Каждый поток имеет собственный **Thread Control Block (TCB)**: - Thread ID - Program Counter - Регистры процессора - Указатель стека - Состояние (running, ready, blocked)
**Многопоточность** позволяет одному процессу выполнять несколько задач параллельно. Браузер загружает файл в одном потоке, отрисовывает страницу в другом и обрабатывает клики в третьем - всё одновременно.
Несколько потоков одновременно изменяют общие данные без синхронизации - возникают **race conditions**. Результат зависит от порядка выполнения, а порядок непредсказуем.
Что является уникальным для каждого потока внутри процесса?
Thread vs Process
**Процесс** - экземпляр программы с собственным адресным пространством. **Поток** - единица выполнения внутри процесса. Выбор между ними определяет архитектуру всей системы.
**Преимущества потоков:** 1. **Скорость создания** - создание потока в ~100 раз быстрее создания процесса 2. **Быстрое переключение контекста** - не требуется смена таблиц страниц 3. **Эффективная коммуникация** - через общую память вместо IPC 4. **Экономия ресурсов** - потоки делят код, данные, файлы
**Преимущества процессов:** 1. **Изоляция** - ошибка в одном процессе не убивает другие 2. **Безопасность** - процессы не могут читать память друг друга 3. **Стабильность** - краш потока убивает весь процесс, краш процесса - только его 4. **Распределённость** - процессы могут работать на разных машинах
**Когда потоки:** Параллельная обработка в рамках одной задачи (веб-сервер, GUI с фоновыми задачами). **Когда процессы:** Изолированные независимые задачи, требующие безопасности (вкладки браузера, микросервисы).
В Python GIL (Global Interpreter Lock) не позволяет нескольким потокам одновременно выполнять Python-код. Для CPU-bound задач - `multiprocessing`, для I/O-bound - `threading` или `asyncio`.
Почему создание потока быстрее создания процесса?
Threading Models
Три основные модели реализации потоков: 1. **User-level threads (ULT)** - управляются библиотекой в user space 2. **Kernel-level threads (KLT)** - управляются ядром ОС 3. **Hybrid (M:N model)** - комбинация обоих подходов
**Примеры реализаций:** - **User-level:** Green Threads в старых JVM, GNU Portable Threads - **Kernel-level:** Linux (NPTL), Windows threads, современные JVM - **Hybrid:** Go runtime (goroutines), Solaris (LWP)
**Современный тренд:** Большинство ОС используют One-to-One (kernel-level). User-level потоки возвращаются как «green threads» или «fibers» для высоконагруженных async-систем - именно это делает Go с goroutines.
User-level поток делает блокирующий syscall (например, read() на пустом сокете) - ядро блокирует весь процесс, включая все остальные user-level потоки. Kernel-level потоки решают эту проблему.
Какая модель потоков позволяет одному потоку заблокироваться в syscall, не блокируя остальные потоки процесса?
Thread Pools
**Thread Pool** - набор заранее созданных потоков, готовых выполнять задачи. Задача отправляется в очередь и выполняется свободным потоком из пула. Nginx и Node.js построены именно так.
**Преимущества Thread Pool:** 1. **Переиспользование** - потоки создаются один раз, используются многократно 2. **Контроль ресурсов** - ограничивает количество одновременных потоков 3. **Производительность** - нет overhead на создание/уничтожение потоков 4. **Предсказуемость** - фиксированное количество потоков упрощает отладку
**Размер пула:** Для CPU-bound задач оптимально N+1 поток (где N - количество ядер). Для I/O-bound - можно больше, потоки часто блокируются в ожидании I/O.
**Типы Thread Pools:** - **Fixed Thread Pool** - фиксированное число потоков - **Cached Thread Pool** - создаёт потоки по требованию, переиспользует неактивные - **Single Thread Executor** - один поток, задачи последовательно - **Scheduled Thread Pool** - для отложенного и периодического выполнения
**Deadlock риск:** Задачи в пуле ждут результаты друг друга - все потоки заняты, новые задачи не запускаются. Классический deadlock thread pool.
Чем больше потоков в пуле, тем быстрее работает приложение
Оптимальный размер зависит от типа задач: CPU-bound - N+1, I/O-bound - больше
Слишком много потоков приводит к overhead на переключение контекста и конкуренцию за ресурсы. Для CPU-bound задач избыточные потоки простаивают в очереди планировщика, замедляя систему.
Почему Thread Pool эффективнее создания нового потока для каждой задачи?
Ключевые идеи
- **Поток** - единица выполнения внутри процесса. Делит память, но имеет собственные стек и регистры
- **Потоки vs Процессы:** Потоки легковеснее (быстрее создаются, переключаются), но не изолированы
- **Threading Models:** User-level (быстро, нет параллелизма), Kernel-level (параллелизм, overhead), Hybrid (баланс)
- **Thread Pools** переиспользуют потоки для многих задач. Оптимальный размер: CPU-bound N+1, I/O-bound больше
Связанные темы
Потоки - основа параллельного программирования и взаимодействуют со многими аспектами ОС:
- CPU Scheduling — Планировщик ОС распределяет CPU время между потоками (и процессами)
- Synchronization — Потоки нуждаются в синхронизации (мьютексы, семафоры) для безопасного доступа к общим данным
- Deadlocks — Многопоточность может привести к deadlocks при неправильной синхронизации
Вопросы для размышления
- Когда в приложении лучше использовать потоки, а когда - процессы?
- Как размер thread pool влияет на производительность CPU-bound и I/O-bound задач?
- Почему в Python для CPU-bound задач рекомендуется multiprocessing вместо threading?
Связанные уроки
- os-02-processes — Процессы - контекст для понимания потоков
- os-04-scheduling — Планировщик распределяет CPU между потоками
- os-05-sync — Синхронизация решает race conditions потоков
- os-06-deadlocks — Deadlocks возникают из-за неправильной синхронизации
- par-01 — Параллельное программирование строится на потоках
- os-19-containers — Контейнеры изолируют процессы, но разделяют ядро
- arch-14-multicore — Hardware-параллелизм - то, что эксплуатируют потоки
- db-25-connection-pooling
- rt-11