Потоки, подобно процессам, характеризуются состояниями выполнения; кроме того, они могут быть синхронизированы друг с другом. Рассмотрим по очереди эти два аспекта.
Состояния потоков
Основными состояниями потоков, как и процессов, являются: состояние выполнения потока, состояние готовности и состояние блокировки. Вообще говоря, состояние приостановки нет смысла связывать с потоками, потому что такие состояния логичнее рассматривать на уровне процессов. В частности, если процесс приостанавливается, обязательно приостанавливаются все его потоки, потому что все они совместно используют адресное пространство этого процесса.
С изменением состояния потоков связаны такие четыре основных действия [ANDE97].
Порождение. Обычно во время создания нового процесса вместе с ним создается поток этого процесса. Далее, в рамках одного и того же процесса один поток может породить другой поток, определив его указатель команд и аргументы. Новый поток создается со своим собственным контекстом регистров и стековым пространством, после чего он помещается в очередь готовых к выполнению потоков.
Блокирование. Если потоку нужно подождать, пока не наступит некоторое событие, он блокируется (при этом сохраняется содержимое его пользовательских регистров, счетчика команд, а также указатели стеков). После этого процессор может перейти к выполнению другого готового потока.
Разблокирование. Когда наступает событие, ожидание которого блокировало поток, последний переходит в состояние готовности.
Завершение. После завершения потока его контекст регистров и стеки удаляются.
Важно понять, должно ли блокирование потока обязательно приводить к блокированию всего процесса. Другими словами, могут ли выполняться какие-нибудь готовые к выполнению потоки процесса, если один из его потоков блокирован? Ясно, что если блокировка одного из потоков будет приводить к блокировке всего процесса, то это существенно уменьшит гибкость и эффективность потоков.
Мы еще вернемся к обсуждению этого вопроса при сравнении потоков на пользовательском уровне и потоков на уровне ядра, а пока что рассмотрим выигрыш в производительности при использовании потоков, которые не блокируют весь процесс. На рис. 4.3 (из [KLEI96]) показана программа, выполняющая два вызова удаленных процедур (remote procedure call — RPC)2 на двух разных узлах, чтобы получить результат после их совместного выполнения. В однопоточной программе результаты получаются последовательно, поэтому программа должна ожидать, пока от каждого сервера по очереди будет получен ответ. Переписав программу так, чтобы для каждого вызова удаленной процедуры она использовала отдельный поток, можно получить существенный выигрыш в скорости. Заметим, что если такая программа работает на однопроцессорной машине, то запросы будут генерироваться последовательно; результаты тоже будут получены последовательно, однако программа будет ожидать двух ответов одновременно.
Рис. 4.3. Удаленный вызов процедуры (RPC), в котором используются потоки
В однопроцессорных системах многозадачность позволяет чередовать различные потоки нескольких процессов. В примере, показанном на рис. 4.4, чередуются три потока, принадлежащие двум процессам. Передача управления от одного процесса другому происходит либо тогда, когда блокируется выполняющийся поток, либо когда заканчивается интервал времени, отведенный для его выполнения. В этом примере поток С начинает выполняться, после того как оканчивается интервал времени, отведенный потоку А, несмотря на то что поток В находится в состоянии готовности. Выбор между потоками В и С — это вопрос планирования; данная тема исследуется в четвертой части книги.
Рис. 4.4. Пример многопоточности в однопроцессорной системе
Синхронизация потоков
Все потоки процесса используют одно и то же адресное пространство, как и другие ресурсы, например открытые файлы. Любое изменение какого-нибудь ресурса одним из потоков процесса оказывает влияние на другие потоки этого же процесса. Поэтому действия различных потоков необходимо синхронизировать, чтобы они не мешали друг другу или чтобы не повредили структуры данных. Например, если каждый из двух потоков будет пытаться добавить свой элемент в двунаправленный список, может быть потерян один из элементов (или нарушена целостность списка).
При рассмотрении синхронизации потоков возникают те же вопросы и используются те же методы, что и при синхронизации процессов. Эти вопросы и методы обсуждаются в главе 5, "Параллельные вычисления: взаимоисключения и многозадачность", и главе 6, "Взаимоблокировка и голодание".
Пример: Adobe PageMaker
Рассмотрим использование потоков на примере приложения Adobe PageMaker, работающего под управлением операционной системы Linux. Программа PageMaker является настольным издательским средством, предназначенным для создания и форматирования документов. Для оптимизации скорости отклика этого приложения была выбрана потоковая структура, показанная на рис. 4.5 [KRON90]. Три потока активны всегда — поток, отвечающий за обработку событий, поток, обновляющий экран, и служебный поток.
Операции, на которые требуется много времени (печать, импорт данных и заливка), выполняются в служебном потоке программы PageMaker, чтобы не блокировать этими операциями возможность обработки поступающих сообщений. В этом же потоке происходит большая часть инициализации программы, что позволяет избежать простоя, который мог бы возникнуть при создании нового документа или открытии уже существующего. Отдельный поток служит для обработки сообщений о новых событиях.
Рис. 4.5. Потоковая структура программы Adobe PageMaker
Синхронизация служебного потока и потока обработки событий является непростой задачей. Пользователь может продолжать набирать текст или работать с мышью, активизируя тем самым поток обработки событий, в то время как служебный поток будет все еще занят. При возникновении подобного конфликта PageMaker фильтрует сообщения и воспринимает только основные из них, такие, как запрос на изменение размера окна.
О завершении выполнения задания свидетельствует сообщение, поступающее из служебного потока. Пока это сообщение не будет получено, возможности пользователя в программе PageMaker ограничены. Об этом свидетельствует отключение пунктов меню и специальный вид курсора. Пользователь может активизировать окна других приложений; когда курсор перемещается в другое окно, он приобретает вид, соответствующий приложению этого окна.
Для обновления экрана используется отдельный поток, что вызвано следующими причинами.
- В программе PageMaker количество объектов на странице не ограничено, и поэтому обработка запроса на обновление экрана может оказаться весьма длительной.
- Использование отдельного потока позволяет пользователю в любой момент остановить вывод изображения на экран. При такой методике, например, может немедленно выполняться команда изменения масштаба. Если бы программа должна была сначала закончить вывод страницы в старом масштабе, а затем полностью вывести ее в новом масштабе, это резко увеличило бы время ее отклика.
Возможна также динамическая прокрутка, т.е. обновление экрана при перетаскивании пользователем ползунка прокрутки. Поток обработки событий отслеживает положение ползунка и перерисовывает размещенные вдоль полей линейки (которые перерисовываются очень быстро, позволяя пользователю сориентироваться в текущем положении документа). В это время поток обновления экрана постоянно пытается перерисовать смещающуюся страницу, отслеживая изменение ее положения.
Реализация динамического обновления экрана без использования потоков привела бы к перегрузке приложения, так как ему приходилось бы согласовывать свои действия в разных частях кода с помощью обмена сообщениями. Многопоточность позволяет более естественно реализовать код, в котором предполагается параллельное выполнение различных действий.