ЗАНЯТИЕ 17. «Потоки»

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

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

Рис. 36. Тексто¬вый процессор, использующий три потока

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

Рис. 37. Многопоточный веб-сервер

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

Рис. 38. Приблизительный набросок кода для модели, изображенной на рис. 37 а — для потока-диспетчера; б — для рабочего потока

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

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

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

При такой конструкции модель «последовательного про­цесса», присутствующая в первых двух случаях, уже не работает. Состояние вычисления должно быть явным образом сохранено и восстановлено из таблицы при каждом переключении сервера с об­работки одного запроса на обработку другого. В результате потоки и их стеки имитируются более сложным образом. Подобная кон­струкция, в которой у каждого вычисления есть сохраняемое состо­яние и имеется некоторый набор событий, которые могут происхо­дить с целью изменения состояния, называются машиной с конеч­ным числом состояний (finite-state machine), или конечным автома­том. Это понятие получило в вычислительной технике весьма ши­рокое распространение.

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

Таблица 5. Три способа создания сервера

Модель Характеристики
Потоки Параллельная работа, блокирующие си­стемные вызовы
Однопоточный процесс Отсутствие параллельной работы, блоки­рующие системные вызовы
Машина с конечным чис­лом состояний Параллельная работа, неблокирующие си­стемные вызовы, прерывания

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

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

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

avatar
  Подписаться  
Уведомление о