Открыть сервис

Поток выполнения

Поток выполнения (также поток, нить, тред; от англ. 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), которые управляются средой выполнения, а не операционной системой, что позволяет создавать миллионы таких потоков с минимальными накладными расходами.

Источники

  1. Таненбаум Э., Бос Х. «Современные операционные системы». 4-е издание. — СПб.: Питер, 2015.
  2. Стивенс У., Раго С. «UNIX. Профессиональное программирование». 3-е издание. — СПб.: Питер, 2014.
  3. Лав Р. «Linux. Системное программирование». 2-е издание. — СПб.: Питер, 2014.
  4. Херлихи М., Шавит Н. «Искусство многопроцессорного программирования». — М.: ДМК Пресс, 2012.
  5. Буч Г. «Объектно-ориентированный анализ и проектирование с примерами приложений». 3-е издание. — М.: Вильямс, 2008.
  6. Документация POSIX.1c (IEEE Std 1003.1c-1995).

BFOmetr — база данных и аналитика по компаниям России.

На главную BFOmetr →