8-900-374-94-44
[email protected]
Slide Image
Меню

Операционная система thread – C Thread — это что такое?

Содержание

C Thread — это что такое?

Что общего у футболки и компьютерной программы? Они обе состоят из многих потоков! В то время как нитки в футболке держат ткань в виде единого полотна, C Thread (в буквальном смысле — «нити» или «потоки») операционной системы объединяют все программы, для того чтобы выполнить последовательные или параллельные действия одновременно. Каждый поток в программе идентифицирует процесс, который запускается, когда система (system Thread C) запрашивает его. Это оптимизирует работу такого сложного устройства как персональный компьютер и положительно сказывается на его быстродействии и производительности.

Определение

В информатике С Thread, или поток выполнения, представляет собой наименьшую последовательность команд, которые управляются независимым планировщиком, который обычно является составным элементом операционной системы.

Для потоков обычно задается определенный приоритет, что означает, что некоторые потоки имеют приоритет над другими. Как только процессор завершит обработку одного потока, он может запустить следующий, ожидающий очереди. Как правило, ожидание не превышает нескольких миллисекунд. Компьютерные программы, реализующие «многопоточность», могут выполнять сразу несколько потоков. Большинство современных ОС поддерживают С Thread на системном уровне. Это означает, что когда одна программа пытается забрать все ресурсы процессора, система принудительно переключается на другие программы и заставляет программу поддержки процессора разделять ресурсы равномерно.

Термин «поток» (С Thread) также может ссылаться на серию связанных сообщений в онлайн-обсуждении. Веб-доски объявлений состоят из множества тем или веток. Ответы, отправленные в ответ на исходную публикацию, являются частью одного и того же потока. В электронной почте поток может ссылаться на серию ответов в виде команд «назад» и «вперед», относящихся к определенному сообщению, и структурировать дерево беседы.

Многопоточность C Thread в Windows

В компьютерном программировании однопоточность — это обработка одной команды за раз. Противоположность однопоточности — многопоточность. Оба термина широко используются в сообществе функционального программирования.

Многопоточность похожа на многозадачность, но позволяет обрабатывать за раз несколько потоков, но не несколько процессов. Поскольку потоки меньше и управляются более простыми инструкциями, многопоточность может возникать также и внутри процессов.

Примеры работы инструмента C Thread task

Многопоточная операционная система может одновременно выполнять несколько фоновых задач, таких как ведение журнала изменений файлов, индексирование данных и управление окнами. Веб-браузеры, поддерживающие многопоточность, могут открывать несколько окон с одновременной работой JavaScript и Flash. Если программа полностью многопоточная, разные процессы не должны влиять друг на друга, если у процессора достаточно мощности для их обработки.

Подобно многозадачности, многопоточность также улучшает стабильность программ. Многопоточность может помешать сбою программы и не допустить сбоя в работе компьютера. Поскольку каждый поток обрабатывается отдельно, то ошибка в одном из них не в состоянии нарушить работу ПК. Таким образом, многопоточность может привести к меньшему количеству сбоев в работе операционной системы в целом.

Многозадачность

Многозадачность обрабатывает несколько задач в параллельном режиме и также характеризует принципы работы компьютера. Процессор может обрабатывать сразу несколько процессов с абсолютной точностью. Тем не менее он обрабатывает только инструкции, отправленные ему программным обеспечением. Поэтому чтобы в полной мере использовать возможности ЦП, программное обеспечение должно иметь возможность обрабатывать более одной задачи за раз и также отличаться многозадачностью.

Историческая ретроспектива

Ранние операционные системы могли запускать сразу несколько программ, но не полностью поддерживали многозадачность. Одна программа могла потреблять все ресурсы процессора при выполнении определенной операции. Основные задачи операционной системы, такие как копирование файлов, не позволяли пользователю выполнять другие задачи (открытие или закрытие окон, например).

Современные операционные системы включают в себя полную поддержку многозадачности — несколько программных решений могут работать одновременно, не мешая функциональности друг друга.

Многозадачность также повышает стабильность работы компьютера. Например, если один из процессов выйдет из строя, это не повлияет на другие запущенные программы, поскольку компьютер обрабатывает каждый процесс отдельно. Это можно сравнить с процессом написания письма: если вы находитесь в середине листа и уже написали часть текста, но ваш веб-браузер неожиданно завершает работу, то вы не потеряете уже выполненную работу.

Одиночные и многопроцессорные системы

Реализация потоковых и процессорных технологий отличается, в зависимости от операционной системы, но чаще всего поток является компонентом процесса. Одновременно несколько потоков способны существовать в едином процессе, выполняя и разделяя ресурсы. В частности, C Thread потоки процесса используют в совместном режиме исполняемый код и значения переменной в любой момент времени.

Системы с одним процессором реализуют многопоточность по времени: центральный процессор (CPU) переключается между различными потоками программного обеспечения. В многопроцессорной, а также в многоядерной системе, некоторое количество потоков выполняется в параллельном режиме, причем каждый процессор или ядро ​​выполняют отдельный поток одновременно.

Виды потоков

Планировщики процессов большинства современных ОС напрямую поддерживают как временную, так и многопроцессорную потоковую обработку, в то время как ядро ​​операционной системы позволяет разработчикам управлять потоками, предоставляя нужные функции через интерфейс системного вызова. Некоторые потоковые реализации называются потоками ядра, тогда как легкие процессы (LWP) — это тип потока, который имеет одно и то же информационное состояние. Также программные решения могут иметь потоки пространства пользователя, когда они используются с таймерами (Thread timer C), сигналами или другими методами, чтобы прервать их собственное выполнение, выполняя своего рода временную привязку ad hoc.

Потоки и процессы: отличия

Потоки отличаются от классических процессов многозадачной ОС следующими характеристиками:

  • процессы обычно независимы, тогда как потоки существуют как подмножества процесса;

  • процессы несут гораздо больше информации, чем потоки;

  • процессы имеют выделенные адресные пространства;

  • процессы взаимодействуют только через системные механизмы коммуникации;

  • переключение контекста между потоками в процессе происходит оперативнее, чем переключение контекста между процессами.

Превентивное и совместное планирование

В многопользовательских операционных системах превентивная многопоточность является более широко используемым подходом для контроля над временем выполнения посредством переключения контекста. Тем не менее превентивное планирование может привести к неконтролируемой программистами приоритезации и сбоям. Напротив, совместная многопоточность опирается на потоки, чтобы отказаться от контроля выполнения. Это может создавать проблемы, если совместный многозадачный поток блокируется путем ожидания ресурса.

Эволюция технологии

До начала 2000-х гг. на большинстве настольных компьютеров был только один одноядерный процессор, который не поддерживал аппаратные потоки. В 2002 г. компания Intel реализовала поддержку одновременной многопоточности на процессоре Pentium 4, который носит имя Hyper-Threading. В 2005 г. был представлен двухъядерный процессор Pentium D и двухъядерный процессор AMD Athlon 64 X2.

Процессоры в интегрированных системах с более высокими требованиями к действиям в режиме реального времени способны поддерживать многопоточность, уменьшая время переключения потоков, и применяют выделенный файл регистров для каждого потока.

Модели

Перечислим основные модели реализации.

1: 1 (поток на уровне ядра) — темы, созданные пользователем в ядре, являются простейшей возможной реализацией потоков. OS/2 и Win32 используют этот подход изначально, тогда как в Linux библиотеке C Thread join реализует данный подход через NPTL или более старые LinuxThreads. Этот подход также используется Solaris, NetBSD, FreeBSD, macOS и iOS.

N: 1 (пользовательский поток) — эта модель предусматривает, что все потоки уровня приложения сопоставляются с одним запланированным объектом уровня ядра. При таком подходе контекстное переключение может быть выполнено очень быстро, и, кроме того, его можно даже реализовать на тех ядрах, которые не поддерживают потоковую обработку. Однако один из основных недостатков заключается в том, что он не выигрывает от аппаратного ускорения на многопоточных процессорах или компьютерах. Например: если один из потоков необходимо выполнить при запросе ввода-вывода, весь процесс блокируется и потоковая передача не может быть использована. В GNU Portable C Thread exception используется как потоковая обработка пользовательского уровня.

M: N (гибридная реализация) — модель отображает некоторое количество потоков приложений для некоторого N числа ячеек ядра, или «виртуальных процессоров». Это компромисс между потоками уровня ядра («1: 1») и пользователя («N: 1»). Системы потоковой передачи «M: N» более сложны, поскольку требуются изменения как кода ядра, так и кода пользователя. В реализации M: N библиотека потоковой обработки отвечает за планирование потоков в доступных планируемых сущностях. Это делает контекст самым оптимальным, поскольку позволяет избежать вызовов системы. Однако это увеличивает сложность и вероятность инверсии, а также субоптимальное планирование без обширной (и дорогостоящей) координации между планировщиком пользовательской среды и планировщиком ядра.

Примеры гибридной реализации — активация планировщика, используемая встроенной реализацией библиотеки POSIX NetBSD (для модели M: N, в отличие от модели реализации ядра 1: 1, или модели пользовательского пространства).

Легкие процессы, используемые более старыми версиями операционной системы Solaris (инструментарий Std Thread C).

Поддержка языков программирования

Многие формальные системы поддерживают функциональность потоков. Реализации C и C++ реализуют эту технологию и обеспечивают доступ к собственным API-интерфейсам для операционной системы. Некоторые языки программирования более высокого уровня, такие как языки Java, Python и .NET Framework, выставляют потоки разработчикам при абстрагировании специфических различий в реализации потоков в среде выполнения. Другие языковые расширения также пытаются абстрагировать концепцию параллелизма и потоковой передачи от разработчика. Некоторые языки предназначены для последовательного параллелизма с использованием графических процессоров.

Ряд интерпретируемых языков имеет реализации, которые поддерживают потоковую и параллельную обработку, но не параллельное выполнение потоков из-за глобальной блокировки интерпретатора (GIL). GIL — это блокировка взаимного исключения, выполняемая интерпретатором, которая может помешать одновременно интерпретировать код приложения на двух или более потоках одновременно, что ограничивает параллелизм в многоядерных системах.

Другие реализации интерпретируемых языков программирования, такие как Tcl, используют расширение Thread sleep C. Это позволяет избегать максимального предела GIL, используя модель, где контент и код должны быть явным образом «разделены» между потоками.

Языки программирования приложений, ориентированные на события, такие как Verilog, и расширение Thread sleep C, имеют иную модель потоков, которая поддерживает максимальное их число для моделирования оборудования.

Практическая многопоточность

Многопоточные библиотеки инициируют вызов функции для генерации нового потока, который принимает функциональное значение в качестве параметра. Затем создается новый параллельный поток и запускается обработка запущенной функции с последующим возвратом. Языки программирования содержат библиотеки потоков, включающие функции глобальной синхронизации, которые позволяют создавать и успешно реализовать многопоточность с условием отсутствия ошибок, используя мьютексы, переменные условия, критические разделы, мониторы и другие виды синхронизации.

fb.ru

Что такое нити (threads)? / Habr

Навеяно предыдущей статьей на эту тему.
Для того чтобы, структурировать свое понимание – что представляют собой threads (это слово переводят на русский язык как «нити» почти везде, кроме книг по Win32 API, где его переводят как «потоки») и чем они отличаются от процессов, можно воспользоваться следующими двумя определениями:
  • Thread – это виртуальный процессор, имеющий свой собственный набор регистров, аналогичных регистрам настоящего центрального процессора. Один из наиважнейших регистров у виртуального процессора, как и у реального – это индивидуальный указатель на текущую инструкцию (например, индивидуальный регистр EIP на процессорах семейства x86),
  • Процесс – это в первую очередь адресное пространство. В современной архитектуре создаваемое ядром ОС посредством манипуляции страничными таблицами. И уже во вторую очередь на процесс следует смотреть как на точку привязки «ресурсов» в ОC. Если мы разбираем такой аспект, как многозадачность для того, чтобы понять суть threads, то нам не нужно в этот момент думать о «ресурсах» ОС типа файлов и к чему они привязаны.
Очень важно понять, что thread – это концептуально именно виртуальный процессор и когда мы пишем реализацию threads в ядре ОС или в user-level библиотеке, то мы решаем именно задачу «размножения» центрального процессора во многих виртуальных экземплярах, которые логически или даже физически (на SMP, SMT и multi-core CPU платформах) работают параллельно друг с другом.
На основном, концептуальном уровне, нет никакого «контекста». Контекст – это просто название той структуры данных, в которую ядро ОС или наша библиотека (реализующая threads) сохраняет регистры виртуального процессора, когда она переключается между ними, эмулируя их параллельную работу. Переключение контекстов – это способ реализации threads, а не более фундаментальное понятие, через которое нужно определять thread.
При подходе к определению понятия thread через анализ API конкретных ОС обычно вводят слишком много сущностей – тут тебе и процессы, и адресные пространства, и контексты, и переключения этих контекстов, и прерывания от таймера, и кванты времени с приоритетами, и даже «ресурсы», привязанные к процессам (в противовес threads). И все это сплетено в один клубок и зачастую мы видим, что идем по кругу, читая определения. Увы, это распространенный способ объяснять суть threads в книгах, но такой подход сильно путает начинающих программистов и привязывает их понимание к конкретике реализации.
Понятное дело, что все эти термины имеют право на существование и возникли не случайно, за каждым из них стоит какая-то важная сущность. Но среди них нужно выделить главные и второстепенные (введенные для реализации главных сущностей или навешанные на них сверху, уже на следующих уровнях абстракции).
Главная идея thread – это виртуализация регистров центрального процессора – эмуляция на одном физическом процессоре нескольких логических процессоров, каждый из которых имеет свое собственное состояние регистров (включая указатель команд) и работает параллельно с остальными.
Главное свойство процесса в контексте этого разговора – наличие у него своих собственных страничных таблиц, образующих его индивидуальное адресное пространство. Процесс не является сам по себе чем-то исполнимым.
Можно говорить в определении, что «у каждого процесса в системе всегда есть по крайней мере один thread». А можно сказать иначе –адресное пространство логически лишено смысла для пользователя, если оно не видно хотя бы одному виртуальному процессору (thread). Поэтому логично, что все современные ОС уничтожают адресное пространство (завершают процесс) при завершении работы последнего thread, работающего на данном адресном пространстве. И можно не говорить в определении процесса, что в нем есть «по крайней мере, один thread». Тем более, что на нижнем системном уровне процесс (как правило) может существовать как объект ОС даже не имея в своем составе threads.
Если Вы посмотрите исходники, например, ядра Windows, то Вы увидите, что адресное пространство и прочие структуры процесса конструируются до создания в нем начальной нити (начальной thread для этого процесса). По сути, изначально в процессе не существует threads вообще. В Windows можно даже создать thread в чужом адресном пространстве через user-level API…
Если смотреть на thread как на виртуальный процессор – то его привязка к адресному пространству представляет собой загрузку в виртуальный регистр базы станичных таблиц нужного значения. 🙂 Тем более, что на нижнем уровне именно это и происходит – каждый раз при переключении на thread, связанную с другим процессом, ядро ОС перезагружает регистр указателя на страничные таблицы (на тех процессорах, которые не поддерживают на аппаратном уровне работу со многими пространствами одновременно).

habr.com

Процессы и потоки in-depth. Обзор различных потоковых моделей / Habr

Здравствуйте дорогие читатели. В данной статье мы рассмотрим различные потоковые модели, которые реализованы в современных ОС (preemptive, cooperative threads). Также кратко рассмотрим как потоки и средства синхронизации реализованы в Win32 API и Posix Threads. Хотя на Хабре больше популярны скриптовые языки, однако основы — должны знать все 😉

Потоки, процессы, контексты…


Системный вызов (syscall). Данное понятие, вы будете встречать достаточно часто в данной статье, однако несмотря на всю мощь звучания, его определение достаточно простое 🙂 Системный вызов — это процесс вызова функции ядра, из приложение пользователя. Режим ядра — код, который выполняется в нулевом кольце защиты процессора (ring0) с максимальными привилегиями. Режим пользователя — код, исполняемый в третьем кольце защиты процессора (ring3), обладает пониженными привилегиями. Если код в ring3 будет использовать одну из запрещенных инструкций (к примеру rdmsr/wrmsr, in/out, попытку чтения регистра cr3, cr4 и т.д.), сработает аппаратное исключение и пользовательский процесс, чей код исполнял процессор в большинстве случаях будет прерван. Системный вызов осуществляет переход из режима ядра в режим пользователя с помощью вызова инструкции syscall/sysenter, int2eh в Win2k, int80h в Linux и т.д.

И так, что же такое поток? Поток (thread) — это, сущность операционной системы, процесс выполнения на процессоре набора инструкций, точнее говоря программного кода. Общее назначение потоков — параллельное выполнение на процессоре двух или более различных задач. Как можно догадаться, потоки были первым шагом на пути к многозадачным ОС. Планировщик ОС, руководствуясь приоритетом потока, распределяет кванты времени между разными потоками и ставит потоки на выполнение.

На ряду с потоком, существует также такая сущность, как процесс. Процесс (process) — не что более иное, как некая абстракция, которая инкапсулирует в себе все ресурсы процесса (открытые файлы, файлы отображенные в память…) и их дескрипторы, потоки и т.д. Каждый процесс имеет как минимум один поток. Также каждый процесс имеет свое собственное виртуальное адресное пространство и контекст выполнения, а потоки одного процесса разделяют адресное пространство процесса.

Каждый поток, как и каждый процесс, имеет свой контекст. Контекст — это структура, в которой сохраняются следующие элементы:

  • Регистры процессора.
  • Указатель на стек потока/процесса.

Также следует отметить, что в случае выполнения системного вызова потоком и перехода из режима пользователя, в режим ядра, происходит смена стека потока на стек ядра. При переключении выполнения потока одного процесса, на поток другого, ОС обновляет некоторые регистры процессора, которые ответственны за механизмы виртуальной памяти (например CR3), так как разные процессы имеют разное виртуальное адресное пространство. Здесь я специально не затрагиваю аспекты относительно режима ядра, так как подобные вещи специфичны для отдельно взятой ОС.

В общем случае, справедливы следующие рекомендации:

  • Если ваша задача требует интенсивного распараллеливания, используйте потоки одного процесса, вместо нескольких процессов. Все потому, что переключение контекста процесса происходит гораздо медленнее, чем контекста потока.
  • При использовании потока, старайтесь не злоупотреблять средствами синхронизации, которые требуют системных вызовов ядра (например мьютексы). Переключение в редим ядра — дорогостоящая операция!
  • Если вы пишете код, исполняемый в ring0 (к примеру драйвер), старайтесь обойтись без использования дополнительных потоков, так как смена контекста потока — дорогостоящая операция.

Волокно (fiber) — облегченный поток, выполняемый в режиме пользователя. Волокно затребует значительно меньше ресурсов, и позволяет в некоторых случаях минимизировать количество системных вызовов и следственно увеличить производительность. Обычно волокна выполняются в контексте потока, который их создал и затребуют лишь сохранения регистров процессора при их переключении. Какбы-то нибыло, но волокна не сыскали должной популярности. Они были реализованы в свое время в множестве BSD ОС, но со временем выбрасывались оттуда. Win32 API также реализует механизм волокон, но используется он лишь для облегчения портирования софта, написанного под другую ОС. Следует отметить, что за переключение волокон ответственен либо планировщик уровня процесса, либо переключение должно быть реализовано в самом приложении, проще говоря вручную 🙂

Классификация потоков


Поскольку классификация потоков — вопрос неоднозначный, то предлагаю их классифицировать следующим способом:
  • По отображению в ядро: 1:1, N:M, N:1
  • По многозадачной модели: вытесняющая многозадачность (preemptive multitasking), кооперативная многозадачность (cooperative multitasking).
  • По уровню реализации: режим ядра, режим польователя, гибридная реализация.

Классификация потоков по отображению в режим ядра

Как я уже упомянул, потоки могут быть созданы не только в режиме ядра, но и в режиме пользователя. Планировщиков потоков в ОС может быть несколько:

  • Центральный планировщик ОС режима ядра, который распределяет время между любым потоком в системе.
  • Планировщик библиотеки потоков. У библиотеки потоков режима пользователя может быть свой планировщик, который распределяет время между потоками различных процессов режима пользователя.
  • Планировщик потоков процесса. Уже рассмотренные нами волокна, ставятся на выполнение именно таким способом. К примеру свой Thread Manager есть у каждого процесса Mac OS X, написанного с использованием библиотеки Carbon.

Итак. Модель 1:1 — самая простая модель. Согласно ее принципам, любой поток созданный в любом процессе управляется напрямую планировщиком ядра ОС. Т.е. имеем отображении 1 к 1 потока пользовательского процесса на поток ядра. Такая модель реализована в Linux начиная с ядра 2.6, а также Windows.

Модель N:M отображает некоторое число потоков пользовательских процессов N на M потоков режима ядра. Проще говоря имеем некую гибридную систему, когда часть потоков ставится на выполнение в планировщике ОС, а большая их часть в планировщике потоков процесса или библиотеки потоков. Как пример можно привести GNU Portable Threads. Данная модель достаточно трудно реализуема, но обладает большей производительностью, так как можно избежать значительного количества системных вызовов.

Модель N:1. Как вы наверное догадались — множество потоков пользовательского процесса отображаются на один поток ядра ОС. Например волокна.

Классификация потоков по многозадачной модели


Во времена DOS, когда однозадачные ОС перестали удовлетворять потребителя, программисты и архитекторы задумали реализовать многозадачную ОС. Самое простое решение было следующим: взять общее количество потоков, определить какой-нибудь минимальный интервал выполнения одного потока, да взять и разделить между всеми -братьями- потоками время выполнения поровну. Так и появилось понятие кооперативной многозадачности (cooperative multitasking), т.е. все потоки выполняются поочередно, с равным временем выполнения. Никакой другой поток, не может вытеснить текущий выполняющийся поток. Такой очень простой и очевидный подход нашел свое применение во всех версиях Mac OS вплоть до Mac OS X, также в Windows до Windows 95, и Windows NT. До сих пор кооперативная многозадачность используется в Win32 для запуска 16 битных приложений. Также для обеспечения совместимости, cooperative multitasking используется менеджером потоков в Carbon приложениях для Mac OS X.

Однако, кооперативная многозадачность со временем показала свою несостоятельность. Росли объемы данных хранимых на винчестерах, росла также скорость передачи данных в сетях. Стало понятно, что некоторые потоки должны иметь больший приоритет, как-то потоки обслуживания прерываний устройств, обработки синхронных IO операций и т.д. В это время каждый поток и процесс в системе обзавелся таким свойством, как приоритет. Подробнее о приоритетах потоков и процессов в Win32 API вы можете прочесть в книге Джефри Рихтера, мы на этом останавливатся не будем 😉 Таким образом поток с большим приоритетом, может вытеснить поток с меньшим. Такой прицип лег в основу вытесняющей многозадачности (preemptive multitasking). Сейчас все современные ОС используют данный подход, за исключением реализации волокон в пользовательском режиме.

Классификация потоков по уровню реализации


Как мы уже успели обсудить, реализация планировщика потоков может осуществлятся на разных уровнях. Итак:
  1. Реализация потоков на уровне ядра. Проще говоря, это классическая 1:1 модель. Под эту категорию подпадают:
    • Потоки Win32.
    • Реализация Posix Threads в Linux — Native Posix Threads Library (NPTL). Дело в том, что до версии ядра 2.6 pthreads в Linux был целиком и полностью реализован в режиме пользователя (LinuxThreads). LinuxThreads реализовывалf модель 1:1 следующим образом: при создании нового потока, библиотека осуществляла системный вызов clone, и создавало новый процесс, который тем не менее разделял единое адресное пространство с родительским. Это породило множество проблем, к примеру потоки имели разные идентификаторы процесса, что противоречило некоторым аспектам стандарта Posix, которые касаются планировщика, сигналов, примитивов синхронизации. Также модель вытеснения потоков, работала во многих случаях с ошибками, по этому поддержку pthread решено было положить на плечи ядра. Сразу две разработки велись в данном направлении компаниями IBM и Red Hat. Однако, реализация IBM не снискала должной популярности, и не была включена ни в один из дистрибутивов, потому IBM приостановила дальнейшую разработку и поддержку библиотеки (NGPT). Позднее NPTL вошли в библиотеку glibc.
    • Легковесные ядерны потоки (Leight Weight Kernel Threads — LWKT), например в DragonFlyBSD. Отличие этих потоков, от других потоков режима ядра в том, что легковесные ядерные потоки могут вытеснять другие ядерные потоки. В DragonFlyBSD существует множество ядерных потоков, например поток обслуживания аппаратных прерываний, поток обслуживания программных прерываний и т.д. Все они работают с фиксированным приоритетом, так вот LWKT могут вытеснять эти потоки (preempt). Конечно это уже более специфические вещи, про которые можно говорить бесконечно, но приведу еще два примера. В Windows все потоки ядра выполняются либо в контексте потока инициировавшего системный вызов/IO операцию, либо в контексте потока системного процесса system. В Mac OS X существует еще более интересная система. В ядре есть лишь понятие task, т.е. задачи. Все операции ядра выполняются в контексте kernel_task. Обработка аппаратного прерывания, к примеру, происходит в контексте потока драйвера, который обслуживает данное прерывание.
  2. Реализация потоков в пользовательском режиме. Так как, системный вызов и смена контекста — достаточно тяжелые операции, идея реализовать поддержку потоков в режиме пользователя витает в воздухе давно. Множество попыток было сделано, однако данная методика популярности не обрела:
    • GNU Portable Threads — реализация Posix Threads в пользовательском режиме. Основное преимущество — высокая портабельность данной библиотеки, проще говоря она может быть легко перенесена на другие ОС. Проблему вытиснения потоков в данной библиотеке решили очень просто — потоки в ней не вытесняются 🙂 Ну и конечно ни о какой мультмпроцессорности речь идти не может. Данная библиотека реализует модель N:1.
    • Carbon Threads, которые я упоминал уже не раз, и RealBasic Threads.
  3. Гибридная реализация. Попытка использовать все преимущества первого и второго подхода, но как правило подобные мутанты обладают гораздо бОльшими недостатками, нежели достоинствами. Один из примеров: реализация Posix Threads в NetBSD по модели N:M, которая была посже заменена на систему 1:1. Более подробно вы можете прочесть в публикации Scheduler Activations: Effective Kernel Support for the User-Level Management of Parallelism.

Win32 API Threads


Если вы все еще не устали, предлагаю небольшой обзор API для работы с потоками и средствами синхронизации в win32 API. Если вы уже знакомы с материалом, можете смело пропускать этот раздел 😉

Потоки в Win32 создаются с помощью функции CreateThread, куда передается указатель на функцию (назовем ее функцией потока), которая будет выполнятся в созданом потоке. Поток считается завершенным, когда выполнится функция потока. Если же вы хотите гарантировать, что поток завершен, то можно воспользоватся функцией TerminateThread, однако не злоупотребляйте ею! Данная функция «убивает» поток, и отнюдь не всегда делает это корректно. Функция ExitThread будет вызвана неявно, когда завершится функция потока, или же вы можете вызвать данную функцию самостоятельно. Главная ее задача — освободить стек потока и его хендл, т.е. структуры ядра, которые обслуживают данный поток.

Поток в Win32 может пребывать в состоянии сна (suspend). Можно «усыпить поток» с помощью вызова функции SuspendThread, и «разбудить» его с помощью вызова ResumeThread, также поток можно перевести в состояние сна при создании, установив значение параметра СreateSuspended функции CreateThread. Не стоит удивлятся, если вы не увидите подобной функциональности в кроссплатформенных библиотеках, типа boost::threads и QT. Все очень просто, pthreads просто не поддерживают подобную функциональность.

Средства синхронихации в Win32 есть двух типов: реализованные на уровне пользователя, и на уровне ядра. Первые — это критические секции (critical section), к второму набору относят мьютексы (mutex), события (event) и семафоры (semaphore).

Критические секции — легковесный механизм синхронизации, который работает на уровне пользовательского процесса и не использует тяжелых системных вызовов. Он основан на механизме взаимных блокировок или спин локов (spin lock). Поток, который желает обезопасить определенные данные от race conditions вызывает функцию EnterCliticalSection/TryEnterCriticalSection. Если критическая секция свободна — поток занимает ее, если же нет — поток блокируется (т.е. не выполняется и не отъедает процессорное время) до тех пор, пока секция не будет освобождена другим потоком с помощью вызова функции LeaveCriticalSection. Данные функции — атомарные, т.е. вы можете не переживать за целостность ваших данных 😉

О мьютексах, событиях и семафорах было сказано достаточно много, по этому детально останавливатся я на них не буду. Следует заметить, что у всех этих механизмов есть общие черты:

  • Они использует примитивы ядра при выполнении, т.е. системные вызовы, что сказывается не производительности.
  • Могут быть именованными и не именованными, т.е. каждому такому объекту синхронизации можно присвоить имя.
  • Работают на уровне системы, а не на уровне процесса, т.е. могут служить механизмом межпроцессного взаимодействия (IPC).
  • Используют для ожидания и захвата примитива единую функцию: WaitForSingleObject/WaitForMultipleObjects.

Posix Threads или pthreads


Сложно представить, какая из *nix подобных операционных систем, не реализует этот стандарт. Стоит отметить, что pthreads также используется в различных операционных системах реального времени (RTOS), потому требование к этой библиотеке (вернее стандарту) — жестче. К примеру, поток pthread не может пребывать в состоянии сна. Также в pthread нет событий, но есть гораздо более мощный механизм — условных переменных (conditional variables), который с лихвой покрывает все необходимые нужды.

Поговорим об отличиях. К примеру, поток в pthreads может быть отменен (cancel), т.е. просто снят с выполнения посредством системного вызова pthread_cancel в момент ожидания освобождения какого-нибудь мьютекса или условной переменной, в момент выполнения вызова pthread_join (вызывающий поток блокируется до тех пор, пока не закончит свое выполнение поток, приминительно к которому была вызвана функция) и т.д. Для работы с мьютексами и семафорами существует отдельные вызовы, как-то pthread_mutex_lock/pthread_mutex_unlock и т.д.

Conditional variables (cv) обычно используется в паре с мьютексами в более сложных случаях. Если мьютекс просто блокирует поток, до тех пор, пока другой поток не освободит его, то cv создают условия, когда поток может заблокировать сам себя до тех пор, пока не произойдет какое-либо условия разблокировки. Например, механизм cv помогает эмулировать события в среде pthreads. Итак, системный вызов pthread_cond_wait ждет, пока поток не будет уведомлен о том, что случилось определенное событие. pthread_cond_signal уведомляет один поток из очереди, что cv сработала. pthread_cond_broadcast уведомляет все потоки, которые вызывали pthread_cond_wait, что сработала cv.

Прощальное слово


На сегодня пожалуй все, иначе информации станет слишком много. Для интересующихся, есть несколько полезных ссылок и книг внизу 😉 Также высказывайте свое мнение, интересны ли вам статьи по данной теме.

«Windows 2000 для профессионалов» — Джефри Рихтер.
GNU Portable Threads
Scheduler Activations: Effective Kernel Support for the User-Level Management of Parallelism

UPD: дополнил статью небольшой информацией о режиме ядра и режиме пользователя.
UPD2: исправил досадные промахи и ошибки. Спасибо комментаторам 😉

habr.com

Поток выполнения — Национальная библиотека им. Н. Э. Баумана

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 16:54, 24 августа 2017.

Поток выполнения — в вычислительной технике, наименьшая последовательность запрограммированных команд, которые могут независимо управляться планировщиком, являющимся частью операционной системы. Реализация потоков и процессов различается в операционных системах, но в большинстве случаев поток является составной частью процесса. Несколько потоков могут существовать в одном процессе, одновременно выполняться и совместно использовать ресурсы, такие как память, в то время как разные процессы не делят эти ресурсы. В частности, потоки процесса совместно используют свой исполняемый код и значения переменных в любой момент времени.

Отличие от процессов

Потоки отличаются от процессов операционной системы по следующим пунктам:

  • процессы обычно независимы, а потоки существуют как подобъекты процесса.
  • процессы несут значительно больше информации о состоянии, в то время как несколько потоков внутри процесса совместно используют информацию о состоянии, память и другие вычислительные ресурсы.
  • процессы имеют отдельные адресные пространства, а потоки используют его совместно.
  • процессы взаимодействуют только через предоставляемые системой механизмы межпроцессного взаимодействия.
  • контекстное переключение между потоками в одном процессе обычно быстрее, чем переключение контекста между процессами.

История

Потоки впервые появились в 1967 году в OS/360 Multiprogramming с переменным числом задач, в контексте которой они назывались «задачами». Термин «поток» был приписан Виктору Высоцкому. Планировщики процессов многих современных операционных систем напрямую поддерживают как поточную, так и многопроцессорную потоковую обработку, а ядро операционной системы позволяет программистам манипулировать потоками, предоставляя необходимые функциональные возможности через интерфейс системных вызовов. Некоторые реализации потоков называются потоками ядра, в то время как легкие процессы (англ. Light-weight processes, LWP) — это особый тип потока ядра, который имеет одно и то же состояние и информацию. Кроме того, программы могут иметь потоки пользовательского пространства при потоковой передаче с помощью таймеров, сигналов или других методов, чтобы прервать собственное выполнение.

Многопоточность

Системы с одним процессором обычно реализуют многопоточность путем временной нарезки: центральный процессор (ЦП) переключается между различными программными потоками. Такое переключение контекста обычно происходит настолько часто и быстро, что пользователи воспринимают потоки и задачи как параллельные. На многопроцессорной или многоядерной системе несколько потоков могут выполняться параллельно, причем каждый процессор или ядро выполняет отдельный поток одновременно; На процессоре или ядре с аппаратными потоками отдельные программные потоки могут также выполняться одновременно отдельными аппаратными потоками[Источник 1]

Преимущества
  • Реактивность: многопоточность может позволить приложению оставаться отзывчивым к вводу. В однопоточной программе, если основной поток выполнения блокирует длительную задачу, все приложение может зависнуть. Перемещая такие длительные задачи в рабочий поток, который выполняется одновременно с основным исполнительным потоком, приложение может оставаться отзывчивым к вводу пользователя при выполнении задач в фоновом режиме. С другой стороны, в большинстве случаев многопоточность не является единственным способом поддержания реакции программы, при этом для получения аналогичных результатов доступны неблокирующие сигналы ввода / вывода и / или Unix.
  • Более быстрое выполнение: это преимущество многопоточной программы позволяет ей работать быстрее в компьютерных системах, имеющих несколько центральных процессоров (CPU) или один или несколько многоядерных процессоров, или через кластер машин при условии достаточной независимости (отсутствия необходимости ждать друг друга).
  • Более низкое потребление ресурсов: при использовании потоков приложение может обслуживать несколько клиентов одновременно, используя меньшее количество ресурсов, чем это требовалось бы при использовании нескольких копий процессов. Например, HTTP-сервер Apache использует пулы потоков: пул потоков слушателя для прослушивания входящих запросов и пул потоков сервера для обработки этих запросов.
  • Эффективное использование системы: в качестве примера файловая система, использующая несколько потоков, может достичь более высокой пропускной способности и меньшей задержки, поскольку данные на более быстром носителе (например, в кэш-памяти) могут быть получены одним потоком, в то время как другой поток извлекает данные с более медленной среды (например, как внешнее хранилище), причем ни один поток не ожидает завершения другого.
  • Упрощенный обмен и обмен данными: в отличие от процессов, для которых требуется передача сообщений или механизм общей памяти для выполнения межпроцессного взаимодействия (англ. inter-process communication, IPC), потоки могут взаимодействовать через данные, код и файлы, которые они уже используют.
  • Распараллеливание: приложения, использующие многоядерные или многопроцессорные системы, могут использовать многопоточность для разделения данных и задач на параллельные подзадачи, и позволить базовой архитектуре управлять тем, как выполняются потоки, либо одновременно на одном ядре, либо параллельно на нескольких ядрах. Графические вычислительные среды, такие как CUDA и OpenCL, используют модель многопоточности, где от нескольких десятков до сотен потоков параллельно работают с данными, используя большое количество ядер.
Недостатки
  • Синхронизация: поскольку потоки используют одно и то же адресное пространство, программист должен быть осторожным, чтобы избежать конкуренции и других неинтуитивных действий. Для правильного управления данными потоки часто должны состыковываться во времени, чтобы обрабатывать данные в правильном порядке. Потоки могут также требовать взаимоисключающих операций (часто реализуемых с помощью семафоров), чтобы предотвратить одновременное изменение или чтение общих данных в процессе их модификации. Небрежное использование таких примитивов может привести к ошибках.
  • Поток разрушает процесс: незаконная операция, выполняемая потоком, приводит к сбою всего процесса; Поэтому один неверный поток может нарушить обработку всех других потоков в приложении.

Планирование

Операционные системы могут планировать потоки предварительно или совместно. Вытесняющая многопоточность обычно считается превосходным подходом, поскольку она позволяет операционной системе определять, когда должно произойти переключение контекста. Недостатком вытесняющей многопоточности является то, что система может осуществлять переключение контекста в неподходящее время, вызывая блокировку конвоя(англ. lock convoy), инверсию приоритета или другие негативные эффекты, которых может избежать совместная многопоточность. С другой стороны, совместная многопоточность полагается на то, что сами потоки отказываются от управления, когда они находятся в точке остановки. Это может создать проблемы, если поток ждет, когда ресурс станет доступным.

До начала 2000-х годов большинство настольных компьютеров имели только один одноядерный процессор без поддержки аппаратных потоков, хотя потоки все еще использовались на таких компьютерах, потому что переключение между потоками, как правило, было все же более быстрым, чем контекстные переключатели полного процесса. В 2002 году Intel добавила поддержку одновременной многопоточности процессора Pentium 4 под названием hyper-threading; В 2005 году они представили двухъядерный процессор Pentium D, а AMD представила двухъядерный процессор Athlon 64 X2.

Процессоры во встроенных системах, которые имеют более высокие требования к поведению в режиме реального времени, могут поддерживать многопоточность за счет уменьшения времени переключения потоков, возможно, путем выделения выделенного файла регистров для каждого потока вместо сохранения / восстановления общего файла регистров.

Процессы, потоки выполнения ядра, пользовательские потоки и файберы

Планирование может выполняться на уровне ядра или на уровне пользователя, а многозадачность может выполняться как превентивно, так и совместно. Это дает множество связанных понятий.

На уровне ядра процесс содержит один или несколько потоков ядра, которые совместно используют ресурсы процесса, такие как память и дескрипторы файлов, — процесс является единицей ресурсов, а поток — единицей планирования и выполнения. Планирование ядра, как правило, равномерно выполняется превентивно или, реже, совместно. На пользовательском уровне такой процесс, как система выполнения, сам может планировать несколько потоков исполнения. Если они не делятся данными, как в Erlang, их обычно аналогично называют процессами , а если они совместно используют данные, то их обычно называют (пользовательскими) потоками, особенно если они предварительно запланированы. Совместно запланированные пользовательские потоки известны как файберы; Различные процессы могут планировать пользовательские потоки по-разному. Пользовательские потоки могут выполняться потоками ядра различными способами (one-to-one, many-to-one, many-to-many). Термин «облегченный процесс» по-разному относится к пользовательским потокам или к механизмам ядра для планирования пользовательских потоков в потоках ядра.

Процесс является «тяжеловесным» модулем планирования ядра, поскольку процессы создания, уничтожения и переключения относительно дороги. Процессы присваивают себе ресурсы, выделенные операционной системой. Ресурсы включают память (для кода и данных), дескрипторы файлов, сокеты, дескрипторы устройств, окна и блок управления процессом. Процессы изолируются с помощью изоляции процессов и не разделяют адресные пространства или файловые ресурсы, за исключением явных методов, таких как наследование дескрипторов файлов или сегментов разделяемой памяти, или отображение одного и того же файла в общем виде.

Поток ядра — это «легкая» единица планирования ядра. По крайней мере, один поток ядра существует в каждом процессе. Если в процессе существует несколько потоков ядра, они совместно используют одни и те же ресурсы памяти и файла. Потоки ядра упреждающе многозадачны, если планировщик процесса операционной системы упреждающий. Потоки ядра не имеют собственных ресурсов, кроме стека, копии регистров, включая счетчик программ, и локального хранилища потоков (если они есть), и поэтому они относительно дешевы для создания и уничтожения. Переключение потоков также относительно дешево: требуется коммутатор контекста (сохранение и восстановление регистров и указателя стека), при этом не изменяется виртуальная память и поэтому не требуется кэширование . Ядро может назначить один поток для каждого логического ядра в системе (так как каждый процессор разбивается на несколько логических ядер, если он поддерживает многопоточность или поддерживает только одно логическое ядро ​​на каждое физическое ядро, если это не так), и может обменивать заблокированные потоки. Тем не менее, потоки ядра занимают гораздо больше времени для обмена, чем пользовательские потоки.

Потоки иногда реализуются в пользовательских библиотеках, которые называются пользовательскими потоками. Ядро не знает о них, поэтому они управляются и планируются в пользовательском пространстве. Некоторые реализации основывают свои пользовательские потоки на нескольких потоках ядра, чтобы использовать многопроцессорные машины (модель M: N). В этой статье термин «поток» по умолчанию ссылается на потоки ядра. Пользовательские потоки, реализованные виртуальными машинами, также называются «зелеными потоками». Пользовательские потоки обычно быстро создаются и управляются, но не могут использовать многопоточность или многопроцессорность, и будут заблокированы, если все связанные с ними потоки ядра будут заблокированы, даже если есть некоторые пользовательские потоки, которые готовы к запуску.

Файбер представляют собой еще более легкую единицу планирования^ выполняющийся файбер должен явно «уступить» право другим файберам на выполнение, что делает их реализацию гораздо легче, чем реализацию потоков выполнения ядра или пользовательских потоков выполнения. Файберы могут быть запланированы для запуска в любом потоке выполнения внутри того же процесса. Это позволяет приложениям получить повышение производительности за счет управления планированием самого себя, вместо того чтобы полагаться на планировщик ядра (который может быть не настроен на такое применение). Параллельные среды программирования, такие как OpenMP, обычно реализуют свои задачи посредством файберов.

Проблемы потоков и файберов
Параллелизм и структуры данных

Потоки в одном процессе имеют одинаковое адресное пространство. Это позволяет одновременно запускать код для надежного и удобного обмена данными без дополнительных затрат или сложности IPC. Однако при совместном использовании потоков ни одна простая структура данных не подвержена гонке, если для их обновления требуется более одной инструкции ЦП: два потока могут в конечном итоге попытаться обновить структуру данных в одно и то же время и обнаружить, что она неожиданно меняется. Ошибки, вызванные условиями гонки, может быть очень трудно воспроизвести и изолировать. Чтобы этого не происходило, потоковые интерфейсы прикладного программирования (API) предлагают примитивы синхронизации, такие как мьютексы, для блокировки структур данных с одновременным доступом. На однопроцессорных системах поток, запущенный в заблокированный мьютекс, должен спать и, следовательно, запускать контекстный переключатель. В многопроцессорных системах поток может вместо этого опросить мьютекс в спин-блокировке. Оба они могут нарушить производительность и заставить процессоры в симметричных многопроцессорных системах (SMP) бороться за шину памяти, особенно если гранулярность блокировки (уровень модульности) в порядке.

Ввод-вывод и планирование

Пользовательские потоки или файберы обычно находятся в пользовательском пространстве. В результате переключение контекста между пользовательскими потоками или волокнами в рамках одного процесса является чрезвычайно эффективным, поскольку он не требует какого-либо взаимодействия с ядром вообще: контекстный переключатель может быть выполнен путем локального сохранения регистров процессора, используемых текущим исполняемым пользователем потоком или файером, а затем загружает регистры, необходимые для потока или файбера. Поскольку планирование выполняется в пользовательском пространстве, политика планирования может быть легче адаптирована к требованиям работы программы. В то же время использование блокировок системных вызовов для пользовательских потоков и файберов имеет свои проблемы. Если пользовательский поток или файбер исполняет системный вызов, другие потоки файберы процесса не могут работать до его завершения. Типичный пример такой проблемы связан с выполнением операций ввода-вывода. Большинство программ рассчитаны на синхронное выполнение ввода-вывода. При инициации ввода-вывода делается системный вызов, и он не возвращается до его завершения. В промежутке весь процесс блокируется ядром и не может исполняться, лишая возможности работы другие пользовательские потоки и файберы этого процесса. Популярным решением этой проблемы является использование отдельного API для ввода-вывода, который реализует синхронный интерфейс с использованием внутреннего неблокирующего ввода-вывода, и запуск другого пользовательского потока выполнения или файбера на время обработки ввода-вывода.

Модели

1:1 (на уровне ядра)

Потоки в данной модели соответствуют диспетчируемым сущностям ядра, что является простейшим способом реализации потоковости. В Windows API этот подход использовался с самого начала. В Linux обычная библиотека C реализует этот подход (через библиотеку потоков POSIX, а в более старших версиях через LinuxThreads). Аналогичный подход используется NetBSD, ОС Solaris, FreeBSD.

N:1 (на уровне пользователя)

Данная модель предполагает, что все потоки выполнения уровня пользователя отображаются на единую планируемую сущность уровня ядра, и ядро ничего не знает о составе прикладных потоков выполнения. При таком подходе переключение контекста может быть сделано очень быстро, и, кроме того, он может быть реализован даже на простых ядрах, которые не поддерживают многопоточность. Однако при таком подходе нельзя извлечь никакой выгоды из аппаратного ускорения на многопоточных процессорах или многопроцессорных компьютерах, потому что только один поток выполнения может быть запланирован на любой момент времени. Эта модель используется в GNU Portable Threads, Netscape Portable Runtime, FSU Pthreads.

M:N (смешанная потоковость)

В модели M:N некоторое число M прикладных потоков выполнения отображаются на некоторое число N сущностей ядра или «виртуальных процессоров». Модель является компромиссной между моделью уровня ядра («1:1») и моделью уровня пользователя («N:1»). Реализована, например в Microsoft Windows 7 или The Glasgow Haskell Compiler (GHC).

Реализация файберов

Файберы могут быть реализованы без поддержки операционной системы, хотя некоторые операционные системы и библиотеки предоставляют явную поддержку для них, например Библиотека Win32 содержит API для файберов, Ruby как реализация «зелёных потоков», Netscape Portable Runtime и т.д.

Поддержка языков программирования

IBM PL / I (F) включила поддержку многопоточности (так называемой многозадачности) в конце 1960-х годов, и это было продолжено в Оптимизационном компиляторе и более поздних версиях. Компилятор IBM Enterprise PL / I представил новую модель API-потоков. Ни одна из версий не была частью стандарта PL / I.

Многие языки программирования поддерживают потоки в некоторой степени. Многие реализации C и C ++ поддерживают потоковую обработку и предоставляют доступ к собственным API-интерфейсам потоков в операционной системе. Некоторые более высокоуровневые (и, как правило, кросс-платформенные) языки программирования, такие как языки Java[Источник 2], предоставляют разработчикам потоки, абстрагируя специфические для платформы различия в реализациях потоков во время выполнения. Некоторые другие языки программирования и языковые расширения также пытаются полностью отделить концепцию параллелизма и потокования от разработчика (Cilk, OpenMP, Message Passing Interface (MPI)). Некоторые языки предназначены для последовательного параллелизма (особенно с использованием графических процессоров), не требуя параллелизма или потоков (Ateji PX, CUDA).

Некоторые интерпретируемые языки программирования имеют реализации (например, Ruby MRI для Ruby, CPython для Python), которые поддерживают потоковую обработку и параллелизм, но не параллельное выполнение потоков из-за глобальной блокировки интерпретатора (GIL). GIL — блокировка взаимного исключения, удерживаемая интерпретатором, которая может помешать интерпретатору одновременно интерпретировать код приложения на двух или более потоках одновременно, что фактически ограничивает параллелизм в нескольких основных системах.

Другие реализации интерпретируемых языков программирования, такие как Tcl, использующие расширение Thread, избегают предела GIL, используя «модель апартаментов», где данные и код должны быть явно «разделяемы» между потоками. В Tcl каждый поток имеет один или несколько интерпретаторов.

Языки описания аппаратного описания событий, такие как Verilog, имеют другую модель потоков, которая поддерживает очень большое их количество (для моделирования оборудования).

Практическая многопоточность

Стандартизованным интерфейсом для реализации потоков является POSIX Threads (Pthreads), который представляет собой набор вызовов библиотеки C-функций[Источник 3]. Поставщики ОС могут свободно внедрять интерфейс по желанию, но разработчик приложения должен иметь возможность использовать один и тот же интерфейс на нескольких платформах. Большинство платформ Unix, включая Linux, поддерживают Pthreads. Microsoft Windows имеет свой собственный набор функций потока в интерфейсе process.h для многопоточности. Java предоставляет еще один стандартизованный интерфейс для операционной системы хоста, используя библиотеку java.util.concurrent для Java.[Источник 4]


Многопоточные библиотеки предоставляют вызов функции для создания нового потока, который принимает функцию в качестве параметра. Затем создается параллельный поток, который запускает функцию и завершается, когда функция возвращается. Библиотеки потоков также предлагают функции синхронизации, которые позволяют реализовать функции многопоточности без условий гонки с использованием взаимных исключений, условных переменных, критических секций, семафоров, мониторов и других примитивов синхронизации.

Еще одна парадигма использования потоков — это пулы потоков, где заданное число потоков создается при запуске, а затем ожидает назначения задачи. Когда приходит новая задача, пул просыпается, выполянет задачу и возвращается к ожиданию после ее завершения. Это позволяет избежать относительно дорогих функций создания и уничтожения потоков для каждой выполняемой задачи и выводит управление потоками из руки разработчика приложения и оставляет его в библиотеке или операционной системе, которая лучше подходит для оптимизации управления потоками. Используется в таких структурах как Grand Central Dispatch и Threading Building Blocks.

В моделях программирования, таких как CUDA, предназначенных для параллельных вычислений данных, массив потоков запускает один и тот же код параллельно, используя только его идентификатор, чтобы найти свои данные в памяти. По существу, приложение должно быть спроектировано таким образом, чтобы каждый поток выполнял одну и ту же операцию в разных сегментах памяти, чтобы они могли работать параллельно и использовать архитектуру графического процессора.

Источники

ru.bmstu.wiki

ПОТОКИ (нити)

Потоки  в операционных системах нужны для двух вещей – для параллелизма и одновременности.

Параллелизм – это физически одновременное выполнение для достижения наибольшей производительности(например, между двумя ядрами).

Одновременность – логическое и/или физическое одновременное выполнение (есть один ЦП, на нем одновременно выполняется несколько программ – многозадачная ОС).

Потоки нужны для того и другого понятия для эффективного использования. В самом простом варианте, чтобы достичь параллелизма – использование множества процессов – программы изолированы друг от друга в разных процессах, поэтому параллелизм есть.

Потоки – другой способ достичь параллелизма. Потоки работают внутри одного процесса. Все потоки процесса имеют одно адресное пространство и те же ресурсы ОС. У потоков есть свой стек и свое состояние ЦП.

Параллелизм (примеры)

Примеры:

Веб-сервер, который для каждого пользовательского процесса создает новый процесс, т.е. должен обслуживать несколько запросов параллельно.

Ожидая данных по запросу клиента из БД сервер в это же время мог бы загрузить данные с диска для другого клиента и обработать запрос третьего клиента.

Веб-браузер – в момент обращения к веб- странице, он мог бы параллельно загружать данные из различных источников.

Некоторая вычислительная программа использующая физический параллелизм – например, когда нужно обработать большой массив данных.

В каждом из этих примеров параллелизма есть общее:

  • Один код
  • Доступ к одним данным
  • Один уровень доступа
  • Одно множество ресурсов.

разное:

  • Стэк и указатель стэка (регистр SP)
  • Счетчик инструкций (регистр IP), указывающий на следующую инструкцию
  • Множество регистров ЦП

Процессы и потоки

Большинство современных ОС поддерживает два объекта:

  1. Процесс, который определяет адресное пространство и общие атрибуты процесса.
  2. Поток, который определяет последовательный поток выполнения в рамках процесса.

Поток привязывается к одному процессу и одному адресному пространству.

  • Но может быть много потоков в одном адресном пространстве
  • Обеспечивают легкий доступ к общим данным
  • Создание потоков занимает очень мало времени

ПОТОКИ стали единицей планирования

Процессывсего лишь контейнер, в котором выполняются потоки.

Процесс – это непосредственно контейнер, а поток – это нити выполнения, которые у него есть внутри.

Многопоточность

Многопоточность полезна для :

  •  обработки одновременных событий
  •  построение параллельных программ.

Поддержка многопоточности – разделение понятие процесса от минимального потока управления.

  • Для параллельного потока выполнения не нужно создавать новые процессы.
  • Работает быстрее, меньше требования к памяти.

Раньше: «Процесс»= адресное пространство + ресурсы ОС+ подразумевался единственный поток

Раньше: «Процесс»= адресное пространство + ресурсы ОС+ все потоки принадлежат процессу

Какие бывают потоки?

Виды потоков 

Потоки делают двумя способами:

1)На уровне ядра —  есть функции ядра для создания нового потока.
— выделяется стек выполнения на внутри адресного пространства процесса;
— создает и инициализирует Thread Control Block (блок упр.процессом).

Потоки ядра

2)На уровне пользователя

потоки уровня пользователя

На рисунке три потока. Есть специальная библиотека Pthreads, которая управляет потоками вне ядра самостоятельно.

В обеих вариантах есть плюсы и минусы

Первый вариант минус —  нужен системный вызов при создании потока, а это время процессора. На уровне пользователя системного вызова не нужно, все управляется специальной библиотекой.

Библиотека Pthreads (положительное):

Каждый поток представляется регистрами ЦП, стеком и небольшим блоком TCB

Создание потока, переключение между потоками и синхронизация потоков выполняется без участия ядра

Потоки уровня пользователя могут быть в 10-100 раз быстрее, чем потоки режима ядра

  • Множество потоков в одном адресном пространстве это хорошо;
  • Потоки режима ядра намного эффективнее процессов, но есть потери на системные вызовы;
  • Потоки режима пользователя имеют преимущества и недостатки:
    — высокая скорость и «дешевизна» создания;
    — могут быть проблемы с вводом/выводом и блокировками, из-за того, что ядро не знает об этих потоках.

Поэтому пользовательские потоки не обрели большой популярности.

Возможно решить эти проблемы на уровне планировщика.

В ОС Windows NT потоки реализованы на уровне ядра, что позволяет более тонко регулировать события.

Ввели fiber – вытесняющую многозадачность.

Отличие потоков от процессов

Потоки выполнения отличаются от традиционных процессов  многозадачной операционной системы тем, что:

  • процессы, как правило, независимы, тогда как потоки выполнения существуют как составные элементы процессов;
  • процессы несут значительно больше информации о состоянии, тогда как несколько потоков выполнения внутри процесса совместно используют информацию о состоянии, а также память и другие вычислительные ресурсы;
  • процессы имеют отдельные адресные пространства, тогда как потоки выполнения совместно используют их адресное пространство;
  • процессы взаимодействуют только через предоставляемые системой механизмы связей между процессами;
  • переключение контекста между потоками выполнения в одном процессе, как правило, быстрее, чем переключение контекста между процессами.

Такие системы, как Windows NT и OS/2, как говорят, имеют «дешёвые» потоки выполнения и «дорогие» процессы. В других операционных системах разница между потоками выполнения и процессами не так велика, за исключением расходов на переключение адресного пространства.

Следующая важная тема в теории операционных систем «Синхронизация».

 

Скачать презентацию к лекции «Процессы и потоки»

 

Скачать тест по теме «Процессы и потоки»

 

 

Понравилась статья, рекомендуйте Вашим друзьям!
Давайте дружить!

komputercnulja.ru

OpenOCD, ThreadX и ваш процессор / Habr

Данная заметка может оказаться полезной для людей, который пишут bare-metal код и используют ThreadX в своих задачах (по собственному выбору или по навязыванию SDK). Проблема в том, что что бы эффективно отлаживать код под ThreadX или другую многопоточную операционную систему нужно иметь возможность видеть эти самые потоки, иметь возможность посмотреть стек-трейс, состояние регистров для каждого потока.

OpenOCD (Open On Chip Debugger) заявляет поддержку ThreadX, но не сильно явно оговаривает её широту. А штатно, на момент написания статьи, в версии 0.8.0, это всего два ядра: Cortex M3 и Cortex R4. Мне же, волею судеб, пришлось работать с чипом Cypress FX3 который построен на базе ядра ARM926E-JS.

Под катом рассмотрим что нужно сделать, что бы добавить поддержку вашей версии ThreadX для вашего CPU. Акцент делается на ARM, но, чисто теоретически, вполне может подойти и для других процессоров. Кроме того, рассматривается случай, когда доступа к исходникам ThreadX нет и не предвидится.

С первых строк сразу же огорчу: без ассемблера никуда. Нет, писать на нём нам не придётся, но читать код — да.

Начнём со знакомства с реализацией поддержки ThreadX в OpenOCD. Это всего один файл: src/rtos/ThreadX.c.

Поддерживаемая система описывается структурой ThreadX_params, которая содержит информацию о имени таргета, «ширины» указателя в байтах, набор смещений в структуре TX_THREAD до необходимых служебных полей, а так же информацию о том, как сохраняется контекст потока при переключении (т.н. stacking info). Сами поддерживаемые системы регистрируются при помощи массива ThreadX_params_list.

Со всеми параметрами, кроме последнего, проблем нет: ширина указателя обычно равна разрядности процессора, смещения считаются ручками (и то, почти всегда они неизменны).

Интересный вопрос: откуда брать информацию по стекингу? А ведь информации там немало:

  • направление роста стека (ну это просто)
  • число регистров в системе (это тоже просто, запускаем на имеющейся версии OpenOCD «info registers» и считаем число строк).
  • выравнивание фрейма на стеке, это значение я получил случайно, для Cortex M3/R4 оно указано 8 байт, для ARM926E-JS — 0 (т.е. без выравнивания). На самом деле выравнивание по 4, но память выделенная при помощи tx_byte_alloc() уже выровнена, а использование стека всегда кратно 4. В общем, попробуйте значения 0, 4 и так далее.
  • массив смещений в стеке (относительно текущей вершины) по которым лежат значения конкретных регистров (размер массива равен числу регистров в выводе «info registers»).

Вот последнее и является самым сложным и непонятным. Могу сразу убедить — стандартного подхода тут нет. Методом тыка эти значения подобрать крайне сложно, а то и невозможно.

Более того, забегая вперёд, как оказалось для ядер Cortex M3/R4 используется одна схема стекинга, а для ARM926E-JS — две! Всё ради экономии.

Кратко (а так же очень грубо и неточно), как работает шедулер в ThreadX: он одновременно обеспечивает кооперативный и вытесняющий подход к организации многозадачности.

Кооперативный подход работает для потоков одинакового приоритета которым не задан слайс времени (0). Т.е. если поток А и Б имеют одинаковый приоритет, поток А начал работу, то поток Б не получит управление пока А:

  • не завершится
  • не вызовет функции, которая приводит к решедулингу (sleep, ожидание на очереди, мутексе, семафоре и т.д.)

Если слайс времени задан, то по его завершению поток будет прерван и управление передастся другому следующему в состоянии Ready (для случая, когда поток засыпает, но не выработал свой слайс, так же сработает кооперативный подход). Здесь уже работает вытесняющий подход. Для его работы нужен таймер и прерывания от него с определённой периодичностью. Так же поток А из примера выше, может быть вытеснен потоком В, если его приоритет выше.

Понятно, что контекст потока сохраняется когда он передаёт управление кому-то и восстанавливается, когда он получает управление. Поймём как это происходит — поймём что нужно описывать в массиве смещений регистров.

Не буду вдаваться в подробности, как я выяснял где и как спрятались основные части планировщика, много тут понамешалось: и смекалка, и удача, и гугл, и дизассемблер. Но приведу основные компоненты оного:

  1. _tx_timer_interrupt() — функция вызывается из контекста прерывания таймера, по сути отвечает за вытесняющую часть планировщика.
  2. _tx_thread_context_save() (или _tx_thread_vectored_context_save()) и _tx_thread_context_restore() — пара функций предназначенных для вызова из прерываний для сохранения и восстановления контекста. При восстановлении контекста производится попытка решедулинга.
  3. _tx_thread_system_return() — часть кооперативного подхода. Вызывается в конце любой цепочки вызовов, которая приводит к решедулингу.
  4. и, наконец, _tx_thread_schedule() — самая важная функция для анализа и, пожалуй самая простая из всех вышеперечисленных.

Я изучал листинги всех этих функций, но если снова потребуется прикручивать поддержку для неподдерживаемого процессора, я буду акцентировать на последних трёх. Но начну с последней, и только после этого (если не хватит информации) буду изучать другие.

Посмотрим на её листинг (некоторую косвенную адресацию я заменил на реальные символы, сами символы
смотрятся в elf-файле при помощи arm-none-eabi-nm):

40004c7c <_tx_thread_schedule>:
40004c7c:       e10f2000        mrs     r2, CPSR
40004c80:       e3c20080        bic     r0, r2, #128    ; 0x80
40004c84:       e12ff000        msr     CPSR_fsxc, r0
40004c88:       e59f104c        ldr     r1, [pc, #76]   ; 40004cdc <_tx_thread_schedule+0x60>
40004c8c:       e5910000        ldr     r0, [r1]
40004c90:       e3500000        cmp     r0, #0
40004c94:       0afffffc        beq     40004c8c <_tx_thread_schedule+0x10>
40004c98:       e12ff002        msr     CPSR_fsxc, r2
40004c9c:       e59f103c        ldr     r1, [pc, #60]   ; 40004ce0 <_tx_thread_schedule+0x64>
40004ca0:       e5810000        str     r0, [r1]
40004ca4:       e5902004        ldr     r2, [r0, #4]
40004ca8:       e5903018        ldr     r3, [r0, #24]
40004cac:       e2822001        add     r2, r2, #1
40004cb0:       e5802004        str     r2, [r0, #4]
40004cb4:       e59f2028        ldr     r2, [pc, #40]   ; 40004ce4 <_tx_thread_schedule+0x68>
40004cb8:       e590d008        ldr     sp, [r0, #8]
40004cbc:       e5823000        str     r3, [r2]
40004cc0:       e8bd0003        pop     {r0, r1}
40004cc4:       e3500000        cmp     r0, #0
40004cc8:       116ff001        msrne   SPSR_fsxc, r1
40004ccc:       18fddfff        ldmne   sp!, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}^
40004cd0:       e8bd4ff0        pop     {r4, r5, r6, r7, r8, r9, sl, fp, lr}
40004cd4:       e12ff001        msr     CPSR_fsxc, r1
40004cd8:       e12fff1e        bx      lr
40004cdc:       4004b754        .word   0x4004b754 ; _tx_thread_execute_ptr
40004ce0:       4004b750        .word   0x4004b750 ; _tx_thread_current_ptr
40004ce4:       4004b778        .word   0x4004b778 ; _tx_timer_time_slice

Функция до безумства простая:
  1. разрешить прерывания (строки 40004c7c-40004c84)
  2. дождаться, что кто-то взведёт _tx_thread_execute_ptr (40004c88-40004c94) — следующий для исполнения тред
  3. запретить прерывания, а точнее — восстановить статусный регистр (40004c98)
  4. сохранить указатель _tx_thread_current_ptr в r0 (40004c9c-40004ca0)
  5. увеличить значение tx_thread_run_count текущего треда на 1 (40004ca4, 40004cac-40004cb0)
  6. получить значение tx_thread_time_slice текущего треда и присвоить его _tx_timer_time_slice (40004ca8, 40004cb4, 40004cbc)
  7. установить новый указатель на стек, сохранённый в структуре потока (прочитать tx_thread_stack_ptr) (40004cb8)

А вот начиная с 40004cb8 идёт код который, собственно и восстанавливает контекст нового потока.

Сначала вычитываются два значения в регистры r0, r1:

40004cc0:       e8bd0003        pop     {r0, r1}

Далее идёт сравнение r0 с нулём:
40004cc4:       e3500000        cmp     r0, #0

Очевидно, что эти значения, по крайней мере r0, часть контекста (ведь стековый регистр уже настроен на стек восстанавливаемого треда), но не совсем похоже, что это регистры. А сравнение с нулём подразумевает какое-то ветвление. Продолжая анализ, видим, что если r0 != 0, то выполняется код:
40004cc8:       116ff001        msrne   SPSR_fsxc, r1
40004ccc:       18fddfff        ldmne   sp!, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}^

Собственно говоря это и похоже на восстановление контекста. Причём значение регистра r1 — это сохранённое значение статусного регистра CPSR. Если строчка 40004ccc выполнится, то управление дальше не пойдёт: восстановится регистр pc (r15) и программа после этой точки вернётся в то место, откуда она была прервана.

Отлично, теперь мы можем написать такую табличку:

  Смещение     Регистр
  --------     -------
  0            флаг
  4            CPSR
  8            r0
  12           r1
  16           r2
  20           r3
  24           r4
  28           r5
  32           r6
  36           r7
  40           r8
  44           r9
  48           sl (r10)
  52           fp (r11)
  56           ip (r12)
  60           lr (r14)
  64           pc (r15)

Каждый регистр и каждый флаг — 32 бит или 4 байта, соответственно на этот контекст нужно 17*4 = 68 байт. Логично, что дальше идёт стек, каким он был на момент прерывания.

Но, как видим, это часть работы. У нас есть этот самый флаг. И если его значение 0, то выполняется код:

40004cd0:       e8bd4ff0        pop     {r4, r5, r6, r7, r8, r9, sl, fp, lr}
40004cd4:       e12ff001        msr     CPSR_fsxc, r1
40004cd8:       e12fff1e        bx      lr

Судя по всему, это тоже контекст, только несколько сокращённый. Более того, возврат из него происходит как из обычной функции, а не восстановлением регистра pc. Переписав табличку выше, получаем:
  Смещение     Регистр
  --------     -------
  0            флаг
  4            CPSR
  8            r4
  12           r5
  16           r6
  20           r7
  24           r8
  28           r9
  32           sl (r10)
  36           fp (r11)
  40           lr (r14)

Для этого контекста нужно всего 11*4 = 44 байта.

Пользуясь гуглом, просмотром листингов дизассемблера, а так же изучением соглашений по вызову процедур приходим к пониманию, что этот тип контекста используется когда работает кооперативная многозадачность: т.е. когда мы вызвали tx_thread_sleep() или иже с ними. А т.к. такое переключение, по сути, просто вызов функции, то и контекст можно сохранять согласно соглашениям о вызовах, по которому, мы имеем право между вызовами не сохранять значения регистров r0-r3, r12. Более того, нам не нужно сохранять pc — вся необходимая информация уже содержится в rl — адресе возврата из tx_thread_sleep(). Выгода на лицо. Кортексы обычно используются на системах с большим количеством памяти, нежели ARM9E, там к подобных ухищрениям не прибегают и используют один тип стекинга.

По информации из интернета накопал, что первый тип контекста называется interrupt, и используется когда поток прерывается прерыванием, сиречь может быть прерван в любом месте, поэтому необходимо сохранять все возможные регистры. Второй тип контекста называется solicited и используется когда тред прерывается по системному вызову, который приводит к решедулингу.

Вот собственно всё и готово, что бы понять какие переделки нужны в OpenOCD:

  • нужно доработать механизм регистрации таргетов, что бы была возможность использовать несколько вариантов стекинга для одного таргета;
  • собственно составить описание таргета.

Код для первого пункта я приводит не буду, посмотрите патч. Для пункта два немного поясню как составлять табличку смещений понятных OpenOCD.

Первым делом смотрим вывод команды ‘info registers’, смотрим сколько регистров и в каком порядке выводится, составляем такую рыбу:

static const struct stack_register_offset rtos_threadx_arm926ejs_stack_offsets_solicited[] = {
        { , 32 },               /* r0        */
        { , 32 },               /* r1        */
        { , 32 },               /* r2        */q
        { , 32 },               /* r3        */
        { , 32 },               /* r4        */
        { , 32 },               /* r5        */
        { , 32 },               /* r6        */
        { , 32 },               /* r7        */
        { , 32 },               /* r8        */
        { , 32 },               /* r9        */
        { , 32 },               /* r10       */
        { , 32 },               /* r11       */
        { , 32 },               /* r12       */
        { , 32 },               /* sp (r13)  */
        { , 32 },               /* lr (r14)  */
        { , 32 },               /* pc (r15)  */
        { , 32 },               /* xPSR      */
};

Здесь 32 — битность регистра. Для ARM всегда 32. Первая колонка заполняется при помощи табличек, которые мы записали выше, когда анализировали восстановление контекста. Учитываем специальные значения: -1 — данный регистр не сохраняется, -2 — стековый регистр, восстанавливается из структуры потока.

Заполненная рыба для solicited контекста получается такой:

static const struct stack_register_offset rtos_threadx_arm926ejs_stack_offsets_solicited[] = {
        { -1, 32 },             /* r0        */
        { -1, 32 },             /* r1        */
        { -1, 32 },             /* r2        */
        { -1, 32 },             /* r3        */
        {  8, 32 },             /* r4        */
        { 12, 32 },             /* r5        */
        { 16, 32 },             /* r6        */
        { 20, 32 },             /* r7        */
        { 24, 32 },             /* r8        */
        { 28, 32 },             /* r9        */
        { 32, 32 },             /* r10       */
        { 36, 32 },             /* r11       */
        { -1, 32 },             /* r12       */
        { -2, 32 },             /* sp (r13)  */
        { 40, 32 },             /* lr (r14)  */
        { -1, 32 },             /* pc (r15)  */
        {  4, 32 },             /* xPSR      */
};

Для interrupt контекста попробуйте написать сами или посмотрите в исходники.

Что это даст:

  • вывод списка потоков по «info threads»
  • стектрейс индивидуально для потока: «thread apply all bt»
  • переключением между потоками: «thread 3»
  • переключением между фреймами: «frame 5»
  • индивидуальный просмотр состояния регистров каждого потока

команды даны для gdb.

В общем, счастливой отладки!

Ресурсы:

PS не хватает хаба «Обратная разработка» и подсветки для разных ассемблеров 😉

UPD /2015-08-15/: Изменения попали в основную ветку OpenOCD: openocd.zylin.com/#/c/2848

habr.com

Лекция 4 Процессы и потоки

Когда в системе одновременно выполняется несколько независимых задач, то возникают дополнительные проблемы. Хотя потоки возникают и выполняются асинхронно, у них может возникнуть необходимость во взаимодействии, например при обмене данными. Согласование скоростей потоков также очень важно для предотвращения эффекта «гонок» (когда несколько потоков пытаются изменить один и тот же файл), взаимных блокировок или других коллизий, которые возникают при совместном использовании ресурсов. Синхронизация потоков является одной из важных функций подсистемы управления процессами и потоками.

Каждый раз, когда процесс завершается, ОС предпринимает шаги, чтобы «зачистить следы» его пребывания в системе. Подсистема управления процессами закрывает все файлы, с которыми работал процесс, освобождает области оперативной памяти, отведенные под коды, данные и системные информационные структуры процесса. Выполняется коррекция всевозможных очередей ОС и списков ресурсов, в которых имелись ссылки на завершаемый процесс.

Понятия «процесс» и «поток»

Чтобы поддерживать мультипрограммирование, ОС должна определить и оформить для себя те внутренние единицы работы, между которыми будет разделяться процессор и другие ресурсы компьютера. В настоящее время в большинстве операционных систем определены два типа единиц работы. Более крупная единица работы, обычно носящая название процесса, или задачи, требует для своего выполнения нескольких более мелких работ, для обозначения которых используют термины «поток», или «нить».

ПРИМЕЧАНИЕ 

 При использовании этих терминов часто возникают сложности. Это происходит в силу нескольких причин. Во-первых, — специфика различных ОС, когда совпадающие по сути понятия получили разные названия, например задача (task) в OS/2, OS/360 и процесс (process) в UNIX, Windows NT, NetWare. Во-вторых, по мере развития системного программирования и методов организации вычислений некоторые из этих терминов получили новое смысловое значение, особенно это касается понятия «процесс», который уступил многие свои свойства новому понятию «поток». В-третьих, терминологические сложности порождаются наличием нескольких вариантов перевода англоязычных терминов на русский язык. Например, термин «thread» переводится как «нить», «поток», «облегченный процесс», «минизадача» и др. Далее в качестве названия единиц работы ОС будут использоваться термины «процесс» и «поток». В тех же случаях, когда различия между этими понятиями не будут играть существенной роли, они объединяются под обобщенным термином «задача».

Итак, в чем же состоят принципиальные отличия в понятиях «процесс» и «поток»?

Очевидно, что любая работа вычислительной системы заключается в выполнении некоторой программы. Поэтому и с процессом, и с потоком связывается определенный программный код, который для этих целей оформляется в виде исполняемого модуля. Чтобы этот программный код мог быть выполнен, его необходимо загрузить в оперативную память, возможно, выделить некоторое место на диске для хранения данных, предоставить доступ к устройствам ввода-вывода, например к последовательному порту для получения данных по подключенному к этому порту модему; и т. д. В ходе выполнения программе может также понадобиться доступ к информационным ресурсам, например файлам, портам TCP/UPD, семафорам. И, конечно же, невозможно выполнение программы без предоставления ей процессорного времени, то есть времени, в течение которого процессор выполняет коды данной программы.

В операционных системах, где существуют и процессы, и потоки, процесс рассматривается операционной системой как заявка на потребление всех видов ресурсов, кроме одного — процессорного времени. Этот последний важнейший ресурс распределяется операционной системой между другими единицами работы — потоками, которые и получили свое название благодаря тому, что они представляют собой последовательности (потоки выполнения) команд.

В простейшем случае процесс состоит из одного потока, и именно таким образом трактовалось понятие «процесс» до середины 80-х годов (например, в ранних версиях UNIX) и в таком же виде оно сохранилось в некоторых современных ОС. В таких системах понятие «поток» полностью поглощается понятием «процесс», то есть остается только одна единица работы и потребления ресурсов — процесс. Мультипрограммирование осуществляется в таких ОС на уровне процессов.

Для того чтобы процессы не могли вмешаться в распределение ресурсов, а также не могли повредить коды и данные друг друга, важнейшей задачей ОС является изоляция одного процесса от другого. Для этого операционная система обеспечивает каждый процесс отдельным виртуальным адресным пространством, так что ни один процесс не может получить прямого достуца к командам и данным другого процесса.

ПРИМЕЧАНИЕ 

Виртуальное адресное пространство процесса — это совокупность адресов, которыми может манипулировать программный модуль процесса. Операционная система отображает виртуальное адресное пространство процесса на отведенную процессу физическую память.

При необходимости взаимодействия процессы обращаются к операционной системе, которая, выполняя функции посредника, предоставляет им средства межпроцессной связи — конвейеры, почтовые ящики, разделяемые секции памяти и некоторые другие.

Однако в системах, в которых отсутствует понятие потока, возникают проблемы при организации параллельных вычислений в рамках процесса. А такая необходимость может возникать. Действительно, при мультипрограммировании повышается пропускная способность системы, но отдельный процесс никогда не может быть выполнен быстрее, чем в однопрограммном режиме (всякое разделение ресурсов только замедляет работу одного из участников за счет дополнительных затрат времени на ожидание освобождения ресурса). Однако приложение, выполняемое в рамках одного процесса, может обладать внутренним параллелизмом, который в принципе мог бы позволить ускорить его решение. Если, например, в программе предусмотрено обращение к внешнему устройству, то на время этой операции можно не блокировать выполнение всего процесса, а продолжить вычисления по другой ветви программы. Параллельное выполнение нескольких работ в рамках одного интерактивного приложения повышает эффективность работы пользователя. Так, при работе с текстовым редактором желательно иметь возможность совмещать набор нового текста с такими продолжительными по времени операциями, как переформатирование значительной части текста, печать документа или его сохранение на локальном или удаленном диске. Еще одним примером необходимости распараллеливания является сетевой сервер баз данных. В этом случае параллелизм желателен как для обслуживания различных запросов к базе данных, так и для более быстрого выполнения отдельного запроса за счет одновременного просмотра различных записей базы.

Потоки возникли в операционных системах как средство распараллеливания вычислений. Конечно, задача распараллеливания вычислений в рамках одного приложения может быть решена и традиционными способами.

Во-первых, прикладной программист может взять на себя сложную задачу организации параллелизма, выделив в приложении некоторую подпрограмму- диспетчер, которая периодически передает управление той или иной ветви вычислений. При этом программа получается логически весьма запутанной, с многочисленными передачами управления, что существенно затрудняет ее отладку и модификацию.

Во-вторых, решением является создание для одного приложения нескольких процессов для каждой из параллельных работ. Однако использование для создания процессов стандартных средств ОС не позволяет учесть тот факт, что эти процессы решают единую задачу, а значит, имеют много общего между собой — они могут работать с одними и теми же данными, использовать один и тот же кодовый сегмент, наделяться одними и теми же правами доступа к ресурсам вычислительной системы. Так, если в примере с сервером баз данных создавать отдельные процессы для каждого запроса, поступающего из сети, то все процессы будут выполнять один и тот же программный код и выполнять поиск в записях, общих для всех процессов файлов данных. А операционная система при таком подходе будет рассматривать эти процессы наравне со всеми остальными процессами и с помощью универсальных механизмов обеспечивать их изоляцию друг от друга. В данном случае все эти достаточно громоздкие механизмы используются явно не по назначению, выполняя не только бесполезную, но и вредную работу, затрудняющую обмен данными между различными частями приложения. Кроме того, на создание каждого процесса ОС тратит определенные системные ресурсы, которые в данном случае неоправданно дублируются — каждому процессу выделяются собственное виртуальное адресное пространство, физическая память, закрепляются устройства ввода-вывода и т. п.

Из всего вышеизложенного, следует, что в операционной системе наряду с процессами нужен другой механизм распараллеливания вычислений, который учитывал бы тесные связи между отдельными ветвями вычислений одного и того же приложения. Для этих целей современные ОС предлагают механизм многопоточной обработки (multithreading). При этом вводится новая единица работы — поток выполнения, а понятие «процесс» в значительной степени меняет смысл. Понятию «поток» соответствует последовательный переход процессора от одной команды программы к другой. ОС распределяет процессорное время между потоками. Процессу ОС назначает адресное пространство и набор ресурсов, которые совместно используются всеми его потоками.

ПРИМЕЧАНИЕ

Заметим, что в однопрограммных системах не возникает необходимости введения понятия, обозначающего единицу работы, так как там не существует проблемы разделения ресурсов.

Создание потоков требует от ОС меньших накладных расходов, чем процессов. В отличие от процессов, которые принадлежат разным, вообще говоря, конкурирующим приложениям, все потоки одного процесса всегда принадлежат одному приложению, поэтому ОС изолирует потоки в гораздо меньшей степени, нежели процессы в традиционной мультипрограммной системе. Все потоки одного процесса используют общие файлы, таймеры, устройства, одну и ту же область оперативной памяти, одно и то же адресное пространство. Это означает, что они разделяют одни и те же глобальные переменные. Поскольку каждый поток может иметь доступ к любому виртуальному адресу процесса, один поток может использовать стек другого потока. Между потоками одного процесса нет полной защиты, потому что, во-первых, это невозможно, а во-вторых, не нужно. Чтобы организовать взаимодействие и обмен данными, потокам вовсе не требуется обращаться к ОС, им достаточно использовать общую память — один поток записывает данные, а другой читает их. С другой стороны, потоки разных процессов по-прежнему хорошо защищены друг от друга.

Итак, мультипрограммирование более эффективно на уровне потоков, а не процессов. Каждый поток имеет собственный счетчик команд и стек. Задача, оформленная в виде нескольких потоков в рамках одного процесса, может быть выполнена быстрее за счет псевдопараллельного (или параллельного в мультипроцессорной системе) выполнения ее отдельных частей. Например, если электронная таблица была разработана с учетом возможностей многопоточной обработки, то пользователь может запросить пересчет своего рабочего листа и одновременно продолжать заполнять таблицу. Особенно эффективно можно использовать многопоточность для выполнения распределенных приложений, например многопоточный сервер может параллельно выполнять запросы сразу нескольких клиентов.

Использование потоков связано не только со стремлением повысить производительность системы за счет параллельных вычислений, но и с целью создания более читабельных, логичных программ. Введение нескольких потоков выполнения упрощает программирование. Например, в задачах типа «писатель-читатель» один поток выполняет запись в буфер, а другой считывает записи из него. Поскольку они разделяют общий буфер, не стоит их делать отдельными процессами. Другой пример использования потоков — управление сигналами, такими как прерывание с клавиатуры (del или break). Вместо обработки сигнала прерывания один поток назначается для постоянного ожидания поступления сигналов. Таким образом, использование потоков может сократить необходимость в прерываниях пользовательского уровня. В этих примерах не столь важно параллельное выполнение, сколь важна ясность программы.

Наибольший эффект от введения многопоточной обработки достигается в мультипроцессорных системах, в которых потоки, в том числе и принадлежащие одному процессу, могут выполняться на разных процессорах действительно параллельно (а не псевдопараллельно).

Создание процессов и потоков

Создать процесс — это прежде всего означает создать описатель процесса, в качестве которого выступает одна или несколько информационных структур, содержащих все сведения о процессе,, необходимые операционной системе для управления им. В число таких сведений могут входить, например, идентификатор процесса, данные о расположении в памяти исполняемого модуля, степень привилегированности процесса (приоритет и права доступа) и т. п. Примерами описателей процесса являются блок управления задачей (ТСВ — Task Control Block) в OS/360, управляющий блок процесса (РСВ — Process Control Block) в OS/2, дескриптор процесса в UNIX, объект-процесс (object-process) в Windows NT.

Создание описателя процесса знаменует собой появление в системе еще одного претендента на вычислительные ресурсы. Начиная с этого момента при распределении ресурсов ОС должна принимать во внимание потребности нового процесса.

Создание процесса включает загрузку кодов и данных исполняемой программы данного процесса с диска в оперативную память. Для этого ОС должна обнаружить местоположение такой программы на диске, перераспределить оперативную память и выделить память исполняемой программе нового процесса. Затем необходимо считать программу в выделенные для нее участки памяти и, возможно, изменить параметры программы в зависимости от размещения в памяти. В системах с виртуальной памятью в начальный момент может загружаться только часть кодов и данных процесса, с тем чтобы «подкачивать» остальные по мере необходимости. Существуют системы, в которых на этапе создания процесса не требуется непременно загружать коды и данные в оперативную память, вместо этого исполняемый модуль копируется из того каталога файловой системы, в котором он изначально находился, в область подкачки — специальную область диска, отведенную для хранения кодов и данных процессов. При выполнении всех этих действий подсистема управления процессами тесно взаимодействует с подсистемой управления памятью и файловой системой.

В многопоточной системе при создании процесса ОС создает для каждого процесса как минимум один поток выполнения. При создании потока так же, как и при создании процесса, операционная система генерирует специальную информационную структуру — описатель потока, который содержит идентификатор потока, данные о правах доступа и приоритете, о состоянии потока и другую информацию. В исходном состоянии поток (или процесс, если речь идет о системе, в которой понятие «поток» не определяется) находится в приостановленном состоянии. Момент выборки потока на выполнение осуществляется в соответствии с принятым в данной системе правилом предоставления процессорного времени и с учетом всех существующих в данный момент потоков и процессов. В случае если коды и данные процесса находятся в области подкачки, необходимым условием активизации потока процесса является также наличие места в оперативной памяти для загрузки его исполняемого модуля.

Во многих системах поток может обратиться к ОС с запросом на создание так называемых потоков-потомков. В разных ОС по-разному строятся отношения между потоками-потомками и их родителями. Например, в одних ОС выполнение родительского потока синхронизируется с его потомками, в частности после завершения родительского потока ОС может снимать с выполнения всех его потомков. В других системах потоки-потомки могут выполняться асинхронно по отношению к родительскому потоку. Потомки, как правило, наследуют многие свойства родительских потоков. Во многих системах порождение потомков является основным механизмом создания процессов и потоков.

Рассмотрим в качестве примера создание процессов в популярной версии операционной системы UNIX System V Release 4. В этой системе потоки не поддерживаются, в качестве единицы управления и единицы потребления ресурсов выступает процесс.

При управлении процессами операционная система использует два основных типа информационных структур: дескриптор процесса и контекст процесса. Дескриптор процесса содержит такую информацию о процессе, которая необходима ядру в течение всего жизненного цикла процесса независимо от того, находится он в активном или пассивном состоянии, находится образ процесса в оперативной памяти или выгружен на диск. (Образом процесса называется совокупность его кодов и данных.)

Дескрипторы отдельных процессов объединены в список, образующий таблицу процессов. Память для таблицы процессов отводится динамически в области ядра. На основании информации, содержащейся в таблице процессов, операционная система осуществляет планирование и синхронизацию процессов. В дескрипторе прямо или косвенно (через указатели, на связанные с процессом структуры) содержится информация о состоянии процесса, о расположении образа процесса в оперативной памяти и на диске, о значении отдельных составляющих приоритета, а также о его итоговом значении — глобальном приоритете, об идентификаторе пользователя, создавшего процесс, о родственных процессах, о событиях, осуществления которых ожидает данный процесс, и некоторая другая информация.

Контекст процесса содержит менее оперативную, но более объемную часть информации о процессе, необходимую для возобновления выполнения процесса с прерванного места: содержимое регистров процессора, коды ошибок выполняемых процессором системных вызовов, информация обо всех открытых данным процессом файлах и незавершенных операциях ввода-вывода и другие данные, характеризующие состояние вычислительной среды в момент прерывания. Контекст, так же как и дескриптор процесса, доступен только программам ядра, то есть находится в виртуальном адресном пространстве операционной системы, однако он хранится не в области ядра, а непосредственно примыкает к образу процесса и перемещается вместе с ним, если это необходимо, из оперативной памяти на диск.

Порождение процессов в системе UNIX происходит в результате выполнения системного вызова fork. ОС строит образ порожденного процесса являющийся точной копией образа породившего процесса, то есть дублируются дескриптор, контекст и образ процесса. Сегмент данных и сегмент стека родительского процесса копируются на новое место, образуя сегменты данных и стека процесса-потомка. Процедурный сегмент копируется только тогда, когда он не является разделяемым. В противном случае процесс-потомок становится еще одним процессом, разделяющим данный процедурный сегмент.

После выполнения системного вызова fork оба процесса продолжают выполнение с одной и той же точки. Чтобы процесс мог опознать, является он родительским процессом или процессом-потомком, системный вызов fork возвращает в качестве своего значения в породивший процесс идентификатор порожденного процесса, а в порожденный процесс — NULL. Типичное разветвление на языке С записывается так:

if( -fork() ) { действия родительского процесса }

else { действия порожденного процесса }

Идентификатор потомка может быть присвоен переменной, входящей в контекст родительского процесса. Так как контекст процесса наследуется его потомками, то потомки могут узнать идентификаторы своих «старших братьев», таким образом сумма знаний наследуется при порождении и может быть распространена между родственными процессами. На независимости идентификатора процесса от выполняемой процессом программы построен механизм, позволяющий процессу перейти к выполнению другой программы с помощью системного вызова ехес.

Таким образом, в UNIX порождение нового процесса происходит в два этапа — сначала создается копия процесса-родителя, затем у нового процесса производится замена кодового сегмента на заданный.

Вновь созданному процессу операционная система присваивает целочисленный идентификатор, уникальный на весь период функционирования системы.

Планирование и диспетчеризация потоков

На протяжении существования процесса выполнение его потоков может быть многократно прервано и продолжено. (В системе, не поддерживающей потоки, все сказанное ниже о планировании и диспетчеризации относится к процессу в целом.)

Переход от выполнения одного потока к другому осуществляется в результате планирования и диспетчеризации. Работа по определению того, в какой момент необходимо прервать выполнение текущего активного потока и какому потоку предоставить возможность выполняться, называется планированием. Планирование потоков осуществляется на основе информации, хранящейся в описателях процессов и потоков. При планировании могут приниматься во внимание приоритет потоков, время их ожидания в очереди, накопленное время выполнения, интенсивность обращений к вводу-выводу и другие факторы. ОС планирует выполнение потоков независимо от того, принадлежат ли они одному или разным процессам. Так, например, после выполнения потока некоторого процесса ОС может выбрать для выполнения другой поток того же процесса или же назначить к выполнению поток другого процесса.

Планирование потоков, по существу, включает в себя решение двух задач:

  определение момента времени для смены текущего активного потока; 

 выбор для выполнения потока из очереди готовых потоков.

Существует множество различных алгоритмов планирования потоков, по-своему решающих каждую из приведенных выше задач. Алгоритмы планирования могут преследовать различные цели и обеспечивать разное качество мультипрограммирования. Например, в одном случае выбирается такой алгоритм планирования, при котором гарантируется, что ни один поток/процесс не будет занимать процессор дольше определенного времени, в другом случае целью является максимально быстрое выполнение «коротких» задач, а в третьем случае — преимущественное право занять процессор получают потоки интерактивных приложений. Именно особенности реализации планирования потоков в наибольшей степени определяют специфику операционной системы, в частности, является ли она системой пакетной обработки, системой разделения времени или системой реального времени.

В большинстве операционных систем универсального назначения планирование осуществляется динамически (on-line), то есть решения принимаются во время работы системы на основе анализа текущей ситуации. ОС работает в условиях неопределенности — потоки и процессы появляются в случайные моменты времени и также непредсказуемо завершаются. Динамические планировщики могут гибко приспосабливаться к изменяющейся ситуации и не используют никаких предположений о мультипрограммной смеси. Для того чтобы оперативно найти в условиях такой неопределенности оптимальный в некотором смысле порядок выполнения задач, операционная система должна затрачивать значительные усилия.

Другой тип планирования — статический — может быть использован в специализированных системах, в которых весь набор одновременно выполняемых задач определен заранее, например в системах реального времени. Планировщик называется статическим (или предварительным планировщиком), если он принимает решения о планировании не во время работы системы, а заранее (off-line). Соотношение между динамическим и статическим планировщиками аналогично соотношению между диспетчером железной дороги, который пропускает поезда строго по предварительно составленному расписанию, и регулировщиком на перекрестке автомобильных дорог, не оснащенном светофорами, который решает, какую машину остановить, а какую пропустить, в зависимости от ситуации на перекрестке.

Результатом работы статического планировщика является таблица, называемая расписанием, в которой указывается, какому потоку/процессу, когда и на какое время должен быть предоставлен процессор. Для построения расписания планировщику нужны как можно более полные предварительные знания о характеристиках набора задач, например о максимальном времени выполнения каждой задачи, ограничениях предшествования, ограничениях по взаимному исключению, предельным срокам и т. д.

После того как расписание готово, оно может использоваться операционной системой для переключения потоков и процессов. При этом накладные расходы ОС на исполнение расписания оказываются значительно меньшими, чем при динамическом планировании, и сводятся лишь к диспетчеризации потоков/процессов.

Диспетчеризация заключается в реализации найденного в результате планирования (динамического или статистического) решения, то есть в переключении процессора с одного потока на другой. Прежде чем прервать выполнение потока, ОС запоминает его контекст, с тем чтобы впоследствии использовать эту информацию для последующего возобновления выполнения данного потока. Контекст отражает, во-первых, состояние аппаратуры компьютера в момент прерывания потока: значение счетчика команд, содержимое регистров общего назначения, режим работы процессора, флаги, маски прерываний и другие параметры. Во-вторых, контекст включает параметры операционной среды, а именно ссылки на открытые файлы, данные о незавершенных операциях ввода-вывода, коды ошибок выполняемых данным потоком системных вызовов и т. д.

Диспетчеризация сводится к следующему:

 сохранение контекста текущего потока, который требуется сменить;

 загрузка контекста нового потока, выбранного в результате планирования;

 запуск нового потока на выполнение.

Поскольку операция переключения контекстов существенно влияет на производительность вычислительной системы, программные модули ОС выполняют диспетчеризацию потоков совместно с аппаратными средствами процессора.

В контексте потока можно выделить часть, общую для всех потоков данного процесса (ссылки на открытые файлы), и часть, относящуюся только к данному потоку (содержимое регистров, счетчик команд, режим процессора). Например, в среде NetWare 4.x различаются три вида контекстов: глобальный контекст (контекст процесса), контекст группы потоков и контекст отдельного потока. Соотношение между данными этих контекстов напоминает соотношение глобальных и локальных переменных в программе, написанной на языке С. Переменные глобального контекста доступны для всех потоков, созданных в рамках одного процесса. Переменные локального контекста доступны только для кодов определенного потока, аналогично локальным переменным функции. В NetWare можно создавать несколько групп потоков внутри одного процесса и эти группы будут иметь свой групповой контекст. Переменные, принадлежащие групповому контексту, доступны всем потокам, входящим в группу, но недоступны остальным потокам.

Очевидно, что такая иерархическая организация контекстов ускоряет переключение потоков, так как при переключении с потока на поток в пределах одной группы нет необходимости заменять контексты групп или глобальные контексты, достаточно лишь заменить контексты потоков, которые имеют меньший объем. Аналогично при переключении с потока одной группы на поток другой группы в пределах одного процесса глобальный контекст не изменяется, а изменяется лишь контекст группы. Переключение же глобальных контекстов происходит только при переходе с потока одного процесса на поток другого процесса.

studfiles.net

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *