В информатике поток выполнения — это наименьшая последовательность запрограммированных инструкций, которая может управляться независимо планировщиком , который обычно является частью операционной системы . [1] Во многих случаях поток является компонентом процесса .
Несколько потоков данного процесса могут выполняться одновременно (через возможности многопоточности), разделяя ресурсы, такие как память , в то время как разные процессы не разделяют эти ресурсы. В частности, потоки процесса совместно используют его исполняемый код и значения его динамически выделяемых переменных и нелокальных для потока глобальных переменных в любой момент времени.
Реализация потоков и процессов различается в разных операционных системах. [2] [ нужна страница ]
Потоки впервые появились под названием «задачи» в операционной системе пакетной обработки IBM OS/360 в 1967 году. Она предоставляла пользователям три доступные конфигурации системы управления OS/360, одной из которых была мультипрограммирование с переменным числом задач (MVT). Зальцер (1966) приписывает Виктору А. Высоцкому термин «поток». [3]
Использование потоков в программных приложениях стало более распространенным в начале 2000-х годов, когда процессоры начали использовать несколько ядер. Приложения, желающие использовать преимущества нескольких ядер для повышения производительности, должны были использовать параллелизм для использования нескольких ядер. [4]
Планирование может осуществляться на уровне ядра или пользователя, а многозадачность может осуществляться упреждающе или кооперативно . Это приводит к множеству связанных концепций.
На уровне ядра процесс содержит один или несколько потоков ядра , которые совместно используют ресурсы процесса, такие как память и дескрипторы файлов — процесс — это единица ресурсов, в то время как поток — это единица планирования и выполнения. Планирование ядра обычно выполняется единообразно с вытеснением или, реже, совместно. На уровне пользователя процесс, такой как система выполнения, может сам планировать несколько потоков выполнения. Если они не разделяют данные, как в Erlang, их обычно по аналогии называют процессами, [5] в то время как если они разделяют данные, их обычно называют (пользовательскими) потоками , особенно если они запланированы с вытеснением. Совместно запланированные пользовательские потоки называются волокнами ; разные процессы могут планировать пользовательские потоки по-разному. Пользовательские потоки могут выполняться потоками ядра различными способами (один к одному, многие к одному, многие ко многим). Термин « легковесный процесс » по-разному относится к пользовательским потокам или к механизмам ядра для планирования пользовательских потоков в потоках ядра.
Процесс является «тяжеловесной» единицей планирования ядра, поскольку создание, уничтожение и переключение процессов относительно затратны. Процессы владеют ресурсами , выделенными операционной системой. Ресурсы включают память (как для кода, так и для данных), дескрипторы файлов , сокеты, дескрипторы устройств, окна и блок управления процессом . Процессы изолированы изоляцией процесса и не разделяют адресные пространства или файловые ресурсы, за исключением явных методов, таких как наследование дескрипторов файлов или сегментов общей памяти, или отображение одного и того же файла общим способом – см. межпроцессное взаимодействие . Создание или уничтожение процесса относительно затратно, поскольку ресурсы должны быть получены или освобождены. Процессы, как правило, являются упреждающе многозадачными, а переключение процессов относительно затратно, помимо базовой стоимости переключения контекста , из-за таких проблем, как очистка кэша (в частности, переключение процессов изменяет адресацию виртуальной памяти, вызывая недействительность и, таким образом, очистку немаркированного буфера поиска трансляции (TLB), особенно на x86).
Поток ядра — это «легковесная» единица планирования ядра. В каждом процессе существует по крайней мере один поток ядра. Если в процессе существует несколько потоков ядра, то они совместно используют одну и ту же память и файловые ресурсы. Потоки ядра являются вытесняющими многозадачными, если планировщик процессов операционной системы является вытесняющим. Потоки ядра не владеют ресурсами, за исключением стека , копии регистров , включая счетчик программ , и локального хранилища потока (если таковое имеется), и поэтому их создание и уничтожение обходятся относительно дёшево. Переключение потоков также относительно дёшево: оно требует переключения контекста (сохранение и восстановление регистров и указателя стека), но не изменяет виртуальную память и, таким образом, дружелюбно к кэшу (оставляя TLB действительным). Ядро может назначать один или несколько программных потоков каждому ядру в ЦП (оно может назначать себе несколько программных потоков в зависимости от его поддержки многопоточности) и может выгружать потоки, которые блокируются. Однако потоки ядра требуют гораздо больше времени для выгрузки, чем пользовательские потоки.
Потоки иногда реализуются в библиотеках пользовательского пространства , поэтому называются пользовательскими потоками . Ядро не знает о них, поэтому они управляются и планируются в пользовательском пространстве. Некоторые реализации основывают свои пользовательские потоки поверх нескольких потоков ядра, чтобы извлечь выгоду из многопроцессорных машин (модель M:N). Пользовательские потоки, реализованные виртуальными машинами, также называются зелеными потоками .
Поскольку реализации пользовательских потоков обычно полностью находятся в пользовательском пространстве, переключение контекста между пользовательскими потоками в пределах одного процесса чрезвычайно эффективно, поскольку оно вообще не требует взаимодействия с ядром: переключение контекста может быть выполнено путем локального сохранения регистров ЦП, используемых текущим выполняемым пользовательским потоком или волокном, а затем загрузки регистров, требуемых пользовательским потоком или волокном для выполнения. Поскольку планирование происходит в пользовательском пространстве, политику планирования можно легче адаптировать к требованиям рабочей нагрузки программы.
Однако использование блокирующих системных вызовов в пользовательских потоках (в отличие от потоков ядра) может быть проблематичным. Если пользовательский поток или волокно выполняет системный вызов, который блокируется, другие пользовательские потоки и волокна в процессе не могут работать, пока системный вызов не вернется. Типичный пример этой проблемы — выполнение ввода-вывода: большинство программ написано для синхронного выполнения ввода-вывода. Когда инициируется операция ввода-вывода, выполняется системный вызов, который не возвращается, пока операция ввода-вывода не будет завершена. В промежуточный период весь процесс «блокируется» ядром и не может работать, что лишает другие пользовательские потоки и волокна в том же процессе возможности выполняться.
Распространенным решением этой проблемы (используемым, в частности, многими реализациями зеленых потоков) является предоставление API ввода-вывода, реализующего интерфейс, который блокирует вызывающий поток, а не весь процесс, путем использования неблокирующего ввода-вывода внутри и планирования другого пользовательского потока или волокна во время выполнения операции ввода-вывода. Аналогичные решения могут быть предоставлены для других блокирующих системных вызовов. В качестве альтернативы программа может быть написана так, чтобы избегать использования синхронного ввода-вывода или других блокирующих системных вызовов (в частности, с использованием неблокирующего ввода-вывода, включая лямбда-продолжения и/или примитивы async/ await [6] ).
Волокна — это еще более легкая единица планирования, которая кооперативно планируется : работающее волокно должно явно « уступить », чтобы разрешить запуститься другому волокну, что делает их реализацию намного проще, чем потоки ядра или пользователя. Волокно может быть запланировано для запуска в любом потоке в том же процессе. Это позволяет приложениям получать улучшения производительности, управляя планированием самостоятельно, вместо того, чтобы полагаться на планировщик ядра (который может быть не настроен для приложения). Некоторые исследовательские реализации модели параллельного программирования OpenMP реализуют свои задачи через волокна. [7] [8] Тесно связаны с волокнами сопрограммы , с той разницей, что сопрограммы являются конструкцией уровня языка, в то время как волокна являются конструкцией уровня системы.
Потоки отличаются от традиционных многозадачных процессов операционной системы несколькими способами:
Говорят, что такие системы, как Windows NT и OS/2, имеют дешевые потоки и дорогие процессы; в других операционных системах разница не столь велика, за исключением стоимости переключения адресного пространства , которое на некоторых архитектурах (особенно x86 ) приводит к очистке буфера TLB ( translation lookaside buffer ).
Преимущества и недостатки потоков по сравнению с процессами включают в себя:
Операционные системы планируют потоки либо упреждающе, либо кооперативно . Многопользовательские операционные системы обычно предпочитают упреждающую многопоточность из-за ее более точного контроля над временем выполнения с помощью переключения контекста . Однако упреждающее планирование может переключать контекст потоков в моменты, непредвиденные программистами, тем самым вызывая блокировку конвоя , инверсию приоритетов или другие побочные эффекты. Напротив, кооперативная многопоточность полагается на потоки, чтобы отказаться от контроля над выполнением, тем самым гарантируя, что потоки будут выполняться до завершения . Это может вызвать проблемы, если кооперативно многозадачный поток блокируется , ожидая ресурса , или если он истощает другие потоки, не уступая контроль над выполнением во время интенсивных вычислений.
До начала 2000-х годов большинство настольных компьютеров имели только один одноядерный процессор без поддержки аппаратных потоков , хотя потоки все еще использовались на таких компьютерах, поскольку переключение между потоками, как правило, все еще было быстрее, чем переключение контекста полного процесса . В 2002 году Intel добавила поддержку одновременной многопоточности в процессор Pentium 4 под названием hyper-threading ; в 2005 году они представили двухъядерный процессор Pentium D , а AMD представила двухъядерный процессор Athlon 64 X2 .
Системы с одним процессором обычно реализуют многопоточность с помощью квантования времени : центральный процессор (ЦП) переключается между различными программными потоками . Это переключение контекста обычно происходит достаточно часто, чтобы пользователи воспринимали потоки или задачи как работающие параллельно (для популярных серверных/настольных операционных систем максимальный квант времени потока, когда другие потоки ждут, часто ограничен 100–200 мс). В многопроцессорной или многоядерной системе несколько потоков могут выполняться параллельно , при этом каждый процессор или ядро одновременно выполняет отдельный поток; на процессоре или ядре с аппаратными потоками отдельные программные потоки также могут выполняться одновременно отдельными аппаратными потоками.
Потоки, созданные пользователем в соответствии 1:1 с планируемыми сущностями в ядре [9], являются простейшей возможной реализацией потоков. OS/2 и Win32 использовали этот подход с самого начала, в то время как в Linux библиотека GNU C реализует этот подход (через NPTL или более старый LinuxThreads ). Этот подход также используется в Solaris , NetBSD , FreeBSD , macOS и iOS .
Модель M :1 подразумевает, что все потоки уровня приложения отображаются в одну запланированную сущность уровня ядра; [9] ядро не имеет сведений о потоках приложения. При таком подходе переключение контекста может быть выполнено очень быстро и, кроме того, его можно реализовать даже на простых ядрах, которые не поддерживают потоки. Однако одним из основных недостатков является то, что он не может воспользоваться аппаратным ускорением на многопоточных процессорах или многопроцессорных компьютерах: никогда не планируется более одного потока одновременно. [9] Например: если одному из потоков необходимо выполнить запрос ввода-вывода, весь процесс блокируется, и преимущество потоков не может быть использовано. GNU Portable Threads использует потоки на уровне пользователя, как и State Threads .
M : N отображает некоторое количество потоков приложения M на некоторое количество N сущностей ядра, [9] или «виртуальных процессоров». Это компромисс между потоками на уровне ядра («1:1») и на уровне пользователя (« N :1»). В целом, системы потоков « M : N » сложнее в реализации, чем потоки ядра или пользователя, поскольку требуются изменения как в коде ядра, так и в коде пользовательского пространства [ необходимо разъяснение ] . В реализации M:N библиотека потоков отвечает за планирование пользовательских потоков на доступных планируемых сущностях; это делает переключение контекста потоков очень быстрым, поскольку позволяет избежать системных вызовов. Однако это увеличивает сложность и вероятность инверсии приоритетов , а также неоптимальное планирование без обширной (и дорогостоящей) координации между планировщиком пользовательского пространства и планировщиком ядра.
SunOS 4.x реализовала легковесные процессы или LWP. NetBSD 2.x+ и DragonFly BSD реализовали LWP как потоки ядра (модель 1:1). SunOS 5.2 — SunOS 5.8, а также NetBSD 2 — NetBSD 4 реализовали двухуровневую модель, мультиплексируя один или несколько потоков уровня пользователя в каждом потоке ядра (модель M:N). SunOS 5.9 и более поздние версии, а также NetBSD 5 устранили поддержку пользовательских потоков, вернувшись к модели 1:1. [10] FreeBSD 5 реализовала модель M:N. FreeBSD 6 поддерживала как 1:1, так и M:N, пользователи могли выбирать, какой из них следует использовать с данной программой, используя /etc/libmap.conf. Начиная с FreeBSD 7, 1:1 стал моделью по умолчанию. FreeBSD 8 больше не поддерживает модель M:N.
В компьютерном программировании однопоточность означает обработку одной команды за раз. [11] В формальном анализе семантики переменных и состояния процесса термин однопоточность может использоваться по-разному, для обозначения «возврата в пределах одного потока», что является общепринятым в сообществе функционального программирования . [12]
Многопоточность в основном встречается в многозадачных операционных системах. Многопоточность — это широко распространенная модель программирования и выполнения, которая позволяет нескольким потокам существовать в контексте одного процесса. Эти потоки разделяют ресурсы процесса, но могут выполняться независимо. Поточная модель программирования предоставляет разработчикам полезную абстракцию параллельного выполнения. Многопоточность также может применяться к одному процессу для обеспечения параллельного выполнения в многопроцессорной системе.
Библиотеки многопоточности, как правило, предоставляют вызов функции для создания нового потока, который принимает функцию в качестве параметра. Затем создается параллельный поток, который начинает выполнение переданной функции и завершается, когда функция возвращает управление. Библиотеки потоков также предлагают функции синхронизации данных.
Потоки в одном процессе совместно используют одно и то же адресное пространство. Это позволяет параллельно работающему коду тесно связываться и удобно обмениваться данными без накладных расходов или сложности IPC . Однако при совместном использовании между потоками даже простые структуры данных становятся подверженными условиям гонки , если для их обновления требуется более одной инструкции ЦП: два потока могут попытаться обновить структуру данных одновременно и обнаружить, что она неожиданно меняется под ногами. Ошибки, вызванные условиями гонки, может быть очень сложно воспроизвести и изолировать.
Чтобы предотвратить это, потоковые интерфейсы прикладного программирования (API) предлагают примитивы синхронизации , такие как мьютексы, для блокировки структур данных от параллельного доступа. В однопроцессорных системах поток, работающий в заблокированном мьютексе, должен заснуть и, следовательно, вызвать переключение контекста. В многопроцессорных системах поток может вместо этого опрашивать мьютекс в спин-блокировке . Оба эти варианта могут снизить производительность и заставить процессоры в симметричных многопроцессорных системах (SMP) бороться за шину памяти, особенно если гранулярность блокировки слишком мала.
Другие API синхронизации включают переменные условия , критические секции , семафоры и мониторы .
Популярный шаблон программирования, включающий потоки, — это пулы потоков , где заданное количество потоков создается при запуске, которые затем ждут назначения задачи. Когда поступает новая задача, она просыпается, завершает ее и возвращается к ожиданию. Это позволяет избежать относительно дорогих функций создания и уничтожения потоков для каждой выполняемой задачи и выводит управление потоками из рук разработчика приложения и оставляет его библиотеке или операционной системе, которые лучше подходят для оптимизации управления потоками.
Многопоточные приложения имеют следующие преимущества по сравнению с однопоточными:
Многопоточные приложения имеют следующие недостатки:
Многие языки программирования в той или иной степени поддерживают многопоточность.
{{cite AV media}}
: CS1 maint: бот: исходный статус URL неизвестен ( ссылка )