Поток выполнения
Поток выполнения (также поток, нить, тред; от англ. thread of execution) — это наименьшая единица обработки, которую операционная система может планировать и выполнять независимо. Поток выполнения представляет собой последовательность инструкций (команд) процессора, выполняемых в рамках одного процесса. В отличие от процесса, который обладает собственным адресным пространством, потоки одного процесса разделяют это пространство, а также другие ресурсы (открытые файлы, сокеты, семафоры), что позволяет им эффективно взаимодействовать и обмениваться данными.
История
Концепция многопоточности начала формироваться в 1960-х годах с появлением мультипрограммирования. Первые операционные системы, такие как CTSS (Compatible Time-Sharing System), поддерживали только однопоточные процессы. Идея разделения процесса на несколько независимых потоков выполнения была впервые реализована в операционной системе OS/360 компании IBM, где использовались «задачи» (tasks), по сути являвшиеся потоками.
Значительный шаг вперёд был сделан в 1980-х годах с появлением операционной системы Unix. В System V Release 4 (SVR4) и 4.3BSD были введены первые реализации потоков на уровне ядра. Однако настоящий прорыв произошёл в 1990-х годах с развитием многопроцессорных систем. Операционные системы Windows NT (1993) и Linux (с версии 2.0) получили полноценную поддержку многопоточности. В 1995 году стандарт POSIX.1c (также известный как POSIX Threads, или Pthreads) унифицировал интерфейс для работы с потоками в Unix-подобных системах.
Современные операционные системы (Windows, Linux, macOS, FreeBSD) поддерживают многопоточность на уровне ядра, а аппаратная поддержка (гиперпоточность, SMT — Simultaneous Multithreading) позволяет одному физическому ядру процессора выполнять несколько потоков одновременно.
Классификация потоков выполнения
По реализации
- Потоки на уровне пользователя (User-Level Threads, ULT). Управляются библиотекой потоков в пространстве пользователя, без участия ядра операционной системы. Ядро «видит» только один процесс, а планирование потоков внутри него осуществляется приложением. Преимущества: низкие накладные расходы на создание и переключение, отсутствие необходимости в системных вызовах. Недостатки: блокировка одного потока (например, при системном вызове) блокирует весь процесс; невозможность использования нескольких ядер процессора.
- Потоки на уровне ядра (Kernel-Level Threads, KLT). Управляются непосредственно операционной системой. Каждый поток является самостоятельной единицей планирования. Преимущества: возможность параллельного выполнения на нескольких ядрах; блокировка одного потока не блокирует другие. Недостатки: более высокие накладные расходы на создание и переключение (требуется переключение в режим ядра).
- Гибридные потоки (Hybrid Threads). Комбинируют оба подхода. Библиотека потоков управляет несколькими пользовательскими потоками, которые отображаются на меньшее количество потоков ядра (модель «многие-к-нескольким»). Примеры: реализация потоков в Solaris до версии 9, некоторые реализации Java Virtual Machine (JVM).
По отношению к процессу
- Однопоточный процесс. Содержит ровно один поток выполнения. Классическая модель процессов в ранних версиях Unix.
- Многопоточный процесс. Содержит два или более потоков. Все потоки разделяют адресное пространство процесса, но имеют собственный стек и регистровый контекст.
Устройство и характеристики
Каждый поток выполнения характеризуется следующими компонентами:
- Идентификатор потока (Thread ID, TID). Уникальный числовой идентификатор, присваиваемый операционной системой или библиотекой потоков.
- Стек потока (Thread Stack). Область памяти, используемая для хранения локальных переменных, параметров функций и адресов возврата. Каждый поток имеет собственный стек, размер которого обычно фиксирован (по умолчанию 1–8 МБ в зависимости от ОС).
- Регистровый контекст (Register Context). Состояние регистров процессора (счётчик команд, указатель стека, флаги), которое сохраняется при переключении контекста.
- Приоритет (Priority). Числовое значение, определяющее, насколько часто поток получает процессорное время. Планировщик ОС использует приоритеты для распределения времени.
- Состояние (State). Поток может находиться в одном из состояний: выполнение (running), готовность (ready), ожидание (waiting/blocked), завершение (terminated).
Разделяемые ресурсы
Потоки одного процесса совместно используют:
- Адресное пространство (код, глобальные и статические переменные, динамически выделенная память — куча).
- Дескрипторы открытых файлов (файловые дескрипторы в Unix, хендлы в Windows).
- Сокеты, каналы (pipes), очереди сообщений.
- Идентификатор процесса (PID), группу процессов, сессию.
- Текущий рабочий каталог.
- Обработчики сигналов (в Unix-подобных системах).
Синхронизация потоков
Поскольку потоки разделяют память, одновременный доступ к общим данным может приводить к состояниям гонки (race conditions), когда результат выполнения зависит от порядка доступа. Для предотвращения этого используются механизмы синхронизации:
- Мьютекс (Mutex, от mutual exclusion). Блокировка, которая позволяет только одному потоку войти в критическую секцию. Поток, пытающийся захватить занятый мьютекс, блокируется до его освобождения.
- Семафор (Semaphore). Счётчик, который позволяет ограничить количество потоков, одновременно работающих с ресурсом. Бинарный семафор (0 или 1) эквивалентен мьютексу.
- Условная переменная (Condition Variable). Позволяет потоку ожидать наступления определённого условия (например, появления данных в очереди) и пробуждаться при его выполнении.
- Блокировка чтения-записи (Read-Write Lock, RW-Lock). Позволяет множеству потоков одновременно читать данные, но блокирует запись, пока читатели активны.
- Атомарные операции (Atomic Operations). Операции (например, сравнение с обменом — CAS, compare-and-swap), которые выполняются как единое неделимое действие, без возможности прерывания.
Проблемы многопоточности
- Взаимная блокировка (Deadlock). Ситуация, когда два или более потоков заблокированы навсегда, ожидая друг от друга освобождения ресурсов. Например, поток A захватил мьютекс 1 и ждёт мьютекс 2, а поток B захватил мьютекс 2 и ждёт мьютекс 1.
- Голодание (Starvation). Поток не получает доступа к ресурсу, несмотря на то, что он не заблокирован, из-за того, что другие потоки постоянно его опережают.
- Инверсия приоритета (Priority Inversion). Низкоприоритетный поток удерживает ресурс, необходимый высокоприоритетному потоку, в то время как средний приоритет вытесняет низкий, блокируя высокий.
Применение
Многопоточность широко используется в различных областях:
- Веб-серверы и серверы баз данных. Обработка множества одновременных запросов от клиентов. Каждый запрос может обслуживаться отдельным потоком (или пулом потоков). Примеры: Apache HTTP Server (использует пул потоков), Nginx (использует асинхронную модель, но также может задействовать потоки).
- Графические пользовательские интерфейсы (GUI). Отдельный поток (обычно главный) отвечает за обновление интерфейса и обработку событий, а фоновые потоки выполняют длительные операции (загрузка файлов, расчёты), чтобы интерфейс не «зависал».
- Научные и инженерные расчёты. Распараллеливание вычислений на несколько ядер процессора для ускорения обработки больших массивов данных (матричные операции, симуляции, обработка изображений).
- Мультимедиа. Одновременное декодирование видео и аудио, рендеринг графики, обработка звука в реальном времени.
- Операционные системы. Ядро ОС само использует потоки для обслуживания системных вызовов, управления устройствами, планирования задач.
Пул потоков (Thread Pool)
Для снижения накладных расходов на создание и уничтожение потоков часто используется пул потоков. При старте приложения создаётся фиксированное количество потоков, которые находятся в состоянии ожидания. При поступлении задачи (например, запроса от клиента) один из свободных потоков из пула получает задание на выполнение. После завершения работы поток возвращается в пул и ожидает следующей задачи. Это позволяет избежать частого создания потоков и ограничить общее количество одновременно работающих потоков.
Интересные факты
- В операционной системе Linux потоки реализованы через системный вызов
clone(), который позволяет создавать новый процесс, разделяющий адресное пространство с родителем. Таким образом, с точки зрения ядра, потоки являются «лёгкими процессами» (lightweight processes, LWP). - Модель памяти Java (Java Memory Model, JMM) определяет, как потоки взаимодействуют через память, и гарантирует определённые свойства видимости изменений (например, через ключевое слово
volatileили синхронизированные блоки). - В языке C++ поддержка многопоточности была стандартизирована только в C++11, когда в стандартную библиотеку были добавлены классы
std::thread,std::mutex,std::condition_variable. - Гиперпоточность (Hyper-Threading) от Intel позволяет одному физическому ядру процессора представлять себя двум логическим ядрам, что повышает эффективность использования конвейера процессора при выполнении многопоточных приложений.
- Некоторые языки программирования (например, Go, Erlang) используют модель лёгких потоков (горутины в Go, процессы в Erlang), которые управляются средой выполнения, а не операционной системой, что позволяет создавать миллионы таких потоков с минимальными накладными расходами.
Источники
- Таненбаум Э., Бос Х. «Современные операционные системы». 4-е издание. — СПб.: Питер, 2015.
- Стивенс У., Раго С. «UNIX. Профессиональное программирование». 3-е издание. — СПб.: Питер, 2014.
- Лав Р. «Linux. Системное программирование». 2-е издание. — СПб.: Питер, 2014.
- Херлихи М., Шавит Н. «Искусство многопроцессорного программирования». — М.: ДМК Пресс, 2012.
- Буч Г. «Объектно-ориентированный анализ и проектирование с примерами приложений». 3-е издание. — М.: Вильямс, 2008.
- Документация POSIX.1c (IEEE Std 1003.1c-1995).
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →