В традиционных операционных системах у каждого про¬цесса есть адресное пространство и единственный поток управле¬ния. Фактически это почти что определение процесса. Тем не менее нередко возникают ситуации, когда неплохо было бы иметь в од¬ном и том же адресном пространстве несколько потоков управле¬ния, выполняемых квазипараллельно, как будто они являются чуть ли не обособленными процессами (за исключением общего адрес¬ного пространства).
14.1. Применение потоков
Зачем нам нужна какая-то разновидность процесса внутри самого процесса? Необходимость в подобных мини-процессах, называемых потоками, обусловливается целым рядом причин. Рас¬смотрим некоторые из них. Основная причина использования пото¬ков заключается в том, что во многих приложениях одновременно происходит несколько действий, часть которых может периодиче¬ски быть заблокированной. Модель программирования упрощается за счет разделения такого приложения на несколько последователь¬ных потоков, выполняемых в квазипараллельном режиме.
Мы уже сталкивались с подобными аргументами. Именно они использовались в поддержку создания процессов. Вместо того чтобы думать о прерываниях, таймерах и контекстных переключа¬телях, мы можем думать о параллельных процессах. Но только те¬перь, рассматривая потоки, мы добавляем новый элемент: возмож¬ность использования параллельными процессами единого адрес¬ного пространства и всех имеющихся данных. Эта возможность играет весьма важную роль для тех приложений, которым не под¬ходит использование нескольких процессов (с их раздельными ад¬ресными пространствами).
Вторым аргументом в пользу потоков является легкость (то есть быстрота) их создания и ликвидации по сравнению с более «тяжеловесными» процессами. Во многих системах создание пото¬ков осуществляется в 10–100 раз быстрее, чем создание процессов. Это свойство особенно пригодится, когда потребуется быстро и динамично изменять количество потоков.
Третий аргумент в пользу потоков также касается произво-дительности. Когда потоки работают в рамках одного центрального процессора, они не приносят никакого прироста производительно¬сти, но когда выполняются значительные вычисления, а также зна¬чительная часть времени тратится на ожидание ввода-вывода, наличие потоков позволяет этим действиям перекрываться по вре¬мени, ускоряя работу приложения.
И наконец, потоки весьма полезны для систем, имеющих несколько центральных процессоров, где есть реальная возмож¬ность параллельных вычислений.
Понять, в чем состоит польза от применения потоков, проще всего на конкретных примерах. Рассмотрим в качестве пер¬вого примера текстовый процессор. Обычно эти программы отоб¬ражают создаваемый документ на экране в том виде, в каком он будет выводиться на печать. В частности, все концы строк и концы страниц находятся именно там, где они в результате и появятся на бумаге, чтобы пользователь мог при необходимости их проверить и подправить (например, убрать на странице начальные и конечные висячие строки, имеющие неэстетичный вид).
Предположим, что пользователь пишет какую-то книгу. С авторской точки зрения проще всего всю книгу иметь в одном файле, облегчая поиск тем, выполнение глобальных замен и т. д. С другой точки зрения каждая глава могла бы быть отдельным фай¬лом. Но если каждый раздел и подраздел будут размещаться в от¬дельных файлах, это принесет массу неудобств, когда понадобится вносить во всю книгу глобальные изменения, поскольку тогда при¬дется отдельно и поочередно редактировать сотни файлов. Напри¬мер, если предложенный стандарт xxxx одобрен непосредственно перед выходом книги в печать, то в последнюю минуту все вхож¬дения «Проект стандарта xxxx» нужно заменить на «Стандарт xxxx». Если вся книга представлена одним файлом, то, как правило, все замены могут быть произведены с помощью одной команды. А если книга разбита на более чем 300 файлов, редактированию дол¬жен быть подвергнут каждый из них.
Теперь представим себе, что происходит, когда пользова¬тель вдруг удаляет одно предложение на первой странице 800-стра¬ничного документа. Теперь, проверив внесенные изменения, он хо¬чет внести еще одну поправку на 600-й странице и набирает ко¬манду, предписывающую текстовому процессору перейти на эту страницу (возможно, посредством поиска фразы, которая только там и встречается). Теперь текстовый процессор вынужден немед¬ленно переформатировать всю книгу вплоть до 600-й страницы, поскольку он не знает, какой будет первая строка на 600-й стра¬нице, пока не обработает всех предыдущие страницы. Перед отоб-ражением 600-й страницы может произойти существенная за¬держка, вызывающая недовольство пользователя.
И здесь на помощь могут прийти потоки. Предположим, что текстовый процессор написан как двухпоточная программа. Один из потоков взаимодействует с пользователем, а другой занимается переформатированием в фоновом режиме. Как только предложение с первой страницы будет удалено, поток, отвечающий за взаимо¬действие с пользователем, приказывает потоку, отвечающему за формат, переформатировать всю книгу. Пока взаимодействующий поток продолжает отслеживать события клавиатуры и мыши, реа¬гируя на простые команды вроде прокрутки первой страницы, вто¬рой поток с большой скоростью выполняет вычисления. Если не¬много повезет, то переформатирование закончится как раз перед тем, как пользователь запросит просмотр 600-й страницы, которая тут же сможет быть отображена.
Ну раз уж начали, то почему бы не добавить и третий по¬ток? Многие текстовые процессоры обладают свойством автомати¬ческого сохранения всего файла на диск каждые несколько минут, чтобы уберечь пользователя от утраты его дневной работы в случае программных или системных сбоев или отключения электропита¬ния. Третий поток может заниматься созданием резервных копий на диске, не мешая первым двум. Ситуация, связанная с примене¬нием трех потоков, показана на рис. 36.

Если бы программа была рассчитана на работу только од¬ного потока, то с начала создания резервной копии на диске и до его завершения игнорировались бы команды с клавиатуры или мыши. Пользователь ощущал бы это как слабую производитель¬ность. Можно было бы сделать так, чтобы события клавиатуры или мыши прерывали создание резервной копии на диске, позволяя до¬стичь более высокой производительности, но это привело бы к сложной модели программирования, основанной на применении прерываний. Программная модель, использующая три потока, го¬раздо проще. Первый поток занят только взаимодействием с поль¬зователем. Второй поток по необходимости занимается переформа¬тированием документа. А третий поток периодически сбрасывает содержимое ОЗУ на диск.
Вполне очевидно, что три отдельных процесса так работать не будут, поскольку с документом необходимо работать всем трем потокам. Три потока вместо трех процессов используют общую па¬мять, таким образом, все они имеют доступ к редактируемому до¬кументу. При использовании трех процессов такое было бы невоз¬можно.
Аналогичная ситуация складывается во многих других ин-терактивных программах. Например, электронная таблица является программой, позволяющей поддерживать матрицу, данные элемен¬тов которой предоставляются пользователем. Остальные элементы вычисляют исходя из введенных данных с использованием потен¬циально сложных формул. Когда пользователь изменяет значение одного элемента, нужно пересчитывать значения многих других элементов. При использовании потоков пересчета, работающих в фоновом режиме, поток, взаимодействующий с пользователем, мо¬жет позволить последнему, пока идут вычисления, вносить допол¬нительные изменения. Подобным образом третий поток может сам по себе периодически сбрасывать на диск резервные копии.
Рассмотрим еще один пример, где могут пригодиться по¬токи: сервер для веб-сайта. Поступают запросы на веб-страницы, и запрошенные страницы отправляются обратно клиентам. На боль¬шинстве веб-сайтов некоторые страницы запрашиваются чаще дру¬гих. Например, главная страница веб-сайта Sony запрашивается намного чаще, чем страница, находящаяся глубже, в ответвлении дерева, содержащем техническое описание какой-нибудь конкрет¬ной видеокамеры. Веб-службы используют это обстоятельство для повышения производительности за счет размещения содержания часто используемых страниц в основной памяти, чтобы исключить необходимость обращаться за ними к диску. Такие подборки назы¬ваются кэшем и используются также во многих других случаях.
Один из способов организации веб-сервера показан на рис. 37. Один из потоков — диспетчер — читает входящие рабочие за¬просы из сети. После анализа запроса он выбирает простаивающий (то есть заблокированный) рабочий поток и передает ему запрос, возможно, путем записи указателя на сообщение в специальное слово, связанное с каждым потоком. Затем диспетчер пробуждает спящий рабочий поток, переводя его из заблокированного состоя¬ния в состояние готовности.

При пробуждении рабочий поток проверяет, может ли за¬прос быть удовлетворен из кэша веб-страниц, к которому имеют доступ все потоки. Если нет, то он, чтобы получить веб-страницу, приступает к операции чтения с диска и блокируется до тех пор, пока не завершится дисковая операция. Когда поток блокируется на дисковой операции, выбирается выполнение другого потока, воз¬можно, диспетчера, с целью получения следующей задачи или, возможно, другого рабочего потока, который находится в готовно¬сти к выполнению.
Эта модель позволяет запрограммировать сервер в виде коллекции последовательных потоков. Программа диспетчера со¬стоит из бесконечного цикла для получения рабочего запроса и пе¬репоручения его рабочему потоку. Код каждого рабочего потока состоит из бесконечного цикла, в котором принимается запрос от диспетчера и веб-кэш проверяется на присутствие в нем страницы. Если страница в кэше, она возвращается клиенту. Если нет, поток получает страницу с диска, возвращает ее клиенту и блокируется в ожидании нового запроса.
Приблизительный набросок кода показан на рис. 38. Здесь, константа TRUE предполагается равной 1. Также buf и page явля¬ются структурами, предназначенными для хранения рабочего за¬проса и веб-страницы соответственно

Рассмотрим, как можно было бы написать код веб-сервера в отсутствие потоков. Можно заставить его работать в виде единого потока. Основной цикл веб-сервера получает запрос, анализирует его и завершает обработку до получения следующего запроса. Ожидая завершения дисковой операции, сервер простаивает и не обрабатывает никаких других входящих запросов. Если веб-сервер запущен на специально выделенной машине, что чаще всего и бывает, то центральный процессор, ожидая завершения дисковой операции, остается без дела. В итоге происходит значительное сокращение количества запросов, обрабатываемых в секунду. Таким образом, потоки существенно повышают производительность, но каждый из них программируется последовательно, то есть обычным способом.
До сих пор мы видели две возможные конструкции: многопоточный и однопоточный веб-серверы. Представьте, что потоки недоступны, а системные программисты считают, что потери производительности при использовании одного потока недопустимы. Если доступна неблокирующая версия системного вызова read, то возможен и третий подход. При поступлении запроса его анализирует один-единственный поток. Если запрос может быть удовлетворен из кэша, то все в порядке, но если нет, то стартует неблокирующая дисковая операция.
Сервер записывает состояние текущего запроса в таблицу, а затем приступает к получению следующего события. Этим событием может быть либо запрос на новую задачу, либо ответ от диска, связанный с предыдущей операцией. Если это новая задача, то процесс приступает к ее выполнению. Если это ответ от диска, то из таблицы выбирается соответствующая информация и происходит обработка ответа. При использовании неблокирующего ввода-вывода ответ, наверное, должен принять форму сигнала или прерывания.
При такой конструкции модель «последовательного процесса», присутствующая в первых двух случаях, уже не работает. Состояние вычисления должно быть явным образом сохранено и восстановлено из таблицы при каждом переключении сервера с обработки одного запроса на обработку другого. В результате потоки и их стеки имитируются более сложным образом. Подобная конструкция, в которой у каждого вычисления есть сохраняемое состояние и имеется некоторый набор событий, которые могут происходить с целью изменения состояния, называются машиной с конечным числом состояний (finite-state machine), или конечным автоматом. Это понятие получило в вычислительной технике весьма широкое распространение.
Теперь, наверное, уже понятно, чем должны быть полезны потоки. Они дают возможность сохранить идею последовательных процессов, которые осуществляют блокирующие системные вызовы (например, для операций дискового ввода-вывода), но при этом позволяют все же добиться распараллеливания работы. Блокирующие системные вызовы упрощают программирование, а параллельная работа повышает производительность. Однопоточные серверы сохраняют простоту блокирующих системных вызовов, но уступают им в производительности. Третий подход позволяет добиться высокой производительности за счет параллельной работы, но использует неблокирующие вызовы и прерывания, усложняя процесс программирования. Сводка моделей приведена в табл. 5.
Таблица 5. Три способа создания сервера
| Модель | Характеристики |
| Потоки | Параллельная работа, блокирующие системные вызовы |
| Однопоточный процесс | Отсутствие параллельной работы, блокирующие системные вызовы |
| Машина с конечным числом состояний | Параллельная работа, неблокирующие системные вызовы, прерывания |
Третьим примером, подтверждающим пользу потоков, являются приложения, предназначенные для обработки очень большого объема данных. При обычном подходе блок данных считывается, после чего обрабатывается, а затем снова записывается. Проблема в том, что при доступности лишь блокирующих вызовов процесс блокируется и при поступлении данных, и при их возвращении. Совершенно ясно, что простой центрального процесса при необходимости в большом объеме вычислений слишком расточителен и его по возможности следует избегать.
Проблема решается с помощью потоков. Структура процесса может включать входной поток, обрабатывающий поток и выходной поток. Входной поток считывает данные во входной буфер. Обрабатывающий поток извлекает данные из входного буфера, обрабатывает их и помещает результат в выходной буфер. Выходной буфер записывает эти результаты обратно на диск. Таким образом, ввод, вывод и обработка данных могут осуществляться одновременно. Разумеется, эта модель работает лишь при том условии, что системный вызов блокирует только вызывающий поток, а не весь процесс.

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