15.1. Классическая модель потоков
Разобравшись в пользе потоков и в порядке их использова¬ния, давайте рассмотрим их применение более пристально. Модель процесса основана на двух независимых понятиях: группировке ресурсов и выполнении. Иногда их полезно отделить друг от друга, и тут на первый план выходят потоки. Сначала будет рассмотрена классическая модель потоков, затем изучена модель потоков, ис¬пользуемая в Linux, которая размывает грань между процессами и потоками.
Согласно одному из взглядов на процесс, он является спо¬собом группировки в единое целое взаимосвязанных ресурсов. У процесса есть адресное пространство, содержащее текст программы и данные, а также другие ресурсы. Эти ресурсы могут включать открытые файлы, необработанные аварийные сигналы, обработ¬чики сигналов, учетную информацию и т. д. Управление этими ре¬сурсами можно значительно облегчить, если собрать их воедино в виде процесса.
Другое присущее процессу понятие — поток выполнения — обычно сокращается до слова поток. У потока есть счетчик команд, отслеживающий, какую очередную инструкцию нужно выполнять. У него есть регистры, в которых содержатся текущие рабочие пе-ременные. У него есть стек с протоколом выполнения, содержащим по одному фрейму для каждой вызванной, но еще не возвратившей управление процедуры. Хотя поток может быть выполнен в рамках какого-нибудь процесса, сам поток и его процесс являются раз¬ными понятиями и должны рассматриваться по отдельности. Про¬цессы используются для группировки ресурсов в единое образова¬ние, а потоки являются «сущностью», распределяемой для выпол¬нения на центральном процессоре.
Потоки добавляют к модели процесса возможность реали¬зации нескольких в значительной степени независимых друг от друга выполняемых задач в единой среде процесса. Наличие не¬скольких потоков, выполняемых параллельно в рамках одного про-цесса, является аналогией наличия нескольких процессов, выполня¬емых параллельно на одном компьютере. В первом случае потоки используют единое адресное пространство и другие ресурсы. А в последнем случае процессы используют общую физическую па¬мять, диски, принтеры и другие ресурсы. Поскольку потоки обла¬дают некоторыми свойствами процессов, их иногда называют об¬легченными процессами. Термин «многопоточный режим» также используется для описания ситуации, при которой допускается ра¬бота нескольких потоков в одном и том же процессе. Выше было показано, что некоторые центральные процессоры обладают непо¬средственной аппаратной поддержкой многопоточного режима и проводят переключение потоков за наносекунды.
На рис. 39, а показаны три традиционных процесса. У каж¬дого из них имеется собственное адресное пространство и един¬ственный поток управления. В отличие от этого, на рис. 39, б пока¬зан один процесс, имеющий три потока управления. Хотя в обоих случаях у нас имеется три потока, на рис. 39, а каждый из них рабо¬тает в собственном адресном пространстве, а на рис. 39, б все три потока используют общее адресное пространство.

Когда многопоточный процесс выполняется на однопроцес¬сорной системе, потоки выполняются, сменяя друг друга. На рис. 32 мы видели работу процессов в многозадачном режиме. За счет переключения между несколькими процессами система создавала иллюзию параллельно работающих отдельных последовательных процессов. Многопоточный режим осуществляется аналогичным способом. Центральный процессор быстро переключается между потоками, создавая иллюзию, что потоки выполняются парал¬лельно, пусть даже на более медленном центральном процессоре, чем реально используемый. При наличии в одном процессе трех потоков, ограниченных по скорости вычисления, будет казаться, что потоки выполняются параллельно и каждый из них выполня¬ется на центральном процессоре, имеющем скорость, которая со¬ставляет одну треть от скорости реального процессора.
Различные потоки в процессе не обладают той независимо¬стью, которая есть у различных процессов. У всех потоков абсо¬лютно одно и то же адресное пространство, а значит, они так же совместно используют одни и те же глобальные переменные. По¬скольку каждый поток может иметь доступ к любому адресу па¬мяти в пределах адресного пространства процесса, один поток мо¬жет считывать данные из стека другого потока, записывать туда свои данные и даже стирать оттуда данные. Защита между пото¬ками отсутствует, потому что ее невозможно осуществить и в ней нет необходимости. В отличие от различных процессов, которые могут принадлежать различным пользователям и которые могут враждовать друг с другом, один процесс всегда принадлежит од¬ному и тому же пользователю, который, по-видимому, и создал не¬сколько потоков для их совместной работы, а не для вражды. В до¬полнение к использованию общего адресного пространства все по¬токи, как показано в табл. 6, могут совместно использовать одни и те же открытые файлы, дочерние процессы, ожидаемые и обычные сигналы и т. п. Поэтому структура, показанная на рис. 39, а, может использоваться, когда все три процесса фактически не зависят друг от друга, а структура, показанная на рис. 39, б, может применяться, когда три потока фактически являются частью одного и того же задания и активно и тесно сотрудничают друг с другом.
Элементы в первом столбце относятся к свойствам про¬цесса, а не потоков. Например, если один из потоков открывает файл, этот файл становится видимым в других потоках, принадле¬жащих процессу, и они могут производить с этим файлом операции чтения-записи. Это вполне логично, поскольку именно процесс, а не поток является элементом управления ресурсами. Если бы у каждого потока были собственные адресное пространство, откры¬тые файлы, необработанные аварийные сигналы и т. д., то он был бы отдельным процессом. С помощью потоков мы пытаемся до¬стичь возможности выполнения нескольких потоков, использую¬щих набор общих ресурсов с целью тесного сотрудничества при реализации какой-нибудь задачи.
Элементы в первом столбце относятся к свойствам про¬цесса, а не потоков. Например, если один из потоков открывает файл, этот файл становится видимым в других потоках, принадле¬жащих процессу, и они могут производить с этим файлом операции чтения-записи. Это вполне логично, поскольку именно процесс, а не поток является элементом управления ресурсами. Если бы у каждого потока были собственные адресное пространство, откры¬тые файлы, необработанные аварийные сигналы и т. д., то он был бы отдельным процессом. С помощью потоков мы пытаемся до¬стичь возможности выполнения нескольких потоков, использую¬щих набор общих ресурсов с целью тесного сотрудничества при реализации какой-нибудь задачи.
Таблица 6. Использование объектов потоками
| Элементы, присущие каждому процессу | Элементы, присущие каждому потоку |
| Адресное пространство | Счетчик команд |
| Глобальные переменные | Регистры |
| Открытые файлы | Стек |
| Дочерние процессы | Состояние |
| Необработанные аварийные сигналы | |
| Сигналы и обработчики сигналов | |
| Учетная информация |
Подобно традиционному процессу (то есть процессу только с одним потоком), поток должен быть в одном из следующих состояний: выполняемый, заблокированный, готовый или завершенный. Выполняемый поток занимает центральный процессор и является активным в данный момент. В отличие от этого, заблокированный поток ожидает события, которое его разблокирует. Например, когда поток выполняет системный вызов для чтения с клавиатуры, он блокируется до тех пор, пока на ней не будет что-нибудь набрано. Поток может быть заблокирован в ожидании какого-то внешнего события или его разблокировки другим потоком. Готовый поток планируется к выполнению и будет выполнен, как только подойдет его очередь. Переходы между состояниями потока аналогичны переходам между состояниями процесса (см. рис. 33).
Следует учесть, что каждый поток имеет собственный стек (рис. 40). Стек каждого потока содержит по одному фрейму для каждой уже вызванной, но еще не возвратившей управление процедуры. Такой фрейм содержит локальные переменные процедуры и адрес возврата управления по завершении ее вызова. Например, если процедура X вызывает процедуру Y, а Y вызывает процедуру Z, то при выполнении Z в стеке будут фреймы для X, Y и Z. Каждый поток будет, как правило, вызывать различные процедуры и, следовательно, иметь среду выполнения, отличающуюся от среды выполнения других потоков. Поэтому каждому потоку нужен собственный стек.

Когда используется многопоточность, процесс обычно начинается с использования одного потока. Этот поток может со¬здавать новые потоки, вызвав библиотечную процедуру, к примеру thread_create. В параметре thread_create обычно указывается имя процедуры, запускаемой в новом потоке. Нет необходимости (или даже возможности) указывать для нового потока какое-нибудь ад¬ресное пространство, поскольку он автоматически запускается в адресном пространстве создающего потока. Иногда потоки имеют иерархическую структуру, при которой у них устанавливаются вза¬имоотношения между родительскими и дочерними потоками, но чаще всего такие взаимоотношения отсутствуют и все потоки счи-таются равнозначными. Независимо от наличия или отсутствия иерархических взаимоотношений создающий поток обычно воз¬вращает идентификатор потока, который дает имя новому потоку.
Когда поток завершает свою работу, выход из него может быть осуществлен за счет вызова библиотечной процедуры, к при¬меру thread_exit. После этого он прекращает свое существование и больше не фигурирует в работе планировщика. В некоторых ис¬пользующих потоки системах какой-нибудь поток для выполнения выхода может ожидать выхода из какого-нибудь другого (указан¬ного) потока после вызова процедуры, к примеру thread_join. Эта процедура блокирует вызывающий поток до тех пор, пока не будет осуществлен выход из другого (указанного) потока. В этом отно¬шении создание и завершение работы потока очень похожи на со¬здание и завершение работы процесса при использовании примерно одних и тех же параметров.
Другой распространенной процедурой, вызываемой пото¬ком, является thread_yield. Она позволяет потоку добровольно уступить центральный процессор для выполнения другого потока. Важность вызова такой процедуры обусловливается отсутствием прерывания по таймеру, которое есть у процессов и благодаря ко¬торому фактически задается режим многозадачности. Для потоков важно проявлять вежливость и время от времени добровольно уступать центральный процессор, чтобы дать возможность выпол-нения другим потокам. Другие вызываемые процедуры позволяют одному потоку ожидать, пока другой поток не завершит какую-ни¬будь работу, а этому потоку — оповестить о том, что он завершил определенную работу, и т. д.
Хотя потоки зачастую приносят пользу, они вносят в мо¬дель программирования и ряд сложностей. Для начала рассмотрим эффект, возникающий при осуществлении системного вызова fork, принадлежащего ОС UNIX. Если у родительского процесса есть несколько потоков, должны ли они быть у дочернего процесса? Если нет, то процесс может неверно функционировать из-за того, что все они составляют его неотъемлемую часть.
Но если дочерний процесс получает столько же потоков, сколько их было у родительского процесса, что произойдет, если какой-нибудь из потоков родительского процесса был заблокиро¬ван системным вызовом read, используемым, к примеру, для чтения с клавиатуры? Будут ли теперь два потока, в родительском и в до¬чернем процессах, заблокированы на вводе с клавиатуры? Если бу¬дет набрана строка, получат ли оба потока ее копию? Или ее полу¬чит только поток родительского процесса? А может быть, она будет получена только потоком дочернего процесса? Сходные проблемы существуют и при открытых сетевых подключениях.
Другой класс проблем связан с тем, что потоки совместно используют многие структуры данных. Что происходит в том слу¬чае, если один поток закрывает файл в тот момент, когда другой поток еще не считал с него данные? Предположим, что один поток заметил дефицит свободной памяти и приступил к выделению до¬полнительного объема. На полпути происходит переключение по¬токов, и новый поток тоже замечает дефицит свободной памяти и приступает к выделению дополнительного объема. Вполне воз¬можно, что дополнительная память будет выделена дважды. Для решения этих проблем следует приложить ряд усилий, но для кор¬ректной работы многопоточных программ требуется все тщательно продумать и спроектировать.
15.2. Потоки в POSIX
Чтобы предоставить возможность создания переносимых многопоточных программ, в отношении потоков институтом IEEE был определен стандарт IEEE standard 1003.1c. Определенный в нем пакет, касающийся потоков, называется Pthreads. Он поддер¬живается большинством UNIX-систем. В стандарте определено бо¬лее 60 вызовов функций. Опишем ряд самых основных функций, чтобы дать представление о том, как они работают. В табл. 7 пере¬числены все вызовы функций, которые мы будем рассматривать.
Таблица 7. Ряд вызовов функций стандарта Pthreads
Вызовы, связанные с потоком Описание
pthread_create Создание нового потока
pthread_exit Завершение работы вызвавшего потока
pthread_join Ожидание выхода из указанного потока
pthread_yield Освобождение центрального процессора, позволяющее выполняться другому потоку
pthread_attr_init Создание и инициализация структуры атри¬бутов потока
pthread_attr_destroy Удаление структуры атрибутов потока
Все потоки Pthreads имеют определенные свойства. У каж¬дого потока есть свои идентификатор, набор регистров (включая счетчик команд) и набор атрибутов, которые сохраняются в опре¬деленной структуре. Атрибуты включают размер стека, параметры планирования и другие элементы, необходимые при использовании потока.
Новый поток создается с помощью вызова функции pthread_create. В качестве значения функции возвращается иденти¬фикатор только что созданного потока. Этот вызов намеренно сде¬лан очень похожим на системный вызов fork (за исключением па¬раметров), а идентификатор потока играет роль PID, главным обра¬зом для идентификации ссылок на потоки в других вызовах.
Когда поток заканчивает возложенную на него работу, он может быть завершен путем вызова функции pthread_exit. Этот вы¬зов останавливает поток и освобождает пространство, занятое его стеком.
Зачастую потоку необходимо перед продолжением выпол¬нения ожидать окончания работы и выхода из другого потока. Ожидающий поток вызывает функцию pthread_join, чтобы ждать завершения другого указанного потока. В качестве параметра этой функции передается идентификатор потока, чьего завершения сле¬дует ожидать.
Иногда бывает так, что поток не является логически забло¬кированным, но считает, что проработал достаточно долго, и наме¬ревается дать шанс на выполнение другому потоку. Этой цели он может добиться за счет вызова функции pthread_yield. Для процес¬сов подобных вызовов функций не существует, поскольку предпо¬лагается, что процессы сильно конкурируют друг с другом и каж¬дый из них требует как можно больше времени центрального про¬цессора. Но поскольку потоки одного процесса, как правило, пи¬шутся одним и тем же программистом, то он добивается от них, чтобы они давали друг другу шанс на выполнение.
Два следующих вызова функций, связанных с потоками, от¬носятся к атрибутам. Функция pthread_attr_init создает структуру атрибутов, связанную с потоком, и инициализирует ее значениями по умолчанию. Эти значения (например, приоритет) могут быть изменены за счет работы с полями в структуре атрибутов.
И наконец, функция pthread_attr_destroy удаляет структуру атрибутов, принадлежащую потоку, освобождая память, которую она занимала. На поток, который использовал данную структуру, это не влияет, и он продолжает свое существование.
Чтобы лучше понять, как работают функции пакета Pthread, рассмотрим простой пример, показанный в листинге 1. Основная программа этого примера работает в цикле столько раз, сколько указано в константе NUMBER_OF_THREADS (количество пото¬ков), создавая при каждой итерации новый поток и предварительно сообщив о своих намерениях. Если создать поток не удастся, она выводит сообщение об ошибке и выполняет выход. После создания всех потоков осуществляется выход из основной программы.

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

Оставить комментарий