Операционные системы

Потоки

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
Потоки

0

1

Войти