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

Обычно выделяют две общие категории потоков: потоки на уровне пользователя (user-level threads - ULT) и потоки на уровне ядра (kernel-level threads - KLT). Потоки второго типа в литературе иногда называются потоками, поддерживаемыми ядром, или облегченными процессами.

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

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

Рис. 4.6. Потоки на пользовательском уровне и на уровне ядра

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

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

  1. Приложение, в котором выполняется поток 2, может произвести системный вызов, например запрос ввода-вывода, который блокирует процесс В. В результате этого вызова управление перейдет к ядру. Ядро вызывает процедуру ввода-вывода, переводит процесс В в состояние блокировки и передает управление другому процессу. Тем временем поток 2 процесса В все еще находится в состоянии выполнения в соответствии со структурой данных, поддерживаемой библиотекой потоков. Важно отметить, что поток 2 не выполняется в том смысле, что он работает с процессором; однако библиотека потоков воспринимает его как выполняющийся. Соответствующие диаграммы состояний показаны на рис. 4.7,6.
  2. В результате прерывания по таймеру управление может перейти к ядру; ядро определяет, что интервал времени, отведенный выполняющемуся в данный момент процессу В, истек. Ядро переводит процесс В в состояние готовности и передает управление другому процессу. В это время, согласно структуре данных, которая поддерживается библиотекой потоков, поток 2 процесса В по-прежнему будет находиться в состоянии выполнения. Соответствующие диаграммы состояний показаны на рис. 4.7,в.
  3. Поток 2 достигает точки выполнения, когда ему требуется, чтобы поток 1 процесса В выполнил некоторое действие. Он переходит в заблокированное состояние, а поток 1 - из состояния готовности в состояние выполнения. Сам процесс остается в состоянии выполнения. Соответствующие диаграммы состояний показаны на рис. 4.7,г.


Рис. 4.7. Примеры взаимосвязей между состояниями потоков пользовательского уровня и состояниями процесса

В случаях 1 и 2 (см. рис. 4.7,6 и в) при возврате управления процессу В возобновляется выполнение потока 2. Заметим также, что процесс, в котором выполняется код из библиотеки потоков, может быть прерван либо из-за того, что закончится отведенный ему интервал времени, либо из-за наличия процесса с более высоким приоритетом. Когда возобновится выполнение прерванного процесса, оно продолжится работой процедуры из библиотеки потоков, которая завершит переключение потоков и передаст управление новому потоку процесса.

Использование потоков на пользовательском уровне обладает некоторыми преимуществами перед использованием потоков на уровне ядра. К этим преимуществам относятся следующие:

  1. Переключение потоков не включает в себя переход в режим ядра, так как структуры данных по управлению потоками находятся в адресном пространстве одного и того же процесса. Поэтому для управления потоками процессу не нужно переключаться в режим ядра. Благодаря этому обстоятельству удается избежать накладных расходов, связанных с двумя переключениями режимов (пользовательского режима в режим ядра и обратно).
  2. Планирование производится в зависимости от специфики приложения. Для одних приложений может лучше подойти простой алгоритм планирования по круговому алгоритму, а для других - алгоритм планирования, основанный на использовании приоритета. Алгоритм планирования может подбираться для конкретного приложения, причем это не повлияет на алгоритм планирования, заложенный в операционной системе.
  3. Использование потоков на пользовательском уровне применимо для любой операционной системы. Для их поддержки в ядро системы не потребуется вносить никаких изменений. Библиотека потоков представляет собой набор утилит, работающих на уровне приложения и совместно используемых всеми приложениями.

Использование потоков на пользовательском уровне обладает двумя явными недостатками по сравнению с использованием потоков на уровне ядра.

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

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

Потоки на уровне ядра

В программе, работа которой полностью основана на потоках, работающих на уровне ядра, все действия по управлению потоками выполняются ядром. В области приложений отсутствует код, предназначенный для управления потоками. Вместо него используется интерфейс прикладного программирования (application programming interface - API) средств ядра, управляющих потоками. Примерами такого подхода являются операционные системы OS/2, Linux и W2K.
На рис. 4.6,6" проиллюстрирована стратегия использования потоков на уровне ядра. Любое приложение при этом можно запрограммировать как многопоточное; все потоки приложения поддерживаются в рамках единого процесса. Ядро поддерживает информацию контекста процесса как единого целого, а также контекстов каждого отдельного потока процесса. Планирование выполняется ядром исходя из состояния потоков. С помощью такого подхода удается избавиться от двух упомянутых ранее основных недостатков потоков пользовательского уровня. Во-первых, ядро может одновременно осуществлять планирование работы нескольких потоков одного и того же процесса на нескольких процессорах. Во-вторых, при блокировке одного из потоков процесса ядро может выбрать для выполнения другой поток этого же процесса. Еще одним преимуществом такого подхода является то, что сами процедуры ядра могут быть многопоточными.
Основным недостатком подхода с использованием потоков на уровне ядра по сравнению с использованием потоков на пользовательском уровне является то, что для передачи управления от одного потока другому в рамках одного и того же процесса приходится переключаться в режим ядра. Результаты исследований, проведенных на однопроцессорной машине VAX под управлением UNIX-подобной операционной системы, представленные в табл. 4.1, иллюстрируют различие между этими двумя подходами. Сравнивалось время выполнения таких двух задач, как (1) нулевое ветвление (Null Fork) - время, затраченное на создание, планирование и выполнение процесса/потока, состоящего только из нулевой процедуры (измеряются только накладные расходы, связанные с ветвлением процесса/потока), и (2) ожидание сигнала (Signal-Wait) - время, затраченное на передачу сигнала от одного процесса/потока другому процессу/потоку, находящемуся в состоянии ожидания (накладные расходы на синхронизацию двух процессов/потоков). Чтобы было легче сравнивать полученные значения, заметим, что вызов процедуры на машине VAX, используемой в этом исследовании, длится 7 us, а системное прерывание - 17 us. Мы видим, что различие во времени выполнения потоков на уровне ядра и потоков на пользовательском уровне более чем на порядок превосходит по величине различие во времени выполнения потоков на уровне ядра и процессов.

Таблица 4.1. Время задержек потоков (ка)

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

Комбинированные подходы

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

Другие схемы

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

Таблица 4.2. Соотношение между потоками и процессами

Соответствие нескольких потоков нескольким процессам

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

  1. Реализовать всю программу в виде единого процесса. Такой прямолинейный подход является вполне обоснованным. Недостатки этого подхода связаны с управлением памятью. Эффективно организованный как единое целое процесс может занимать в памяти много места, в то время как для подпрограммы ввода-вывода требуется относительно небольшое адресное пространство. Из-за того что подпрограмма ввода-вывода выполняется в адресном пространстве более объемной программы, во время выполнения ввода-вывода весь процесс должен оставаться в основной памяти, либо операция ввода-вывода будет выполняться с применением свопинга. То же происходит и в случае, когда и основная программа, и подпрограмма ввода-вывода реализованы в виде двух потоков в одном адресном пространстве.
  2. Основная программа и подпрограмма ввода-вывода реализуются в виде двух отдельных процессов. Это приводит к накладным затратам, возникающим в результате создания подчиненного процесса. Если ввод-вывод производится достаточно часто, то необходимо будет либо оставить такой подчиненный процесс активным на все время работы основного процесса, что связано с затратами на управление ресурсами, либо часто создавать и завершать процесс с подпрограммой, что приведет к снижению эффективности.
  3. Реализовать действия основной программы и подпрограммы ввода-вывода как единый поток. Однако для основной программы следует создать свое адресное пространство (свой домен), а для подпрограммы ввода-вывода - свое. Таким образом, поток в ходе выполнения программы будет переходить из одного адресного пространства к другому. Операционная система может управлять этими двумя адресными пространствами независимо, не затрачивая никаких дополнительных ресурсов на создание процесса. Более того, адресное пространство, используемое подпрограммой ввода-вывода, может использоваться совместно с другими простыми подпрограммами ввода-вывода.

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

Соответствие одного потока нескольким процессам

В области распределенных операционных систем (разрабатываемых для управления распределенными компьютерными системами) представляет интерес концепция потока как основного элемента, способного переходить из одного адресного пространства в другое (В последние годы активно исследуется тема перехода процессов и потоков из одного адресного пространства в другое (миграция). Эта тема рассматривается в главе 14, "Управление распределенными процессами". ). Заслуживают упоминания операционная система Clouds и, в особенности, ее ядро, известное под названием Ra . В качестве другого примера можно привести систему Emerald .

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

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

Что общего у футболки и компьютерной программы? Они обе состоят из многих потоков! В то время как нитки в футболке держат ткань в виде единого полотна, 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 г. был представлен двухъядерный процессор и двухъядерный процессор AMD Athlon 64 X2.

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

Модели

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

1: 1 (поток на уровне ядра) — темы, созданные пользователем в ядре, являются простейшей возможной реализацией потоков. OS/2 и Win32 используют этот подход изначально, тогда как в Linux 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, имеют иную модель потоков, которая поддерживает максимальное их число для моделирования оборудования.

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

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

Потоки (threads) и многопоточное выполнение программ (multi-threading)

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

Последовательный (однопоточный) процесс – это процесс, который имеет только один поток управления (control flow), характеризующийся изменением его счетчика команд. Поток (thread) – это запускаемый из некоторого процесса особого рода параллельный процесс, выполняемый в том же адресном пространстве, что и процесс-родитель. Схема организации однопоточного и многопоточного процессов изображена на рис. 1.

Рис. 11. Однопоточный и многопоточный процессы.

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

Многопоточность имеет большие преимущества:

  • Увеличение скорости (по сравнению с использованием обычных процессов). Многопоточность основана на использовании облегченных процессов (lightweight processes) ,работающих в общем пространстве виртуальной памяти. Благодаря многопоточности, не возникает больше неэффективных ситуаций, типичных для классической системы UNIX.
  • Использование общих ресурсов . Потоки одного процесса используют общую память и файлы.
  • Экономия . Благодаря многопоточности, достигается значительная экономия памяти, по причинам, объясненным выше. Также достигается и экономия времени, так как переключение контекста на облегченный процесс, для которого требуется только сменить стек и восстановить значения регистров, значительно быстрее, чем на обычный процесс.

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



Пользовательские потоки и потоки ядра

Модели многопоточности . Реализация многопоточности в ОС, как и многих других возможностей, имеет несколько уровней абстракции. Самый высокий из них – пользовательский уровень. С точки зрения пользователя и его программ, управление потоками реализовано через библиотеку потоков пользовательского уровня (user threads) Отметим, что существует несколько моделей потоков пользовательского уровня.

Низкоуровневые потоки, в которые отображаются пользовательские потоки, называются потоками ядра (kernel threads) .Они поддержаны и используются на уровне ядра операционной системы. Как и подходы к пользовательским потокам, подходы к архитектуре и реализации системных потоков и к отображению пользовательских потоков в системные в разных ОС различны. Например, собственные модели потоков ядра со своей спецификой реализованы в следующих ОС:

  • Windows 95/98/NT/2000/XP/2003/2008/7;
  • Solaris;
  • Tru64 UNIX;
  • BeOS;
  • Linux.

Существуют различные модели многопоточности – способы отображения пользовательских потоков в потоки ядра. Теоретически возможны (и на практике реализованы) следующие модели многопоточности:

Модель много / один (many-to-one) – отображение нескольких пользовательских потоков в один и тот же поток ядра. Используется в операционных системах, не поддерживающих множественные системные потоки (например, с целью экономии памяти). Данная модель изображена на рис. 2.

Рис. 10.2. Схема модели многопоточности "много / один".

Модель один / один (one-to-one) – взаимно-однозначное отображение каждого пользовательского потока в определенный поток ядра. Примеры ОС, использующих данную модель, - Windows 95/98/NT/2000/XP/2003/2008/7; OS/2. Данная модель изображена на рис. 3.

Рис. 10.3. Схема модели многопоточности "один / один".

Модель много / много (many-to-many) – модель, допускающая отображение нескольких пользовательских потоков в несколько системных потоков. Такая модель позволяет ОС создавать большое число системных потоков. Характерным примером ОС, использующей подобную модель, является ОС Solaris, а также Windows NT / 2000 / XP / 2003 / 2008 / 7 с пакетом ThreadFiber . Данная модель изображена на рис. 4.

Рис. 4. Схема модели многопоточности "много / много".

Потоки в Windows 2000

Как уже отмечалось, в системе Windows реализована модель многопоточности "один / один". Каждый поток содержит:

  • идентификатор потока (thread id);
  • набор регистров
  • отдельные стеки для пользовательских и системных процедур;
  • область памяти для локальных данных потока (thread-local storage – TLS).

Проблемы многопоточности

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

Семантика системных вызовов fork() и exec() .Как уже отмечалось, в классической ОС UNIX системный вызов fork создает новый "тяжеловесный" процесс со своим адресным пространством, что значительно "дороже", чем создание потока. Однако, с целью поддержания совместимости программ снизу вверх, приходится сохранять эту семантику, а многопоточность вводить с помощью новых системных вызовов.

Прекращение потоков . Важной проблемой является проблема прекращения потоков: например, если родительский поток прекращается, то должен ли при этом прекращаться дочерний поток? Если прекращается стандартный процесс, создавший несколько потоков, то должны ли прекращаться все его потоки? Ответы на эти вопросы в разных ОС неоднозначны.

Обработка сигналов . Сигналы в UNIX – низкоуровневый механизм обработки ошибочных ситуаций. Примеры сигналов: SIGSEGV - нарушение сегментации (обращение по неверному адресу, чаще всего по нулевому); SIGKILL – сигнал процессу о выполнении команды kill его уничтожения. Пользователь может определить свою процедуру-обработчик сигнала системным вызовом signal . Проблема в следующем: как распространяются сигналы в многопоточных программах и каким потоком они должны обрабатываться? В большинстве случаев этот вопрос решается следующим образом: сигнал обрабатывается потоком, в котором он сгенерирован, и влияет на исполнение только этого потока. В более современных ОС (например, Windows 2000 и более поздних версиях Windows), основанных на объектно-ориентированной методологии, концепция сигнала заменена более высокоуровневой концепцией исключения (exception) .Исключение распространяется по стеку потока в порядке, обратном порядку вызовов методов, и обрабатывается первым из них, в котором система находит подходящий обработчик. Аналогичная схема обработки исключений реализована в Java и в.NET.

Группы потоков . В сложных задачах, например, задачах моделирования, при большом числе разнородных потоков, возникает потребность в их структурировании и помощью концепции группы потоков – совокупности потоков, имеющей свое собственное имя, над потоками которой определены групповые операции. Наиболее удачно, с нашей точки зрения, группы потоков реализованы в Java (с помощью класса ThreadGroup ). Следует отметить также эффективную реализацию пулов потоков (ThreadPool) в.NET.

Локальные данные потока (thread-local storage - TLS) – данные, принадлежащие только определенному потоку и используемые только этим потоком. Необходимость в таких данных очевидна, так как многопоточность – весьма важный метод распараллеливания решения большой задачи, при котором каждый поток работает над решением порученной ему части. Все современные операционные системы и платформы разработки программ поддерживают концепцию локальных данных потока.

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

Тупики (deadlocks) и их предотвращение . Как и процессы, потоки могут взаимно блокировать друг друга (т.е. может создаться ситуация deadlock ), при их неаккуратном программировании.

42. Файловые системы. Обзор.

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

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

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

  • ♦ создание, удаление, переименование (и другие операции) именованных наборов данных (именованных файлов) из своих программ или посредством специальных управляющих программ, реализующих функции интерфейса пользователя с его данными и активно использующих систему управления файлами;
  • ♦ работа с не дисковыми периферийными устройствами как с файлами;
  • ♦ обмен данными между файлами, между устройствами, между файлом и уст-

ройством (и наоборот);

  • ♦ работа с файлами с помощью обращений к программным модулям системы

управления файлами (часть API ориентирована именно на работу с файлами);

  • ♦ защита файлов от несанкционированного доступа.

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

Другими словами, термин «файловая система» определяет, прежде всего,

принципы доступа к данным, организованным в файлы. Этот же термин часто ис-

пользуют и по отношению к конкретным файлам, расположенным на том или ином

носителе данных. А термин «система управления файлами» следует употреблять

по отношению к конкретной реализации файловой системы, то есть это – комплекс

программных модулей, обеспечивающих работу с файлами в конкретной операци-

онной системе.

Следует ещё раз заметить, что любая система управления файлами не сущест-

вует сама по себе – она разработана для работы в конкретной ОС. В качестве при-

мера можно сказать, что всем известная файловая система FAT (file allocation table)

имеет множество реализации как система управления файлами. Так, система, полу-

чившая это название и разработанная для первых персональных компьютеров, на-

зывалась просто FAT (сейчас её называют FAT-12). Её разрабатывали для работы с

дискетами, и некоторое время она использовалась при работе с жесткими дисками.

Потом её усовершенствовали для работы с жесткими дисками большего объёма, и

эта новая реализация получила название FAT-16. Это название файловой системы

мы используем и по отношению к системе управления файлами самой MS-DOS.

Реализацию же системы управления файлами для OS/2, которая использует основ-

ные принципы системы FAT, называют super-FAT; основное отличие – возмож-

ность поддерживать для каждого файла расширенные атрибуты. Есть версия системы управления файлами с принципами FAT и для Windows 95/98, для Windows

NT и т. д. Другими словами, для работы с файлами, организованными в соответствии с некоторой файловой системой, для каждой ОС должна быть разработана соответствующая система управления файлами. Эта система управления файлами будет работать только в той ОС, для которой она и создана; но при этом она позволит работать с файлами, созданными с помощью системы управления файлами другой ОС, работающей по тем же основным принципам файловой системы.

Файловая система FAT

Как мы уже отмечали, аббревиатура FAT (file allocation table) расшифровывается как «таблица размещения файлов». Этот термин относится к линейной табличной структуре со сведениями о файлах – именами файлов, их атрибутами и другими данными,

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

ского диска делится на две области (рис. 4.6): системную область и область дан-

Рис.4.6. Структура логического диска

R | Sec | Fat1 | Fat2 | Dir | Каталоги и файлы

________________________________________________________

Системная область | Область данных

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

Загрузочной записи (boot record, BR);

Зарезервированных секторов (reserved sector, ResSecs);

Таблицы размещения файлов (file allocation table, FAT);

Корневого каталога (root directory, RDir).

Таблица размещения файлов

Таблица размещения файлов является очень важной информационной структурой. Можно сказать, что она представляет собой карту (образ) области данных, в которой описывается состояние каждого участка области данных. Область данных разбивают на так называемые кластеры. Кластер представляет собой один или несколько смежных секторов в логическом дисковом адресном пространстве (точнее – только в области данных). В таблице FAT кластеры, принадлежащие одному файлу (некорневому каталогу), связываются в цепочки. Для указания номера кластера и системе управления файлами FAT-16 используется 16-битовое слово, следовательно, можно иметь до 216 = 65536 кластеров (с номерами от 0 до 65535). Кластер – это минимальная адресуемая единица дисковой памяти, выделяемая файлу (или некорневому каталогу). Файл или каталог занимает целое число кластеров. Последний кластер при этом может быть задействован не полностью, что приведет к заметной потере дискового пространства при большом размере кластера. На дискетах кластер занимает один или два сектора, а на жёстких дисках – в зависимости от объёма раздела.

FAT32 - последняя версия файловой системы FAT и улучшение предыдущей версии, известной как FAT16. Она была создана, чтобы преодолеть ограничения на размер тома в FAT16, позволяя при этом использовать старый код программ MS-DOS и сохранив формат. FAT32 использует 32-разрядную адресацию кластеров. FAT32 появилась вместе с Windows 95 OSR2.

NTFS (от англ. New Technology File System - «файловая система новой технологии») - стандартная файловая система для семейства операционных систем Microsoft Windows NT.

NTFS заменила использовавшуюся в MS-DOS и Microsoft Windows файловую систему FAT. NTFS поддерживает системуметаданных

И использует специализированные структуры данных для хранения информации о файлах для улучшения производительности, надёжности и эффективности использования дискового пространства. NTFS хранит информацию о файлах в главной файловой таблице - Master File Table (MFT). NTFS имеет встроенные возможности разграничивать доступ к данным для различных пользователей и групп пользователей (списки контроля доступа - Access Control Lists (ACL)), а также назначать квоты (ограничения на максимальный объём дискового пространства, занимаемый теми или иными пользователями). NTFS использует систему журналирования USN для повышения надёжности файловой системы.

43. Управление памятью в операционной системе. Типы адресов.

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

Следует иметь в виду важную особенность основной памяти. В компьютерных архитектурах имеется два различных способа нумерации байтов в слове. По традиции будем представлять себе память как линейный массив, расположенный "слева направо", такой, что адреса слов, находящихся левее, меньше, чем адреса слов, находящихся правее. Каждое слово делится на байты, имеющие в слове свои номера – 0, 1 и т.д.. Например, в 64-разрядных системах в слове 8 байтов, с номерами от 0 до 7, в более старых 16-разрядных (x86) – два байта, с номерами 0 и 1. Если нумерация байтов в слове начинается слева, т.е. начиная со старших битов, то такую архитектуру принято называть big endian , если же справа, т.е. начиная с младших битов, то little endian. Например, при big endian – архитектуре 32-разрядного процессора байты двух соседних слов памяти нумеруются так: 0, 1, 2, 3, 0, 1, 2, 3. При little endian же архитектуре нумерация будет иной: 3, 2, 1, 0, 3, 2, 1, 0. Представим теперь, что мы хотим рассматривать эти же два слова как массив байтов длиной 8 и записать туда байт за байтом символы строки: "ЭТОТЕКСТ" (всего – 8 символов). Такая операция при обеих архитектурах будет выполнена одинаково, т.е. последовательные байты получат именно эти значения. Затем рассмотрим результат снова, но уже как последовательность их двух слов. Каково будет содержимое этих слов? При big endian – архитектуре сюрпризов не будет: первое слово – "ЭТОТ", второе "ЕКСТ". Однако при little endian – архитектуре результат будет совсем иным: первое слово – "ТОТЭ", второе – "ТСКЕ" ! Не забудем, что при обработке целого слова в little endian – архитектуре байты как бы "переставляются" в обратном порядке. Разумеется, это неудобно. С подобной проблемой автор столкнулся при переносе написанного им компилятора с архитектуры SPARC (big endian) на архитектуру Intel x86 (little endian), используя типы byte и word на Турбо-Паскале. Подобная операция типична для системных программ, например, таблица идентификаторов в компиляторе должна содержать как символы идентификатора (последовательность байтов), так и другую информацию о нем (длину, ссылки в различные таблицы и т.д.). Поэтому при little endian – архитектуре приходится хранить и обрабатывать байтовые массивы и массивы слов отдельно, и нельзя изменять точку зрения на одну и ту же область памяти и рассматривать ее то как массив байтов, то как массив слов.

Пример little endian – архитектуры – x86. Пример big endian – архитектуры – SPARC. При программировании на языках высокого уровня разработчику, как правило, не приходится учитывать это различие. Однако если при реализации распределения памяти требуется одну и ту же область памяти рассматривать то как массив слов, то как массив байтов, то для little endian – архитектур могут быть "сюрпризы", связанные с тем, что при записи в память как в массив слов байты как бы переставляются.

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

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

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

Типы адресов

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

Рис. 2.7. Типы адресов

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

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

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

44. Методы распределения памяти без использования дискового пространства.

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

Рис. 2.8. Классификация методов распределения памяти

В лекции рассматриваются понятие потока (thread) и многопоточное выполнение (multi-threading); модели многопоточности; пользовательские потоки и потоки ядра; потоки в "Эльбрусе", Solaris, Linux, POSIX, Windows 2000, Java.

    Введение

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

    История многопоточности

    Пользовательские потоки и потоки ядра

    Проблемы многопоточности

    Потоки POSIX (Pthreads)

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

    Потоки в Windows 2000

    Потоки в Linux

    Потоки в Java

    Ключевые термины

    Краткие итоги

    Набор для практики

Введение

Многопоточность (multi-threading) – одна из наиболее интересных и актуальных тем в данном курсе и, по-видимому, в области ИТ вообще, и, кроме того, одна из излюбленных тем автора. Актуальность данной темы особенно велика, в связи с широким распространением многоядерных процессоров. В лекции рассмотрены следующие вопросы:

    Исторический обзор многопоточности

    Модели многопоточного исполнения

    Проблемы, связанные с потоками

    Потоки в POSIX (Pthreads)

    Потоки в Solaris 2

    Потоки в Windows 2000/XP

    Потоки в Linux

    Потоки в Java и.NET.

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

К сожалению, до сих пор мышление многих программистов при разработке программ остается чисто последовательным. Не учитываются широкие возможности параллелизма, в частности, многопоточности. Последовательный (однопоточный) процесс – это процесс, который имеет только один поток управления (control flow), характеризующийся изменением его счетчика команд. Поток (thread) – это запускаемый из некоторого процесса особого рода параллельный процесс, выполняемый в том же адресном пространстве, что и процесс-родитель. Схема организации однопоточного и многопоточного процессов изображена на рис. 10.1 .

Рис. 10.1. Однопоточный и многопоточный процессы.

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

Многопоточность имеет большие преимущества:

    Увеличение скорости (по сравнению с использованием обычных процессов). Многопоточность основана на использовании облегченных процессов (lightweight processes), работающих в общем пространстве виртуальной памяти. Благодаря многопоточности, не возникает больше неэффективных ситуаций, типичных для классической системе UNIX, в которой каждая команда shell (даже команда вывода содержимого текущей директории ls исполнялась как отдельный процесс , причем в своем собственном адресном пространстве. В противоположность облегченным процессам, обычные процессы (имеющие собственное адресное пространство) часть называют тяжеловесными (heavyweight).

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

    Экономия . Благодаря многопоточности, достигается значительная экономия памяти, по причинам, объясненным выше. Также достигается и экономия времени, так как переключение контекста на облегченный процесс, для которого требуется только сменить стек и восстановить значения регистров, значительно быстрее, чем на обычный процесс (см. "Методы взаимодействия процессов " ).

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

История многопоточности

Как небезынтересно отметить, один из первых шагов на пути к широкому использованию многопоточности, по-видимому, был сделан в 1970-е годы советскими разработчиками компьютерной аппаратуры и программистами. МВК "Эльбрус-1", разработанный в 1979 году, поддерживал в аппаратуре и операционной системе эффективную концепцию процесса, которая была близка к современному понятию облегченного процесса. В частности, процесс в "Эльбрусе" однозначно характеризовался своим стеком. Иначе говоря, все процессы были облегченными и исполнялись в общем пространстве виртуальной памяти – других процессов в "Эльбрусе" просто не было!

Концепция многопоточности начала складываться, по-видимому, с 1980-х гг. в системе UNIX и ее диалектах. Наиболее развита многопоточность была в диалекте UNIX фирмы AT&T, на основе которого, как уже отмечалось в общем историческом обзоре, была разработана система Solaris. Все это отразилось и в стандарте POSIX, в который вошла и многопоточность, наряду с другими базовыми возможностями UNIX.

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

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

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

Платформа.NET, появившаяся в 2000 г., предложила свой механизм многопоточности, который фактически является развитием идей Java.

Различие подходов к многопоточности в разных ОС и на разных платформах разработки программ сохраняется и до настоящего времени, что приходится постоянно учитывать разработчикам. Для прикладных программ мы рекомендуем реализовывать многопоточность на платформе Java или.NET, что наиболее удобно и позволяет использовать высокоуровневые понятия и конструкции. Однако в нашем курсе, посвященном операционным системам, мы, естественно, больше внимания уделяем системным вопросам многопоточности и ее реализации в операционных системах.

Пользовательские потоки и потоки ядра

Модели многопоточности. Реализация многопоточности в ОС, как и многих других возможностей, имеет несколько уровней абстракции. Самый высокий из них – пользовательский уровень. С точки зрения пользователя и его программ, управление потоками реализовано через библиотеку потоков пользовательского уровня (user threads). Подробнее конкретные операции над пользовательскими потоками будут рассмотрены немного позже. Пока отметим лишь, что существует несколько моделей потоков пользовательского уровня, среди которых:

    POSIX Pthreads – потоки, специфицированные стандартом POSIX и используемые в POSIX-приложениях (рассмотрены позже в данной лекции);

    Mac C-threads – пользовательские потоки в системе MacOS;

    Solaris threads – пользовательские потоки в ОС Solaris (рассмотрены позже в данной лекции).

Низкоуровневые потоки, в которые отображаются пользовательские потоки, называются потоками ядра (kernel threads). Они поддержаны и используются на уровне ядра операционной системы. Как и подходы к пользовательским потокам, подходы к архитектуре и реализации системных потоков и к отображению пользовательских потоков в системные в разных ОС различны.Например, собственные модели потоков ядра со своей спецификой реализованы в следующих ОС:

    Windows 95/98/NT/2000/XP/2003/2008/7;

Существуют различные модели многопоточности – способы отображения пользовательских потоков в потоки ядра. Теоретически возможны (и на практике реализованы) следующие модели многопоточности:

Модель много / один (many-to-one) – отображение нескольких пользовательских потоков в один и тот же поток ядра. Используется в операционных системах, не поддерживающих множественные системные потоки (например, с целью экономии памяти). Данная модель изображена на рис. 10.2 .

Рис. 10.2. Схема модели многопоточности "много / один".

Модель один / один (one-to-one) – взаимно-однозначное отображение каждого пользовательского потока в определенный поток ядра. Примеры ОС, использующих данную модель, - Windows 95/98/NT/2000/XP/2003/2008/7; OS/2. Данная модель изображена на рис. 10.3 .

Рис. 10.3. Схема модели многопоточности "один / один".

Модель много / много (many-to-many) – модель, допускающая отображение нескольких пользовательских потоков в несколько системных потоков. Такая модель позволяет ОС создавать большое число системных потоков. Характерным примером ОС, использующей подобную модель, является ОС Solaris, а также Windows NT / 2000 / XP / 2003 / 2008 / 7 с пакетом ThreadFiber . Данная модель изображена на рис. 10.4 .

Рис. 10.4. Схема модели многопоточности "много / много".

Проблемы многопоточности

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

Семантика системных вызовов fork() и exec(). Как уже отмечалось, в классической ОС UNIX системный вызов fork создает новый "тяжеловесный" процесс со своим адресным пространством, что значительно "дороже", чем создание потока. Однако, с целью поддержания совместимости программ снизу вверх, приходится сохранять эту семантику, а многопоточность вводить с помощью новых системных вызовов.

Прекращение потоков . Важной проблемой является проблема прекращения потоков: например, если родительский поток прекращается, то должен ли при этом прекращаться дочерний поток? Если прекращается стандартный процесс, создавший несколько потоков, то должны ли прекращаться все его потоки? Ответы на эти вопросы в разных ОС неоднозначны.

Обработка сигналов . Сигналы в UNIX – низкоуровневый механизм обработки ошибочных ситуаций. Примеры сигналов: SIGSEGV - нарушение сегментации (обращение по неверному адресу, чаще всего по нулевому); SIGKILL – сигнал процессу о выполнении команды kill его уничтожения. Пользователь может определить свою процедуру-обработчик сигнала системным вызовом signal . Проблема в следующем: как распространяются сигналы в многопоточных программах и каким потоком они должны обрабатываться? В большинстве случаев этот вопрос решается следующим образом: сигнал обрабатывается потоком, в котором он сгенерирован, и влияет на исполнение только этого потока. В более современных ОС (например, Windows 2000 и более поздних версиях Windows), основанных на объектно-ориентированной методологии, концепция сигнала заменена более высокоуровневой концепцией исключения (exception). Исключение распространяется по стеку потока в порядке, обратном порядку вызовов методов, и обрабатывается первым из них, в котором система находит подходящий обработчик. Аналогичная схема обработки исключений реализована в Java и в.NET.

Группы потоков . В сложных задачах, например, задачах моделирования, при числе разнородных потоков, возникает потребность в их структурировании и помощью концепции группы потоков – совокупности потоков, имеющей свое собственное имя, над потоками которой определены групповые операции. Наиболее удачно, с нашей точки зрения, группы потоков реализованы в Java (с помощью класса ThreadGroup ). Следует отметить также эффективную реализацию пулов потоков (ThreadPool) в.NET.

Локальные данные потока (thread-local storage - TLS) – данные, принадлежащие только определенному потоку и используемые только этим потоком. Необходимость в таких данных очевидна, так как многопоточность – весьма важный метод распараллеливания решения большой задачи, при котором каждый поток работает над решением порученной ему части. Все современные операционные системы и платформы разработки программ поддерживают концепцию локальных данных потока.

Синхронизация потоков . Поскольку потоки, как и процессы (см. ) могут использовать общие ресурсы и реагировать на общие события, необходимы средства их синхронизации. Эти средства подробно рассмотрены позже в данном курсе.

Тупики (deadlocks) и их предотвращение . Как и процессы (см. "Методы взаимодействия процессов " ), потоки могут взаимно блокировать друг друга (т.е. может создаться ситуация deadlock ), при их неаккуратном программировании. Меры по борьбе с тупиками подробно рассмотрены позже в данном курсе.

Потоки POSIX (Pthreads)

В качестве конкретной модели многопоточности рассмотрим потоки POSIX (напомним, что данная аббревиатура расшифровывается как Portable Operating Systems Interface of uniX kind – стандарты для переносимых ОС типа UNIX). Многопоточность в POSIX специфицирована стандартом IEEE 1003.1c, который описывает API для создания и синхронизации потоков. Отметим, что POSIX-стандарт API определяет лишь требуемое поведение библиотеки потоков. Реализация потоков оставляется на усмотрение авторов конкретной POSIX-совместимой библиотеки. POSIX-потоки распространены в ОС типа UNIX, а также поддержаны, с целью совместимости программ, во многих других ОС, например, Solaris и Windows NT.

Стандарт POSIX определяет два основных типа данных для потоков: pthread_t – дескриптор потока ; pthread_attr_t – набор атрибутов потока.

Стандарт POSIX специфицирует следующий набор функций для управления потоками:

    pthread_create(): создание потока

    pthread_exit(): завершение потока (должна вызываться функцией потока при завершении)

    pthread_cancel(): отмена потока

    pthread_join(): заблокировать выполнение потока до прекращения другого потока, указанного в вызове функции

    pthread_detach(): освободить ресурсы занимаемые потоком (если поток выполняется, то освобождение ресурсов произойдёт после его завершения)

    pthread_attr_init(): инициализировать структуру атрибутов потока

    pthread_attr_setdetachstate(): указать системе, что после завершения потока она может автоматически освободить ресурсы, занимаемые потоком

    pthread_attr_destroy(): освободить память от структуры атрибутов потока (уничтожить дескриптор).

Имеются следующие примитивы синхронизации POSIX-потоков с помощью мюьтексов (mutexes) – аналогов семафоров – и условных переменных (conditional variables) – оба эти типа объектов для синхронизации подробно рассмотрены позже в данном курсе:

    Pthread_mutex_init() – создание мюьтекса;

    Pthread_mutex_destroy() – уничтожение мьютекса;

    Pthread_mutex_lock() – закрытие мьютекса;

    Pthread_mutex_trylock() – пробное закрытие мьютекса (если он уже закрыт, вызов игнорируется, и поток не блокируется);

    Pthread_mutex_unlock() – открытие мьютекса;

    Pthread_cond_init() – создание условной переменной;

    Pthread_cond_signal() – разблокировка условной переменной;

    Pthread_cond_wait() – ожидание по условной переменной.

Рассмотрим пример использования POSIX-потоков на языке Си.

#include

#include

#include

#include

static void wait_thread(void)

time_t start_time = time(NULL);

while (time(NULL) == start_time)

// никаких действий, кроме занятия процессора на время до 1 с.

static void *thread_func(void *vptr_args)

for (i = 0; i < 20; i++) {

fputs(" b\n", stderr);

pthread_t thread;

if (pthread_create(&thread, NULL, thread_func, NULL) != 0) {

return EXIT_FAILURE;

for (i = 0; i < 20; i++) {

if (pthread_join(thread, NULL) != 0) {

return EXIT_FAILURE;

return EXIT_SUCCESS;

Пример иллюстрирует параллельное выполнение основного потока, выдающего в стандартный вывод последовательность букв "a", и дочернего потока, выдающего в стандартный поток ошибок (stderr) последовательность букв "b". Обратите внимание на особенности создания потока (pthread_create), указания его тела (исполняемой процедуры потока thread_func) и ожидания завершения дочернего потока (pthread_join).

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

В ОС Solaris, как уже было отмечено, используется модель потоков много / много . Кроме того, в системе используется также уже известное нам понятие облегченный процесс (lightweight process промежуточное между концепцией пользовательского потока и системного потока. Таким образом, в ОС Solaris каждый пользовательский поток отображается в свой облегченный процесс, который, в свою очередь, отображается в поток ядра; последний может исполняться на любом процессоре (или ядре процессора) компьютерной системы. Схема организации потоков в Solaris изображена на рис. 10.5 .

Рис. 10.5. Потоки в Solaris.

На рис. 10.6 изображена схема организации процесса в ОС Solaris.

Рис. 10.6. Процессы в Solaris.

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

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

Вступление

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

Вы можете захотеть загрузить файлы из необычного места, такого как сжатый/зашифрованный архив, или, например, из удаленной сетевой папки. Для этих особых ситуаций, SFML предоставляет третью функцию: loadFromStream . Эта функция считывает данные, используя абстрактный интерфейс sf::InputStream , который позволяет вам иметь собственную реализацию класса потока, который будет работать с SFML.

В этой статье будет рассказано, как писать и использовать ваши собственные классы потоков.

Стандартные потоки C++?

Как и многие другие языки, C++ уже содержит класс потока данных: std::istream . По факту, таких классов два: std::istream является только front-end решением, абстрактным интерфейсом к данным, получаемым из std::streambuf .

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

По этой причине SFML предоставляет собственный потоковый класс, который, как мы надеемся, более простой и быстрый, нежели перечисленные выше.