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