Дипломные работы, курсовые проекты на заказ, контрольные работы на заказ

 

Начертательная геометрия Практикум по решению задач Геометрическое черчение Инженерная графика ЕСКД Кратные интегралы Математический анализ Матрицы Пределы Производные Векторная алгебра Интегральное исчисление ТФКП Ядерная физика Электростатика Магнетизм Оптика Информационные технологии

Теория операционных систем начало

 

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

 Ты выбежалза угол купить вина
Ты вернулся, а вместо дома стена.
Б. Гребенщиков

На палубу вышел, и палубы нет
В глазах у него помутилось.
В. Пелевин

В предыдущей главе мы видели, что даже в современном однопроцессорном персональном компьютере происходит множество параллельных процессов: звуковая карта играет, жесткий диск и сетевой интерфейс передают данные, пользователь двигает мышью — работа кипит! А что начнется, если пользователь запустит задание на печать, так и просто страшно подумать. Написание программ, способных работать в среде с множеством параллельно происходящих процессов, представляет собой нетривиальную задачу. На первый взгляд, сложности здесь никакой нет — аппаратура предоставляет нам механизм прерываний. Обработал прерывание — и наступило счастье. В действительности, никакого счастья от одной только обработки прерывания не наступит, пока мы не сообщим о происшедшем событии основному потоку программы, заинтересованной в этом событии.
Основной поток программы и реализуемые этой программой обработчики прерываний должны взаимодействовать и разделять те или иные данные. При этом в обработчике прерывания мы не всегда можем точно выяснить, в какой точке основной поток программы был прерван (в принципе, можно проанализировать сохраненный счетчик команд и, возможно, локальные переменные основного потока, но это очень сложно и само по себе вряд ли приблизит нас к реализации корректно взаимодействующих потоков), а основной поток не всегда может знать, в какой момент происходило (и происходило ли) прерывание.
Большинство практически применяемых структур данных должны соответствовать тем или иным предположениям, критериям целостности. Например, в упорядоченном массиве каждый следующий элемент должен быть больще (то, что в данном конкретном случае подразумевается под "больше", называется критерием или условием сортировки) предыдущего или равен ему основной способ модификации упорядоченного массива — это вставка в него дополнительного элемента. Вставка в такой массив может быть осуществлена различными способами, например, добавлением нового элемента в конец и выполнением сортировки методом "пузырька", или поиском места, куда элемент должен быть вставлен, и перемещением элементов с большими индексами.
Важно, что любой способ вставки происходит не мгновенно, и все время работы этой процедуры массив не является упорядоченным. Если вставка происходила в основном потоке программы, обработчик прерывания, который в это время попытается работать с массивом, как с упорядоченным — например, произвести в нем дихотомический поиск — будет жестоко разочарован.
Задача разработки программы, взаимодействующей с обработчиком прерывания, таким образом, может быть переформулирована как написание программы, некоторые переменные которой подвержены изменению в непредсказуемые моменты времени.
Это обстоятельство резко усложняет анализ алгоритмов (в частности, доказательство корректности программ) и доставило в свое время много волнений теоретикам программирования. Например, в [Дейкстра 1978] один из основателей структурного программирования, Э. Дейкстра, очень эмоционально описывает свою реакцию при первом столкновении с системой, использующей прерывания. Кроме теоретических сложностей, разработка таких программ сопряжена и со сложностями практическими.
При разработке параллельной программы мы можем неявно сделать и использовать при кодировании предположение, что состояние некоторого объекта в некоторый период времени не меняется — а оно может измениться. Если такая ошибка сделана в последовательно исполняющейся программе, она может быть выявлена при первом же тестовом прогоне. Для выявления же ее в программе с асинхронно исполняющимися модулями потребуется гораздо больше тестовых запусков, при которых мы должны вызывать прерывание в различные моменты времени.
Для исчерпывающего тестирования необходимо перебрать все возможные относительные моменты вызова прерывания, т. е. обеспечить хотя бы раз вызов прерывания после каждой из команд в каждой из возможных последовательностей исполнения основной программы. Стоимость такого тестирования запретительно высока, поэтому ошибки такого рода (в англоязычной литературе они называются race condition (дословно — ошибка соревнования), хорошего же русского термина автору неизвестно) практически невозможно искоренить в процессе тестирования.
Таким образом, единственный способ избежать ошибок соревнования — это не делать их. Для того чтобы не делать ошибок, нужна формальная методика разработки и кодирования параллельно исполняющихся программ. Понятно, что и наличие методики не может полностью исключить ошибки. Однако, если выработанная методика адекватна, каждая ошибка будет ее нарушением, поэтому ошибки могут выявляться анализом кода на соответствие формальным требованиям.
К счастью, автору этой книги нет необходимости заниматься разработкой такой методики с чистого листа. Достаточно лишь, по возможности связно, изложить уже изобретенные методы. Не буду утруждать себя и читателя полным доказательством корректности предлагаемых методик, приводя лишь "интуитивное" обоснование их применимости. Сомневающимся могу предложить либо разработать полное доказательство самостоятельно, либо обратиться к специальной литературе, например [Хоар 1989].
Приведенная ранее формулировка задачи справедлива не только для взаимодействия основного потока программы с обработчиком прерывания, но и для взаимодействия программ, исполняющихся на различных процессорах, а также для программы, непосредственно взаимодействующей с внешними событиями, например посредством опроса. В разд. Вытесняющая многозадачность мы увидим, что [псевдо]параллельные нити исполнения, не являющиеся обработчиками прерываний, довольно легко можно реализовать на однопроцессорной машине, и практически все современные ОС предоставляют такой сервис.
Большинство концепций, обсуждаемых в этой главе, приложимы ко всем перечисленным случаям, поэтому далее в тексте мы будем говорить не о программе и обработчике прерывания, а о двух или более потоках или нитях исполнения. В действительности, одна из взаимодействующих "нитей" может не быть процессом исполнения программы, а представлять собой физический процесс (например, перемотку ленты, перемещение считывающей головки дисковода, или химическую или даже ядерную реакцию в установке, которой управляет наш компьютер) или процесс, происходящий в голове или других модулях нервной системы пользователя-человека, но в большинстве случаев нас эта тонкость не интересует.