Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Стили и методы программирования. Учебное пособие для СПО

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
теоретическом, но плохо поддерживаемом современными системами программирования) присваивания можно было бы вообще изгнать, сделав переход старого значения реального параметра цикла к новому неявным, так же, как это сделано для формального, скажем, в языке Pascal.
И, наконец, необходимо заметить следующее.
Внимание!
Призраки и инварианты необходимо осознавать до начала написания текста работающей программы. Если Вы оставите это на потом, то наверняка спутаете подпорки, которые займут у Вас основную часть мысленных ресурсов на завершающем этапе реализации, с сущностями. Более того, восстановить обоснование по тексту уже готовой программы, как ни парадоксально, обычно труднее, чем построить его заранее (то, что восстановить его во всяком случае не легче, было обосновано даже теоретически; смотри последнюю главу книги [20]
).
Переходы и выдаваемые значения
В общее употребление структурное программирование вошло после популяризировавшей его работы Э. Дейкстры, в которой, к сожалению, не было даже намека на его ограничения. Ограничения структурного программирования вытекают как из самой его сути, так и из теоремы Бема-Джакопини. Применение структурных переходов, которые ввел в практику и теорию Д. Кнут (откопавший оригинальную работу Бема­Джакопини и четко выделивший ограничения дейкстровского
структурного подхода
13)
), избавляет от многих недостатков, присущих методике Дейкстры. Структурные переходы - переходы лишь вперед и на более высокий уровень структурной иерархии управления, ни в каком случае не выводящие нас за пределы данного модуля.
/* Примеры структурных goto.*/ . . . do {y = f( x ,y)};
if (y>0) break; x=g(x,y); }while (x>0);
Стили и методы программированияНепейвода Н.Н.
241
. . . {. . . { if (All_Done)goto Success; } . . . Success: Hurra;}
Пример 14.5.1.
Структурные переходы в настоящее время также включаются в общераспространенные языки программирования. Их использование понемногу проникает и в учебные курсы.
Структурные переходы являются паллиативом. Они возникли из-за необходимости выразить мысль о том, что успех либо неудача глобального процесса может выявиться внутри одной из решаемых подзадач, и дальнейшая работа и над текущей задачей, и над всей последовательностью вложенных подзадач становится просто бессмысленной. В этом случае нужны даже не переходы, а операторы завершения. Но они во многих распространенных языках действуют лишь на один уровень иерархии вверх, а даже теоретически этого
недостаточно
14)
. Стоит заметить, что между идеей и ее корректной реализацией часто проходят долгие годы. Ныне в общераспространенном языке Java завершители наконец-то более или менее корректно реализованы.
Есть одно ограничение структурных переходов, известное с 80-х гг. ХХ века (cм. [20]
), по крайней мере один раз достоверно повредившее создателям отечественной серии машин Эльбрус, в которых на аппаратном уровне поддерживалось структурное и функциональное программирование. Структурные переходы (в том числе и завершители) некорректны, когда они выводят нас из функции-параметра вызова другой функции. Ирония в том, что абсолютно четкая и полная реализация завершителей еще до осознания необходимости данного средства и тем более задолго до осознания пределов его применимости была проделана именно там, где они в общем случае некорректны (в языке LISP). В нем, как мы видели, процедуры являются полноправными значениями, могут быть параметрами и результатами
Стили и методы программированияНепейвода Н.Н.
242
других процедур. Самый очевидный случай некорректности (правда, вылавливаемый системой обнаружения динамических ошибок Common Lisp), когда мы внутри созданной процедуры завершаем блок, существовавший в момент создания процедуры, но переставший существовать в тот момент, когда ее вызвали. Наверняка многие наталкивались на непонятное поведение программ с завершителями, но в соответствии с общераспространенным "позитивным" мышлением не обращали внимания на данный феномен.
Есть еще одна, на первый взгляд исключительно привлекательная, возможность. В обычных языках программирования часто раздражает то, что значение, выработанное один раз, не удается сразу же использовать в следующем операторе, и, более того, по какой-то очевидной глупости совершенно безвредное и использованное еще в Algol 60 понятие условного выражения, подобного
if x=0 then sin(y) else tan(y)
оказалось выброшено из некоторых распространенных языков (может быть, потому, что в данном случае часть else обязательна). В том же языке LISP, где была (достаточно уникальный случай в программировании) создана четкая и достаточно замкнутая система концепций (ни одной неувязки, которую можно было диагностировать при тогдашнем состоянии теории и практики, найти в нем не удалось), каждый оператор выдает значение, которое может быть использовано объемлющим оператором.
Эту концепцию попытались перенести на язык традиционного типа в языке Алгол 68. На первый взгляд получилось очень красиво. Например, оператор
if x>0 then a1 else a2 fi [if y>0 then j else i fi]:= if z>0 then sin(u) else tan(u) fi
намного компактней и красивей последовательности условных операторов, которую придется написать в Pascal или C++. Но концепция вырабатываемого значения оказалась в концептуальном противоречии и с оператором присваивания, и с совместными вычислениями. Например, согласно семантике Алгола 68, следующая запись
Стили и методы программированияНепейвода Н.Н.
243
(x:=3, у:=x+y, z:=x+y)
является блоком программы, в котором все три присваивания исполняются совместно. Но очевидно, что в данном случае результат вычислений неоднозначен. Одновременно, поскольку значения не забываются, эта же конструкция является изображением массива из трех элементов либо записи с тремя полями.
Следовательно, если распространить систему выдаваемых значений на все операторы структурного программирования, то появляются формально допустимые и соблазнительные для хакерских трюков неустойчивые выражения. Выражение, изменяющее свой контекст (в частности, присваивание), выдавать значение не должно, чтобы не было соблазна поместить его в окружение, где оно будет вызывать трудно диагностируемую неоднозначность.
1)
Если нечто в данный момент используется практически монопольно, то из этого не следует, что оно будет занимать столь же исключительное положение и в будущем и что оно окажется лучшим средством для решения Ваших конкретных задач.
2)
На самом деле традиционным вычислительным программам соответствует не классическая, а интуиционистская логика, но структура доказательств и большинство правил вывода в них совпадают.
3)
В этой системе требований без труда распознается так называемая блочная структура языков программирования, появившаяся еще в Algol 60 и ставшая в настоящее время фактическим стандартом.
4)
Кстати, программистам стоит почаще вспоминать, что, с точки зрения пользователя или заказчика, их внутрипрограммные объекты как раз являются такими же призраками, которые не имеют никакого отношения к реальной действительности. Так что при переходе от уровня рассмотрения программы самой по себе к уровню программы как части архитектуры реальной человеко-машинной системы идеальные и реальные объекты порою меняются местами.
5)
Как видим, программа должна составляться после того, как программист хорошенько подумал, а не до того.
6)
Конечно, уже всем в зубах навязли алгоритмы Евклида и факториалы! Но в изложении основ структурного программирования они как раз на месте, поскольку этот стиль создавался для сравнительно небольших (по нынешним масштабам) вычислительных программ.
Стили и методы программированияНепейвода Н.Н.
244
7)
В принципе, если думать об эффективности в смысле используемой памяти либо времени счета, для любой рекурсивной программы можно подобрать более экономную циклическую реализацию. Но эта реализация будет переполнена подпорками, поэтому потребует гораздо больше усилий от программиста и развалится при первой же попытке модификации.
8)
Впрочем, неявно это следует из того, что парой строк выше мы
использовали термин 'множество'.
9)
Для удобства хакеров, что ли?
10)
Прямо переписать в языке Common LISP данную программку в рекурсивную с использованием mapcar не удастся, поскольку реализаторы превратили данный функционал в макрос, для того чтобы в стандартных случаях чуть-чуть выиграть в эффективности (многим так не хочется писать тривиальные вещи!). Переопределив mapcar как функцию, можно реализовать рекурсивное определение указанной выше функции search .
11)
Всегда возникает соблазн сделать свой излюбленный способ действий универсальнее, исправив его замеченные недостатки. Но порою достоинства являются продолжением недостатков, и наоборот. При устранении любого частного недостатка, возникшего из-за неуниверсальности системы, появятся другие, как правило, худшие, хотя, может быть, на первых порах менее заметные.
12)
За исключением мелких.
13)
Так что, применяя теоретический результат, всегда интересуйтесь не только его формулировкой, но и доказательством, желательно в работе авторов.
14)
В связи с этим стоит помянуть добрым словом отечественные разработки в области алгоритмических языков, в частности язык ЯРМО, в котором необходимость многоуровневых средств завершения конструкций была осознана еще в начале 70-х гг.
Стили и методы программированияНепейвода Н.Н.
245
Проблемы, встающие перед параллельным программированием
Параллелизм и совместность. Виды параллелизма. Трудности параллельного программирования. Организация квазипараллелизма.
Параллельное программирование уже около 40 лет является перспективным направлением. Это - вечно новая тема в программировании. Столь затянувшееся младенчество наводит на мысль о том, что не все ладно с этим дитятей. Поэтому здесь мы прежде всего критически разберем основные понятия, которые нужно освоить, и только затем садиться кодировать параллельную программу.
Естественный параллелизм алгоритмов
При рассмотрении стилей программирования выяснилось, что зачастую линейный порядок исполнения операторов программы навязывается ей извне и служит лишь подпоркой, необходимой для реализации в конкретной системе.
Обратимся к классическим примерам, когда вычислительный алгоритм хорошо распараллеливается.
Пример 15.1.1. Умножение матриц - это случай, на котором достаточно широкие массы программистов и заказчиков вычислительных программ осознали потенциальную выгодность распараллеливания. Если разбить матрицу произведения k*m x l*n на подматрицы размера m x n, то для вычисления каждой такой подматрицы достаточно иметь m строк первого сомножителя и n столбцов второго. Разделив вычисления по независимым процессорам, мы можем ценой некоторого дублирования исходных данных значительно быстрее получить результат.
Аналогично распараллеливается задача решения системы линейных уравнений.
Пример 15.1.2. Пусть задача представляется как совокупность нескольких взаимодействующих автоматов (таким образом, в принципе она укладывается в рамки автоматного программирования, но единая
Стили и методы программированияНепейвода Н.Н.
246
система структурируется на подсистемы, и большинство действий работают внутри подсистем). Тогда у нас возникает естественный параллелизм.
Таковы многие игровые задачи, в которых задействовано несколько персонажей, таково же и большинство задач синтаксического разбора.
Уже на этих двух примерах видно, что имеются принципиально различные случаи. В первом примере задачи индифферентны по отношению к тому, как будет вычисляться результат. В таком случае мы используем для распараллеливания свойства конкретного алгоритма вычислений, который имеет множество потенциально подходящих для реализации информационных взаимосвязей в соответствующей сети данных вычислительных структур (последовательная обязательно находится среди них).
Во втором примере параллелизм естественно диктуется самой задачей, поскольку система распадается на подсистемы. Надеюсь, у Вас не вызывает удивления то, что в данном случае естественное с теоретической точки зрения решение оказывается практически почти никогда не реализуемым. Нынешние параллельные системы имеют фиксированное (как правило, небольшое) число процессоров, если же процессоров много, то они завязаны в жесткую структуру. Поэтому задачи с естественным параллелизмом очень редко помещаются в прокрустово ложе такой системы. Гораздо легче запихнуть туда то, что не является параллельным по природе, но может быть распараллелено, поскольку в этом случае структуру реализации алгоритма можно подогнать к требованиям конкретной системы.
Таким образом, основная беда параллелизма состоит в том, что программирование и архитектура машин вступают в концептуальное противоречие.
В жизни каждый из Вас встречался с простейшим видом параллелизма: длительный и относительно автономный процесс печати запускается, как правило, параллельно с выполнением других программ. Случай, когда Вы одновременно запустили несколько программ, как правило, настоящим параллелизмом не является (это рассматривается чуть ниже).
Стили и методы программированияНепейвода Н.Н.
247
Виды параллелизма
Результатом команды считается результат участника, пришедшего последним.
(из правил соревнований по военно-прикладным видам спорта)
- Доктор, а чем я болен?
- Не волнуйтесь, больной, вскрытие покажет.
(русский анекдот)
Тот из видов параллелизма, который возникает при перемножении матриц, естественен для структурного программирования, но на самом деле параллелизмом не является. В структурном программировании данные организованы в сеть, по которой движется программа. Эта сеть является частичным порядком, и если в сети можно выделить несколько подсетей, слабо связанных друг с другом и проходящих через некоторый сегмент исходной сети с начала до конца, то эти подсети можно исполнять на независимых вычислителях, которые в критические моменты времени взаимодействуют между собой. Обычно такое разбиение сети данных не единственно, и поэтому можно один и тотже алгоритм исполнять на параллельных системах разной архитектуры.
Пример 15.2.1. Пусть сеть данных имеет следующий вид (рис. 15.1
)
Рис. 15.1. Развилка
Тогда ее естественно исполнять на одно-, двух- либо трехпроцессорной машине. В случае многопроцессорной машины в начале и на
Стили и методы программированияНепейвода Н.Н.
248
заключительном этапе вычисление идет последовательно, а три ветви распределяются между процессорами. В частности, если две боковые ветви занимают меньше времени счета, чем главная, можно на двухпроцессорной машине распределить их обе на один и тот же процессор.
В распараллеленной программе заключительный этап исполнения начинается лишь после того, как завершатся все три ветви, дающие для него исходные данные. Такой параллелизм, когда для завершения блока параллельных процессов нужно успешно завершить все параллельные ветви, стандартно возникает в вычислительных задачах и носит название &-параллелизма . Насколько успешен будет результат распараллеливания, зависит от равномерности распределения вычислительной нагрузки между ветвями (см. первый эпиграф).
Другой вид параллелизма был впервые осознан на задачах поиска. Например, если нам известно, что сбежавший из зоопарка лев находится в одном из четырех кварталов и никак не может перебраться из одного в другой, для ускорения поиска можно (если у нас есть достаточное количество поисковых команд) послать по команде в каждый из кварталов, и закончить поиск, как только одна из команд найдет беглеца. Таким образом, возникает - параллелизм, когда для успешного завершения параллельного блока достаточно, чтобы к успеху пришел один из запущенных процессов.
Рассмотрим теперь, каковы же общие условия применимости ­параллелизма. Частный пример про льва показывает, что в принципе можно было бы представить "процедуру" поиска беглеца в виде следующего условного предложения:
if
Лев в квартале1 -> Искать в квартале 1, Лев в квартале2 -> Искать в квартале 2, Лев в квартале3 -> Искать в квартале 3, Лев в квартале4 -> Искать в квартале 4
fi
Но беда в том, что для выяснения истинности охраны нужно выполнить охраняемую команду, т.е. найти льва. Таким образом, данный условный оператор может служить лишь призраком.
Стили и методы программированияНепейвода Н.Н.
249
Далее, имеется еще одно неявное условие, которое иллюстрируется вторым эпиграфом. Если исполнение охраняемой команды с неподходящей охраной может навредить (ресурсы-то оно займет, вред ­это нечто более существенное, наподобие лечения согласно неверному диагнозу), то мы скорее всего будем вынуждены производить вскрытие программы после внезапной и мучительной ее кончины.
Таким образом, - параллелизм является подпоркой для эффективной реализации призрачного условного оператора, удовлетворяющего двум требованиям:
1. проверить охрану ничуть не проще, чем выполнить соответствующую охраняемую команду;
2. выполнение команды из неверной альтернативы ничем, кроме растраты ресурсов, повредить не может.
Тогда можно запустить различные варианты параллельно на разных процессорах и посмотреть, какой первый придет к удаче.
В качестве примера задачи, естественно допускающей > ­параллелизм и не являющейся задачей поиска, можно рассмотреть часто возникающую в дифференциальных играх (в частности, в игре "перехватчик и носитель") ситуацию, когда имеется точка выбора. После данной точки предлагается несколько несовместимых способов, а чтобы определить, какая из стратегий приведет к поимке цели, нужно просчитать поведение системы при рассматриваемой стратегии до конца. Тогда, если впереди точка выбора и нужно заранее принять решение, целесообразно запустить сразу несколько процессов моделирования.
Третий вид параллелизма на самом деле параллелизмом не является, и, пожалуй, шире всего распространен в практике программирования. Это квазипараллелизм. Вы встречаетесь с ним в программах, когда порождаете подпроцесс командой типа thread. В этом случае несколько процессов исполняются как один процесс, в котором перемешаны шаги подпроцессов, так что у программиста создается впечатление, что процессы исполняются параллельно. Квазипараллелизм может с внешней стороны выглядеть как любой из двух рассмотренных ранее видов параллелизма. В первом приближении
Стили и методы программированияНепейвода Н.Н.
250