Программно-аппаратные платформы и вычислительные наноструктуры

Особенности использования программных инструментальных платформ параллельных вычислительных систем общего назначения

Разбить на страницы
Показывать лекцию целиком

2.1. Методы и средства представления и поддержки параллелизма

Использование параллельных вычислительных систем сопряжено с целым рядом проблем, связанных с представлением и поддержкой параллелизма, потому что пока не удалось найти формальные методы, способные эффективно вскрывать имеющийся в последовательном алгоритме потенциальный уровень распараллеливания каждой фазы вычислений, а тем более распределять между процессорами вытекающую из него вычислительную нагрузку. Поэтому процесс проектирования параллельных программ и реализующих их систем был и пока остается интерактивным. В таких условиях остается только предоставить программисту эффективные языковые средства и инструментальные платформы, обеспечивающие отладку и загрузку параллельных программ в существующую или создаваемую многопроцессорную систему. Языки параллельного программирования и их инструментальные платформы отталкиваются от архитектурных особенностей поддержки параллелизма в аппаратной платформе и поэтому не могут быть универсальными, так как выразительные и исполнительные средства - две стороны одной медали. Отсюда, либо аппаратная платформа разрабатывается под заранее заданный язык параллельного программирования, как это имеет место в транспьютерах [267], архитектура которых ориентирована на язык высокого уровня ОККАМ, либо для аппаратной платформы пишется узкоспециализированный язык, что характерно для спецпроцессоров. Аналогичная с ОККАМ ситуация складывалась и в случае ПРОЛОГ- и ЛИСП-процессоров, процессоров баз данных и баз знаний, которая была характерна для японской микроэлектроники 80-х годов прошлого столетия [278].

Все инструментальные платформы для параллельных вычислителей можно разбить на две группы: одни ориентированы на использование уже существующих аппаратных платформ, а вторые - на их создание. В первом случае допускается только специфицированная реконфигурация аппаратной платформы под требования вычислительного алгоритма пользователя, что характерно для транспьютерных систем [267], а во втором случае вычислительный алгоритм известен и требуется методами и средствами (полу)заказного проектирования синтезировать аппаратуру его поддержки, что характерно для систолических матриц [70].

Типичная архитектура транспьютера (рис. 2.1) ориентирована на многопроцессорные ВС МКМД-типа с массовыми пересылками данных и управляемыми потоками данных, в которых запуск инструкции осуществляется при наличии обрабатываемых операндов. При этом для выравнивания скоростей обработки и пересылки данных каждый транспьютер, исполненный в виде отдельной СБИС, содержит встроенное ОЗУ, функции которого сходны с функциями кеш-памяти в современных процессорах общего назначения. Объясняется это тем, что внутренняя пересылка ОЗУ - регистр выполняется гораздо быстрее любой внешней пересылки, что характерно для всех без исключения микроэлектронных технологий.

(рис 2.1) Внутренняя архитектура транспьютера IMS T800 и формат инструкции [267]

Ассемблерные инструкции транспьютера рассчитаны на разработку программ, написанных на языке высокого уровня ОККАМ. Как и в RISC -технологиях, для эффективной компиляции программ набор ассемблерных инструкций сокращен до минимума и все они имеют одинаковый формат (см. рис. 2.1), в котором выделено поле для кода функции и для данных. Для увеличения разрядности преобразуемых операндов в языке ОККАМ предусмотрены две префиксные конструкции: prefix и negative prefix, первая из которых сдвигает влево на четыре разряда четырехбитный операнд, загруженный в регистр операнда. Вторая инструкция делает то же самое, но предварительно преобразует содержимое пересылаемых данных в дополнительный код.

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

В одном из первых транспьютеров IMS T800 каждый последовательный процесс поддерживался всего шестью регистрами (рис. 2.2), что упростило логику управления и повысило скорость работы каналов при обмене с внутренним ОЗУ. Три регистра A, B и C реализуют механизм аппаратного стека с "вершиной" в A и являются приемниками и источниками для большинства арифметико-логических операций транспьютера. В трех оставшихся регистрах хранятся соответственно указатель области памяти с локальными переменными, указатель на следующую команду (счетчик команд) и наращиваемый с помощью prefix и negative prefix операнд. Все команды оперируют только с вершиной стека, и поэтому нет необходимости переопределять положение операндов инструкций по ходу вычислений.

(рис 2.2) Связный список процессов [267]

Каждый последовательный процесс, включенный в состав параллельного, может находиться в следующих состояниях:

  • активен: выполняется или находится в очереди на выполнение;
  • пассивен: готов к вводу, готов к выводу, ожидает, когда наступит ука занное время.
  • Микропрограммный планировщик IMS T800 устроен так, что пассивные процессы не занимают процессорное время, а активные, ожидающие процессы находятся в списке рабочих областей процессов, который поддерживается двумя регистрами (см. рис. 2.2): один указывает на первый процесс из списка, а второй - на последний. В нашем случае процесс S исполняется, а процессы P, Q и R активны, но ожидают выполнения.

    Каждый последовательный процесс выполняется до тех пор, пока не перейдет в режим ожидания ввода, вывода или таймера, что как раз и соответствует идеологии управления потоками данных. Как только процесс не может исполняться, его счетчик команд сохраняется в рабочей области и выбирается следующий процесс из списка для выполнения. Как видно из схемы рис. 2.2, "цена" такого прерывания - 12 пересылок, обновляющих содержимое 6 регистров с сохранением предыдущего содержимого в ОЗУ, которое расположено на том же кристалле, то есть можно считать, что в штатном режиме $$\tau_{c}(p) = \tau_{ОЗУ} $$ (см. раздел 1.2).

    Управление потоками данных в параллельных транспьютерных системах поддерживается альтернативными конструкциями языка ОККАМ в составе: enable channel, enable timer, alternative wait, disable channel, disable timer (соответственно допустимый канал (таймер), альтернативное ожидание и вышел из строя канал (таймер)). Механизм реализации следующий. Сначала выполняются инструкции enable channel или enable timer каждой программной компоненты, включенной в список диспетчеризации. Затем с помощью инструкции alternative wait из списка диспетчеризации исключается процесс, если ни один канал или таймер не готов. После этого выполняется инструкция disable channel или disable timer. Процесс возвращается в список диспетчеризации, если любой из обслуживающих его каналов или таймеров станет готовым.

    При работе транспьютера с данными формата плавающей точки в IMS T800 использована традиционная адресная арифметика, когда адрес операнда хранится в стеке центрального процессора, а операнд загружается в стек процессора плавающей точки из адресуемой ячейки памяти (см. рис. 2.1). Формат float и double задаются специальным тегом, что сокращает число инструкций. В частности, вместо двух инструкций типа float add single и float add double достаточно одной доопределяемой тегом инструкции float add.

    В программе, написанной на языке ОККАМ, программист должен в явном виде указать одно из двух ключевых слов: SEQ или PAR, первое из которых является директивой последовательного, а второе - параллельного выполнения нижележащих процессов. Но перед этим программист должен осуществить декомпозицию всей задачи на составляющие процессы, причем сделать это надо так, чтобы равномерно распределить вычислительную нагрузку между транспьютерами сети.

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

    Во всех случаях цикл разработки программ практически одинаков и включает: редактирование текста программы, проверку текста программы на ошибки, компиляцию программных секций, отвечающих последовательным участкам программы, в машинные коды, компоновку отдельно скомпилированных секций кода, конфигурирование программы для ее выполнения в целевой сети, загрузку программы в целевую сеть и выполнение программы.

    Главная особенность технологии конструирования параллельных ОККАМ-программ сосредоточена:

  • в компоновке независимо скомпилированных фрагментов программ;
  • в конфигурировании, в рамках которого программист готовит описание конфигурации, отображающее процессы на процессоры и каналы на реальные линии связи.
  • Компоновка независимо скомпилированных фрагментов программ удобна тем, что позволяет оперативно вносить дополнения и изменения в проект без его полной перекомпиляции.

    Конфигурирование отдельных секций программы для транспьютерной сети сопряжено с появлением зацикливаний и тупиков, которые невозможно обнаружить при локальной отладке отдельных секций. Зацикливание приводит к тому, что несколько процессов замыкаются сами на себя и не выходят на связь с внешним миром. Тупики возникают в ситуациях "буриданова осла", когда каждый процесс ждет некоторого действия со стороны другого процесса. Устранение зацикливаний и тупиков требует наглядного отображения значений переменных и структур данных во время выполнения процесса. Но и в инструментальных, и в целевых аппаратных платформах в лучшем случае доступны только периферийные транспьютеры. Поэтому при отладке ОККАМ-программ, прошедших конфигурирование, программисту приходится встраивать модули, обеспечивающие прямую передачу данных от интересующих его периферийных к внутренним транспьютерам, и наоборот. Такое манипулирование структурой ОККАМ-программ уже предопределяет и нтерактивный режим работы инструментальной платформы на этапе конфигурирования и комплексной отладки.

    Специфические особенности целевых аппаратных платформ вынуждают их производителей создавать инструментальные платформы, способные подержать программное конструирование только производимого ряда процессоров. Так, фирма Inmos, производящая транспьютеры, поставляет и систему разработки программ TDS ( Transputer Development System ), которая представляет собой интегрированную однопользовательскую среду разработки на базе компиляторов ОККАМ и инструментальной ЭВМ типа IBM PS с встроенной транспьютерной платой. На плате расположен один или несколько транспьютеров и блок буферной памяти, симулирующий режим реального времени процессов обмена данными с внешней средой.

    Аналогичные по структуре и функциям инструментальные платформы поставляют производители ЦПОС и RISC -процессоров [70], при создании которых уже учтены базовые принципы разработки СБИС-архитектур: модульность, регулярность, локальность связей, массовый параллелизм и минимизированный ввод-вывод. Все эти принципы ориентированы на максимальное использование СБИС-технологий, для которых основным ограничением является аппаратно-временная стоимость межсоединений, которые занимают большую часть пространства кристалла и снижают тактовую частоту.

    В идеале ЦПОС, RISC - и систолические проекты должны заканчиваться созданием кристалла матричной СБИС или системы на целой пластине, для чего требуется интегрированная программная среда проектирования. Входом такой среды является вычислительный и, как правило, рекурсивный алгоритм с параметрами потоков преобразуемых данных: количество операндов, обрабатываемых в одном цикле, период и точность обработки и т. п. Основу интегрированный среды составляет кремниевый компилятор, а интеллектуальная надстройка над ним призвана отобразить исходный алгоритм на матричную или иную структуру, адекватную граф-потоку сигналов. Совокупность кремниевого и структурного компиляторов представляет собой матричный компилятор, который должен удовлетворять следующим требованиям:

  • дружественный интерфейс, основанный на графике;
  • вход, описывающий поведение на языке высокого уровня;
  • интерактивное взаимодействие с пользователем;
  • иерархическая итерационная методология нисходящего и/или восходящего проектирования;
  • согласованная база данных для всех уровней иерархии проекта;
  • многоуровневое и смешанное моделирование;
  • верифицируемость каждого уровня и конечный результат.
  • Иерархическая итерационная методология нисходящего и/или восходящего проектирования (рис. 2.3) предполагает выбор одной из альтернатив реализации: программируемые ЦПОС, RISC -процессоры или алгоритмически ориентированные систолические структуры и отображение алгоритма на структуру выбранных элементов, что требует методов и средств вскрытия и выражения векторно-конвейерного параллелизма, присущего исходному вычислительному алгоритму.

    (рис 2.3) Нисходящее иерархическое проектирование матричных структур [70]

    Методология поиска такого отображения сводится к следующему [70]. Сначала строится граф зависимости, что проще всего сделать для рекурсивных алгоритмов, для которых проще всего построить пространственно-временное индексное пространство, которое и отображается с помощью дуг. Затем строится граф-поток сигналов, который состоит из узлов обработки, ребер связи и задержек и который, как правило, представляет собой проекцию трехмерного индексного пространства на двумерный массив граф-потока сигналов. Для этого этапа разработана методология канонического и обобщенного отображения [70], отвечающая рис. 2.3. Наконец на завершающем этапе осуществляется разработка матричного процессора, где в силу вступает кремниевый компилятор, для которого синтезированный граф-поток сигналов является почти директивой.

    Разработка матричного процессора заканчивается созданием среды программирования для пользователя (рис. 2.4), главная задача которого - вскрыть потенциальный параллелизм выбранного рекурсивного алгоритма.

    (рис 2.4) Структура инструментальных платформ пользователя матричных процессоров [70]

    Как показал опыт, для представления потенциального параллелизма больше подходят функциональные языки, так как традиционные процедурные языки типа Фортран контекстно соответствуют фон-неймановской модели вычислений, основанной на операторе присваивания и связанного с ним состояния процессора. Поэтому на таких языках, даже снабженных оптимизирующими и векторизирующими компиляторами, трудно выразить параллелизм, что требует преобразования исходного алгоритма в форму с однократным присваиванием. В языках типа ОККАМ программист в явном виде должен указать процессы, выполняемые последовательно и параллельно. Поэтому для достаточно обширной предметной области типа цифровая обработка сигналов и изображений были разработаны специальные языки типа SISAL (Streams and Iterations in a Single Assignment Language - потоки и итерации в языке с однократным присваиванием). SISAL имеет две формы цикла $$for$$, одна из которых традиционна, а другая генерирует все пары индексов дл я вычисления как декартова, так и скалярного произведения.

    Однако для генерации кода возможностей функциональных языков недостаточно, так как граф зависимости оперирует абстрактными функциями узла, в то время как заранее изготовленные аппаратные платформы способны реализовать ограниченный список реальных функций. Для согласования затребованных и реализуемых функций в узле необходима информация о графе и о функциях, что требует синтеза графа структуры программы, в котором отражено распределение программных модулей между процессорами. Чтобы получить такое промежуточное представление вычислительного алгоритма, используются языки, основанные на ациклических графах типа IF1 [70].

    Таким образом, приведенные данные позволяют заключить:

  • Интерактивный режим является неотъемлемой частью как пользовательских инструментальных платформ, так и инструментальных платформ "собственных нужд", и он должен быть поддержан дружественным графическим интерфейсом.
  • Центральную проблему представления, извлечения и реализации потенциального параллелизма, заложенного в вычислительный алгоритм, можно решить поэтапно и с помощью характерных для этого формальных средств, заложенных в специализированные языки программирования.
  • Во всех без исключения параллельных вычислительных технологиях распределение нагрузки между процессорами или процессорными элементами фактически сводится к конструированию тела программы, загружаемой в целевую аппаратную платформу.
  • Чем шире алгоритмическая база предметной области, тем более интенсивно используется интерактивный режим (микро)программного конструирования, так как область применения специализированных формальных методов представления и извлечения сужается до более простых программных секций.
  • Ориентированные на максимально достижимый коэффициент распараллеливания вычислений алгоритмы с однократным присваиванием не учитывают ограничений целевой аппаратной платформы, так как предполагают организацию вычислений по типу "одна операция присваивания - один процессор требуемой структурно-функциональной сложности".
  • На практике даже вновь создаваемая целевая аппаратная платформа ограничена массо-габаритами и потребляемой мощностью и неспособна поддержать аппаратно каждый оператор присваивания и стоящую за ним вычислительную, коммутационную или управляющую функцию. В результате ее приходится использовать итеративно, что порождает синонимы одних и тех же переменных, устранить которые можно только на более высоком уровне организации вычислений.

    2.2. Средства поддержки параллелизма в системах программирования общего назначения

    Анализ стилей программирования показывает, что линейный порядок исполнения операторов программы определяется не прагматикой решаемой задачи, а семантикой языковых конструкций исполнителя и служит лишь базой для реализации в конкретной системе [268]. Данное положение является идеологической базой распределения функций между программистом и компилятором, в рамках которого первый задает линейный порядок исполнения операторов и только помечает потенциально распараллеливаемые фрагменты программы, а второй воплощает потенциальный параллелизм каждой программы в вычислительном процессе с учетом имеющегося динамически перераспределяемого аппаратного ресурса конкретной вычислительной системы.

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

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

    Если при решении задачи умножения матриц разбить матрицу произведения (k*m)*(m*n) на подматрицы размера m*n, то для вычисления каждой такой подматрицы достаточно иметь m строк первого сомножителя и n столбцов второго. Разделив вычисления по независимым процессорам, можно ценой дублирования исходных данных значительно сократить время получения результата.

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

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

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

    Распределенное исполнение. Данный вид параллелизма предполагает, что процессы исполняются не только параллельно, но и на разных, часто слабосвязанных исполнителях.

    Современные системы программирования для вычислительных систем общего назначения содержат механизмы, поддерживающие параллелизм на уровне потоков исполнения. Механизмы параллелизма, например, включены в интегрированные среды разработки программ, использующих в качестве инструментальных языков Java, C#, C++.

    В качестве примера рассмотрим средства реализации потоков исполнения в С# [270]. Многопоточная программа состоит из двух или больше частей, которые могут выполняться одновременно. Каждая часть такой программы рассматривается как поток исполнения ( thread ). Каждый поток определяет собственную трассу выполнения операций. Таким образом, мно-гопоточность представляет собой специальную форму многозадачности. Многопоточное программирование опирается на сочетание средств, предусмотренных языком С#, и классов, определенных в среде . NET Framework.

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

    Поток - это управляемая единица выполняемого кода. В многозадачной среде, ориентированной на потоки, все процессы имеют по крайней мере один поток.

    Процессно-ориентированная многозадачность обеспечивает одновременное выполнение программ, а поточно-ориентированная - одновременное выполнение частей одной и той же программы.

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

    Все процессы имеют по крайней мере один поток управления, который обычно называется основным ( main thread ), поскольку именно с этого потока начинается выполнение программы.

    Язык С# и среда . NET Framework поддерживают как процессно-, так и поточно-ориентированную многозадачность. Следовательно, используя С#, можно создавать как процессы, так и потоки, а затем ими управлять. Поскольку поддержка многопоточности является встроенной, С# значительно упрощает создание многопоточных программ по сравнению с C++.

    Многопоточная система встроена в класс Thread, который инкапсулирует поток управления. В классе Thread определены методы и свойства для управления потоками.

    Поток создается путем создания объекта типа Thread, а выполнение потока инициируется методом Start(). Для управления потоком предусмотрены средства проверки завершения потока и ожидания завершения потока:

  • свойство IsAlive возвращает значение true, если поток, для которого оно опрашивается, еще выполняется. В противном случае оно возвращает значение false.
  • метод Join(), вызов которого приводит к ожиданию завершения потока, для которого этот метод вызван.
  • Среда . NET Framework определяет два типа потоков: высокоприоритетные и фоновые. Единственное различие между ними состоит в том, что процесс не завершится до тех пор, пока не завершится выполнение всех его высокоприоритетных потоков, при этом фоновые потоки заканчиваются автоматически после завершения всех высокоприоритетных потоков. По умолчанию любой поток создается как высокоприоритетный. При необходимости его можно сделать фоновым с помощью свойства IsBackground.

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

    После старта дочерний поток получает стандартное значение приоритета. Его можно изменить с помощью свойства Priority.

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

    В основе синхронизации лежит понятие блокировки, то есть управление доступом к некоторому блоку кода в объекте. На то время, когда объект заблокирован одним потоком, никакой другой поток не может получить доступ к заблокированному блоку кода. Когда поток снимет блокировку, объект станет доступным для использования другим потоком.

    Средство блокировки встроено в язык С#, поэтому доступ ко всем объектам может быть синхронизирован. Синхронизация поддерживается ключевым словом lock.

    Для поддержки параллелизма используется механизм событий. Управление процессом выполнения программы на основе механизма событий иллюстрируется схемой, приведенной на рис. 2.5.

    (рис 2.5) Схема организации программы "Источник - Приемник"

    В классе-источнике определяется особый тип данных - событие. Источник может содержать определение нескольких типов событий. Кроме того, в классе-источнике определяются методы - генераторы событий. Назначение генераторов событий - создание событий в ответ на сообщение, полученное от других объектов, других программ или внешних устройств.

    В классе-приемнике определяются методы обработки событий. При создании объекта-приемника производится привязка метода обработки к типу события, определенному в классе-источнике. Объект-источник в ответ на сообщение генерирует событие и автоматически рассылает извещение о нем всем объектам, которые на него подписались. С точки зрения разработчика программы рассылка извещения равносильна вызову метода - обработчика события.

    Разработка класса-источника и класса-приемника производится независимо друг от друга. Это означает, что классу-приемнику неизвестны имена вызываемых методов. Указание конкретного вызываемого метода должно выполняться в ходе выполнения программы. Для решения этой проблемы в языке C# используются специальные объекты - делегаты. Делегат может хранить ссылку на любой метод определенной сигнатуры и вызывать этот метод. В этом смысле можно считать, что объект-делегат хранит вызов метода. Таким образом, делегат позволяет вызывать метод опосредованно, не по имени метода, а по имени делегата.

    Хотя вызов метода-обработчика производит объект-источник, но первопричиной этого вызова является не сам объект-источник, а объект, выдавший сообщение об изменении своего состояния. Другими словами, при разработке класса источника необходимы средства, позволяющие трансформировать сообщение в вызов метода-обработчика. Для решения этой проблемы в языке C# используются специальные объекты-события. Событие может хранить один или несколько однотипных делегатов и вызывать эти делегаты. Другими словами, событие позволяет опосредованно вызывать делегат, и как следствие, метод-обработчик.

    Рассмотрим пример программы, использующей события. Прежде всего, определим предметную область и укажем функциональность приложения.

    Человек выступает в роли сотрудника предприятия и клиента банка, в котором он хранит вклад. Текущий вклад составляет 200 условных единиц. Банк ежемесячно начисляет клиенту 5 % от суммы вклада и выдает клиенту справку о состоянии вклада. Человек работает на двух предприятиях. Ежемесячный оклад на обоих предприятиях одинаков и составляет 100 условных единиц. Каждое из предприятий ежемесячно выплачивает сотруднику оклад, произведя предварительно налоговые отчисления в размере 10 %. Полученную сумму на каждом предприятии человек полностью переводит на свой вклад в банке.

    Требуется получить справку о состоянии вклада в течение трех месяцев. Справка выдается после перевода на счет клиента суммы, полученной на обоих предприятиях.

    Программа организована по схеме "Источник - Приемник" и содержит 5 классов. Комментарии и имена переменных достаточно полно раскрывают смысл методов и объектов, поэтому остановимся только на основных моментах.

    class Человек {
    public string фамилия;
    public double оклад;
    public double сумма;
    public double вклад;
    public Человек(string фамилия, double оклад, double вклад)
    {
    this.фамилия = фамилия; this.оклад = оклад; this.вклад = вклад;
    }
    //Объявление делегата для представления методов, принимающих
    //в качестве аргумента любой объект класса Человек
    public delegate void Расчет(Человек чел); }

    Все операции с окладом и вкладом конкретного человека будут выполняться методами с одинаковой сигнатурой: void ИмяМетода (Человек чел). Представителем таких методов будет делегат типа " Расчет ", объявленный в классе " Человек ".

    class Предприятие {
    public static double налог = 10.0;
    //Обработчик события "Окончание месяца" - начисление зарплаты
    public void Начислить(Человек сотр)
    {
    сотр.сумма = сотр.оклад; }
    //Обработчик события "Окончание месяца" //вычет налоговых отчислений public void Вычесть(Человек сотр) {
    сотр.сумма = сотр.оклад * (100.0 - налог) / 100.0; } }

    Класс служит типом для создания объектов - приемников извещений о событии " Окончание месяца ". Класс реализует обработчики событий, выполняющие начисление заработной платы и налоговые вычеты.

    class Банк {
    public static double процент = 5.0;
    //Обработчик события "Окончание месяца" - внесение вклада //в размере месячной зарплаты public void Внести(Человек клиент) {
    клиент.вклад = клиент.вклад + клиент.сумма;
    клиент.сумма = 0; }
    //Обработчик события "Окончание месяца" - начисление процентов
    public void Пересчитать(Человек клиент)
    {
    клиент.вклад = клиент.вклад * (100.0 + процент) / 100.0; }
    //Обработчик события "Окончание месяца" - 
    //выдача справки клиенту о текущей величине вклада 
    public void Сообщить(Человек клиент) 
     {
       Console.WriteLine("Вклад {0}:{1:f2}", клиент.фамилия, клиент.вклад); 
      } }

    Класс служит типом для создания объектов - приемников извещений о событии "Окончание месяца". Класс реализует обработчики событий, выполняющие операции с вкладом и выдачу справки о текущемм состоянии вклада.

    //Класc, объявляющий событие и выполняющий рассылку извещения
    //о событии для всех объектов, которые подписались
    //на это событие
    class Извещение
    {
     Человек чел;
     public Извещение(Человек чел) { this.чел = чел; }
     //Событие, связанное с окончанием текущего месяца public event Человек.Расчет Месяц;
     //Рассылка ивещений о событии для всех подписавшихся
     public void Разослать()
     {
      if (Месяц != null) Месяц(чел); 
     } 
    }

    Класс служит типом для создания объекта - источника рассылки извещений о событии. Обратите внимание, что при объявлении события " Месяц " имя делегата " Расчет " расширяется именем класса " Человек ", в котором определен этот делегат. Метод " Разослать " будет вызываться источником сообщений (в данном примере это будет метод Main ) и вызывать через событие " Месяц " все делегаты, подписавшиеся на это событие. Генерации события предшествует проверка наличия подписки.

    class Program {
    static void Main(string[] args) {
    Человек   чел = new Человек("Иванов", 100.0, 200.0); 
    Предприятие пр1 = new Предприятие(), пр2 = new Предприятие(); 
    Банк     бнк = new Банк();
    //Создаем объект, который будет выполнять 
    //рассылку извещений о событии 
    Извещение изв = new Извещение(чел);
    //Подписываемся на рассылку
    изв.Месяц += new	Человек.Расчет(бнк.Пересчитать);
    изв.Месяц += new	Человек.Расчет(пр1.Начислить);
    изв.Месяц += new	Человек.Расчет(пр1.Вычесть);
    изв.Месяц += new	Человек.Расчет(бнк.Внести);
    изв.Месяц += new	Человек.Расчет(пр2.Начислить);
    изв.Месяц += new	Человек.Расчет(пр2.Вычесть);
    изв.Месяц += new	Человек.Расчет(бнк.Внести);
    изв.Месяц += new	Человек.Расчет(бнк.Сообщить);
    //Генерируем сообщение - "Конец месяца" 
    for(int i=0; i<3; i++) изв.Разослать(); } }

    При запуске программы в методе Main создается объект типа " Человек ". Этот объект будет использоваться в качестве аргумента во всех обработчиках.

    В качестве объекта - источника для рассылки извещений создается объект c именем изв и три объекта - приемника извещений: два предприятия ( пр1 и пр2 ) и один банк ( бнк ). Объекты производят подписку на события, регистрируя в событии изв.Месяц делегаты типа " Расчет ", которым поручено представлять методы - обработчики событий. Обратите внимание, что имя делегата " Расчет " расширено именем класса "Человек". Необходимость такого расширения вызвана тем, что определение делегата локализовано в классе " Человек ".

    Выполнение программы завершается трехкратной генерацией сообщения об окончании месяца, которое реализовано как вызов метода рассылки извещений " изв.Разослать() ".На каждое сообщение об окончании месяца будут разосланы извещения и вызваны обравботчики событий в следующей последовательности:

    бнк.Пересчитать(чел);
    пр1.Начислить(чел);
    пр1.Вычесть(чел);
    бнк.Внести(чел);
    пр2.Начислить(чел);
    пр2.Вычесть(чел);
    бнк.Внести(чел);
    бнк.Сообщить(чел);

    В результате клиент получит три справки о состоянии вклада:

    Вклад Иванов: 390,00 
    Вклад Иванов: 589,50 
    Вклад Иванов: 798,98

    По рассмотренной схеме реализуются приложения с графическим интерфейсом пользователя, например, приложения, создаваемые в среде Visual Studio.Net по шаблону Windows Application (рис. 2.6).

    (рис 2.6) Типовая структура Windows-приложения

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

    Технологически разработка программы в этом случае сводится к следующим процессам:

  • Автоматическая генерация каркаса программы по заданному разработчиком шаблону.
  • Конструирование формы пользовательского интерфейса в визуальном режиме, сводящееся к размещению на форме элементов из имеющегося набора и настройка их свойств.
  • Связывание элементов интерфейса в визуальном режиме с обработчиками событий.
  • Ручная разработка кода, необходимого для обработки событий.
  • На рис. 2.7 приведена форма приложения, выполняющего вывод сообщения в окно и очистку этого окна.

    (рис 2.7) Форма приложения и элементы управления

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

    using System;
    using System.Collections.Generic;
    using System.Windows.Forms;
    namespace Начало
    {
     static class Program
     {
      [STAThread]
      static void Main() 
      {
       Application.EnableVisualStyles(); 
       Application.SetCompatibleTextRenderingDefault(false); 
       Application.Run(new Form1();форма); 
      } 
     } 
    }

    В классе Program определен метод Main, с которого начинается выполнение программы. Статический метод Run класса Application принимает ссылку на объект-приемник и организует цикл опроса очереди сообщений.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    namespace Начало
    {
     public partial class Form1 : Form 
     {
      //Конструктор формы
      public Form1()
      {
       InitializeComponent();
      }
      //Обработчик щелчка мышью на кнопке Выдать (имя объекта
      //button1)
      private void button1_Click(object sender, EventArgs e)
      {
       textBox1.Text = "Привет";
      }
      //Обработчик щелчка мышью на кнопке Очистить(имя объекта
      //button2)
      private void button2_Click(object sender, EventArgs e)
      {
       textBox1.Text = "";
      }
      //Обработчик щелчка мышью на кнопке Выход(имя объекта
      //button3)
      private void button3_Click(object sender, EventArgs e)
      {
       Close(); 
      } 
     } 
    }
    using System.Drawing; 
    using System.Windows.Forms; 
    using System.ComponentModel; 
    using System;
    namespace Начало 
    {
     partial class Form1 
     {
      //Поля - ссылки на объекты
      private Button button1;
      private Button button2;
      private Button button3;
      private TextBox textBox1;
      private IContainer components = null;
      //Освобождение ресурсов при закрытии формы
      //(метод вызывается автоматически при закрытии формы)
      protected override void Dispose(bool disposing)
      {
       if (disposing  (components != null)) 
       {
        components.Dispose(); 
       }
       base.Dispose(disposing); 
      }
      //Инициализация полей формы и самой формы
      private void InitializeComponent()
      {
       //Создание объектов интерфейса
       button1 = new Button();
       button2 = new Button();
       button3 = new Button();
       textBox1 = new TextBox();
       //Запрещение приема извещений о событиях на время установки
       //свойств объектов интерфейса и формы
       SuspendLayout(); 
       //1.Установка свойств объекта button1 (кнопка Выдать)
       //1.1.Расположение элемента на форме (левый верхний угол)
       button1.Location = new Point(12, 57);
       //1.2.Имя объекта
       button1.Name = "button1";
       //1.3.Размер объекта
       button1.Size = new Size(75, 23);
       //1.4.Очередность передачи фокуса управления клавишей
       //табуляции
       button1.TabIndex = 0;
       //1.5.Текст, отображаемый на изображении объекта
       button1.Text = "Выдать";
       //1.6.Режим использования фонового цвета для элемента
       //(использовать фоновый цвет данного элемента)
       button1.UseVisualStyleBackColor = true;
       //1.7.Подписка на событие
       button1.Click += new EventHandler(button1_Click); 
       //2.Установка свойств объекта button2 (кнопка Очистить)
       //2.1.Расположение элемента на форме (левый верхний угол)
       button2.Location = new Point(110, 57);
       //2.2.Имя объекта
       button2.Name = "button2";
       //2.3.Размер объекта
       button2.Size = new Size(75, 23);
       //2.4.Очередность передачи фокуса управления клавишей
       //табуляции
       button2.TabIndex = 1;
       //2.5.Текст, отображаемый на изображении объекта
       button2.Text = "Очистить";
       //2.6.Режим использования фонового цвета для элемента
       //(использовать фоновый цвет данного элемента)
       button2.UseVisualStyleBackColor = true;
       //2.7.Подписка на событие
       button2.Click += new EventHandler(button2_Click); 
       //3.Установка свойств объекта button3 (кнопка Выход)
       //3.1.Расположение элемента на форме (левый верхний угол)
       button3.Location = new Point(205, 57);
       //3.2.Имя объекта
       button3.Name = "button3";
       //3.3.Размер объекта
       button3.Size = new Size(75, 23);
       //3.4.Очередность передачи фокуса управления клавишей
       //табуляции
       button3.TabIndex = 2;
       //3.5.Текст, отображаемый на изображении объекта
       button3.Text = "Выход";
       //3.6.Режим использования фонового цвета для элемента
       //(использовать фоновый цвет данного элемента)
       button3.UseVisualStyleBackColor = true;
       //3.7.Подписка на событие
       button3.Click += new EventHandler(button3_Click); 
       //4.Установка свойств объекта textBox1 (отображение текстового
       //сообщения)
       //4.1.Расположение элемента на форме (левый верхний угол)
       textBox1.Location = new Point(52, 12);
       //4.2.Имя объекта
       textBox1.Name = "textBox1";
       //4.3.Размер объекта
       textBox1.Size = new Size(167, 20);
       //4.4.Очередность передачи фокуса управления клавишей
       //табуляции
       textBox1.TabIndex = 3; 
       //5.Установка свойств объекта Form1
       //5.1.Установка единиц измерения и режима изменения размеров
       //в зависимости от выбранного шрифта
       AutoScaleDimensions = new SizeF(6F, 13F);
       this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
       //5.2.Имя объекта
       Name = "Form1";
       //5.3.Текст, отображаемый на изображении объекта
       Text = "Form1";
       //5.4.Размер клиентской части формы (без учета заголовка)
       ClientSize = new Size(292, 104);
       //5.5.Запоминание ссылок на элементы управления
       //в динамическом массиве Controls
       Controls.Add( textBox1);
       Controls.Add( button3);
       Controls.Add( button2);
       Controls.Add( button1);
       //Разрешение приема извещений о событиях
       ResumeLayout(false); 
       PerformLayout(); 
      } 
     } 
    }

    Жирным шрифтом выделен код обработчиков событий, который необходимо ввести вручную.

    Реально приложения будут иметь гораздо более сложную функциональность и объем вводимого вручную кода значительно возрастет. Поддержка генерации кода пользовательских классов предусмотрена во многих инструментальных средах, предназначенных для разработки моделей на основе диаграмм языка UML [269, 273].

    Примерами такого рода инструментальных сред являются Rational Rose и IBM Rational Software Architect. Среды разработки программ могут иметь встроенные средства трансформации логической структуры программы, представленной диаграммой классов, в исходный код и обратной трансформации и интегрировать в себя средства специализированных сред визуального моделирования на основе диаграмм UML. Так, встроенные средства среды Visual Studio.Net поддерживают постоянную синхронизацию программного кода и соответствующей ему диаграммы классов, выполняя прямую и обратную трансформацию при изменении визуального представления диаграммы классов или программного кода C#. Вид диаграммы классов в среде Visual Studio.Net приведен на рис. 2.8. Кроме того, в среду Visual Studio.Net могут быть интегрированы средства Retional XDE, что обеспечивает работу со всеми видами диаграмм UML.

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

    Традиционным подходом к моделированию дискретно-событийных систем является разработка библиотеки многократно используемых компонент, из которых могут быть собраны модели различных систем [271]. Примером такой библиотеки может служить библиотека AnyLogicTM Enterprise Library. Библиотека предоставляет высокоуровневый интерфейс для быстрого создания дискретно-событийных моделей с помощью блок-схем.

    (рис 2.8) Диаграмма классов в Visual Studio.Net

    Графическое представление систем с помощью блок-схем широко используется во многих сферах деятельности: производстве, логистике, системах обслуживания, бизнес-процессах, моделировании компьютерных и телекоммуникационных сетей и т. д. Поддержка графического представления моделируемой системы в виде, привычном для специалиста в предметной области, позволяет конструировать модель и выполнять ее параметризацию в стиле "перетащить и оставить" (drag-and-drop). На рис. 2.9 приведена блок-схема модели системы массового обслуживания, сконструированная из стандартных объектов: генераторов заявок, очередей, задержек, разветвителей и т. п.

    Сконструированная модель компилируется в код Java, что позволяет программным способом реализовать варианты управления моделируемыми процессами и варианты обработки результатов моделирования, отличные от принятых в библиотеке по умолчанию.

    Исполнение разработанной модели выполняется в искусственном модельном времени. Для каждого процесса определяется структура данных, достаточная для запоминания его состояния в дискретный момент времени. Запомненное состояние называется точкой возобновления. Состояние процесса запоминается в управляющем списке. Каждый процесс может находиться в одном из четырех состояний:

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

    Ход времени рассматривается как последовательность события в той же логической последовательности и с теми же относительными по отношению к реальному времени интервалами, что и в моделируемой сиcтеме. Моделирование реакции системы на событие сводится к перестройке управляющего списка.

    Реализация отмеченного механизма имитации реального времени создает структуру, для внешнего наблюдателя практически неотличимую от реального параллелизма, причем с неограниченным количеством процессов.

    (рис 2.9) Блок-схема модели массового обслуживания в обозначениях AnyLogic Enterprise Librar

    Таким образом, приведенные данные позволяют утверждать, что методы и средства программного конструирования стали неотъемлемым атрибутом не только параллельных, но и классических программных платформ, что приближает современные вычислительные технологии к "идеальному" распределению функций между программистом и компилятором, первый из которых "выражает" потенциально достижимый параллелизм в прикладной программе, написанной и отлаженной на последовательной ЭВМ, а второй "воплощает" этот параллелизм в реальном вычислительном процессе.

    Системотехнические выводы по лекции 2

  • Отправной точкой распараллеливания вычислений является алгоритм, откуда следует, что коэффициент распараллеливания программы не может быть выше, чем у вычислительного алгоритма, реализуемого этой программой. Сам вычислительный алгоритм является эквивалентной формой записи аналитической модели предметной области, в которой строго определены приоритеты, а значит, и порядок выполнения операций.
  • Методы и средства конструирования программ являются не только прерогативой параллельных нейрокомпьютерных технологий и технологий с микропрограммным уровнем поддержки, но и широко используются при создании программных продуктов для ВС общего назначения. При этом надо отличать инструментальные платформы, которые ориентированы на использование уже существующих аппаратных платформ, от инструментальных платформ, ориентированных на их создание. В первом случае допускается только специфицированная реконфигурация аппаратной платформы под требования вычислительного алгоритма пользователя, что было апробировано в транспьютерных и многопроцессорных ЦПОС -проектах, а во втором случае вычислительный алгоритм известен и требуется методами и средствами (полу)заказного проектирования синтезировать аппаратуру его поддержки, что характерно для систолических матриц.
  • Центральная проблема создания современных инструментальных платформ - это формализованные средства представления и поддержки параллелизма, использование которых требует углубленного знания возможностей целевой аппаратной платформы, призванной воплотить в жизнь затребованный программным конструктором параллелизм.
  • Решить в общем виде задачу формализованного представления параллелизма пока не удалось, что вынуждает:
  • использовать интерактивный режим (микро)программного конструирования либо на всех стадиях проекта, как это имеет место в систолических и бит-потоковых технологиях, либо на отдельных, но самых ответственных этапах декомпозиции проекта и распределения (конфигурирования) затребованных ресурсов между коллективом вычислителей, как это имеет место в транспьютерных и многопроцессорных ЦПОС- и RISC-технологиях;
  • строить по иерархическому принципу инструментальные платформы "собственных нужд", поэтапно решая задачи извлечения из вычислительного алгоритма потенциально достижимого коэффициента распараллеливания вычислений, представления граф-потока сигнала в "терминах" целевой аппаратной платформы, устранения синонимии, возникающей из-за ограниченных размеров целевой аппаратной платформы.
  • Страницы:

    2.1. Методы и средства представления и поддержки параллелизма

    Использование параллельных вычислительных систем сопряжено с целым рядом проблем, связанных с представлением и поддержкой параллелизма, потому что пока не удалось найти формальные методы, способные эффективно вскрывать имеющийся в последовательном алгоритме потенциальный уровень распараллеливания каждой фазы вычислений, а тем более распределять между процессорами вытекающую из него вычислительную нагрузку. Поэтому процесс проектирования параллельных программ и реализующих их систем был и пока остается интерактивным. В таких условиях остается только предоставить программисту эффективные языковые средства и инструментальные платформы, обеспечивающие отладку и загрузку параллельных программ в существующую или создаваемую многопроцессорную систему. Языки параллельного программирования и их инструментальные платформы отталкиваются от архитектурных особенностей поддержки параллелизма в аппаратной платформе и поэтому не могут быть универсальными, так как выразительные и исполнительные средства - две стороны одной медали. Отсюда, либо аппаратная платформа разрабатывается под заранее заданный язык параллельного программирования, как это имеет место в транспьютерах [267], архитектура которых ориентирована на язык высокого уровня ОККАМ, либо для аппаратной платформы пишется узкоспециализированный язык, что характерно для спецпроцессоров. Аналогичная с ОККАМ ситуация складывалась и в случае ПРОЛОГ- и ЛИСП-процессоров, процессоров баз данных и баз знаний, которая была характерна для японской микроэлектроники 80-х годов прошлого столетия [278].

    Все инструментальные платформы для параллельных вычислителей можно разбить на две группы: одни ориентированы на использование уже существующих аппаратных платформ, а вторые - на их создание. В первом случае допускается только специфицированная реконфигурация аппаратной платформы под требования вычислительного алгоритма пользователя, что характерно для транспьютерных систем [267], а во втором случае вычислительный алгоритм известен и требуется методами и средствами (полу)заказного проектирования синтезировать аппаратуру его поддержки, что характерно для систолических матриц [70].

    Типичная архитектура транспьютера (рис. 2.1) ориентирована на многопроцессорные ВС МКМД-типа с массовыми пересылками данных и управляемыми потоками данных, в которых запуск инструкции осуществляется при наличии обрабатываемых операндов. При этом для выравнивания скоростей обработки и пересылки данных каждый транспьютер, исполненный в виде отдельной СБИС, содержит встроенное ОЗУ, функции которого сходны с функциями кеш-памяти в современных процессорах общего назначения. Объясняется это тем, что внутренняя пересылка ОЗУ - регистр выполняется гораздо быстрее любой внешней пересылки, что характерно для всех без исключения микроэлектронных технологий.

    (рис 2.1) Внутренняя архитектура транспьютера IMS T800 и формат инструкции [267]

    Ассемблерные инструкции транспьютера рассчитаны на разработку программ, написанных на языке высокого уровня ОККАМ. Как и в RISC -технологиях, для эффективной компиляции программ набор ассемблерных инструкций сокращен до минимума и все они имеют одинаковый формат (см. рис. 2.1), в котором выделено поле для кода функции и для данных. Для увеличения разрядности преобразуемых операндов в языке ОККАМ предусмотрены две префиксные конструкции: prefix и negative prefix, первая из которых сдвигает влево на четыре разряда четырехбитный операнд, загруженный в регистр операнда. Вторая инструкция делает то же самое, но предварительно преобразует содержимое пересылаемых данных в дополнительный код.

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

    В одном из первых транспьютеров IMS T800 каждый последовательный процесс поддерживался всего шестью регистрами (рис. 2.2), что упростило логику управления и повысило скорость работы каналов при обмене с внутренним ОЗУ. Три регистра A, B и C реализуют механизм аппаратного стека с "вершиной" в A и являются приемниками и источниками для большинства арифметико-логических операций транспьютера. В трех оставшихся регистрах хранятся соответственно указатель области памяти с локальными переменными, указатель на следующую команду (счетчик команд) и наращиваемый с помощью prefix и negative prefix операнд. Все команды оперируют только с вершиной стека, и поэтому нет необходимости переопределять положение операндов инструкций по ходу вычислений.

    (рис 2.2) Связный список процессов [267]

    Каждый последовательный процесс, включенный в состав параллельного, может находиться в следующих состояниях:

  • активен: выполняется или находится в очереди на выполнение;
  • пассивен: готов к вводу, готов к выводу, ожидает, когда наступит ука занное время.
  • Микропрограммный планировщик IMS T800 устроен так, что пассивные процессы не занимают процессорное время, а активные, ожидающие процессы находятся в списке рабочих областей процессов, который поддерживается двумя регистрами (см. рис. 2.2): один указывает на первый процесс из списка, а второй - на последний. В нашем случае процесс S исполняется, а процессы P, Q и R активны, но ожидают выполнения.

    Каждый последовательный процесс выполняется до тех пор, пока не перейдет в режим ожидания ввода, вывода или таймера, что как раз и соответствует идеологии управления потоками данных. Как только процесс не может исполняться, его счетчик команд сохраняется в рабочей области и выбирается следующий процесс из списка для выполнения. Как видно из схемы рис. 2.2, "цена" такого прерывания - 12 пересылок, обновляющих содержимое 6 регистров с сохранением предыдущего содержимого в ОЗУ, которое расположено на том же кристалле, то есть можно считать, что в штатном режиме $$\tau_{c}(p) = \tau_{ОЗУ} $$ (см. раздел 1.2).

    Управление потоками данных в параллельных транспьютерных системах поддерживается альтернативными конструкциями языка ОККАМ в составе: enable channel, enable timer, alternative wait, disable channel, disable timer (соответственно допустимый канал (таймер), альтернативное ожидание и вышел из строя канал (таймер)). Механизм реализации следующий. Сначала выполняются инструкции enable channel или enable timer каждой программной компоненты, включенной в список диспетчеризации. Затем с помощью инструкции alternative wait из списка диспетчеризации исключается процесс, если ни один канал или таймер не готов. После этого выполняется инструкция disable channel или disable timer. Процесс возвращается в список диспетчеризации, если любой из обслуживающих его каналов или таймеров станет готовым.

    При работе транспьютера с данными формата плавающей точки в IMS T800 использована традиционная адресная арифметика, когда адрес операнда хранится в стеке центрального процессора, а операнд загружается в стек процессора плавающей точки из адресуемой ячейки памяти (см. рис. 2.1). Формат float и double задаются специальным тегом, что сокращает число инструкций. В частности, вместо двух инструкций типа float add single и float add double достаточно одной доопределяемой тегом инструкции float add.

    В программе, написанной на языке ОККАМ, программист должен в явном виде указать одно из двух ключевых слов: SEQ или PAR, первое из которых является директивой последовательного, а второе - параллельного выполнения нижележащих процессов. Но перед этим программист должен осуществить декомпозицию всей задачи на составляющие процессы, причем сделать это надо так, чтобы равномерно распределить вычислительную нагрузку между транспьютерами сети.

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

    Во всех случаях цикл разработки программ практически одинаков и включает: редактирование текста программы, проверку текста программы на ошибки, компиляцию программных секций, отвечающих последовательным участкам программы, в машинные коды, компоновку отдельно скомпилированных секций кода, конфигурирование программы для ее выполнения в целевой сети, загрузку программы в целевую сеть и выполнение программы.

    Главная особенность технологии конструирования параллельных ОККАМ-программ сосредоточена:

  • в компоновке независимо скомпилированных фрагментов программ;
  • в конфигурировании, в рамках которого программист готовит описание конфигурации, отображающее процессы на процессоры и каналы на реальные линии связи.
  • Компоновка независимо скомпилированных фрагментов программ удобна тем, что позволяет оперативно вносить дополнения и изменения в проект без его полной перекомпиляции.

    Конфигурирование отдельных секций программы для транспьютерной сети сопряжено с появлением зацикливаний и тупиков, которые невозможно обнаружить при локальной отладке отдельных секций. Зацикливание приводит к тому, что несколько процессов замыкаются сами на себя и не выходят на связь с внешним миром. Тупики возникают в ситуациях "буриданова осла", когда каждый процесс ждет некоторого действия со стороны другого процесса. Устранение зацикливаний и тупиков требует наглядного отображения значений переменных и структур данных во время выполнения процесса. Но и в инструментальных, и в целевых аппаратных платформах в лучшем случае доступны только периферийные транспьютеры. Поэтому при отладке ОККАМ-программ, прошедших конфигурирование, программисту приходится встраивать модули, обеспечивающие прямую передачу данных от интересующих его периферийных к внутренним транспьютерам, и наоборот. Такое манипулирование структурой ОККАМ-программ уже предопределяет и нтерактивный режим работы инструментальной платформы на этапе конфигурирования и комплексной отладки.

    Специфические особенности целевых аппаратных платформ вынуждают их производителей создавать инструментальные платформы, способные подержать программное конструирование только производимого ряда процессоров. Так, фирма Inmos, производящая транспьютеры, поставляет и систему разработки программ TDS ( Transputer Development System ), которая представляет собой интегрированную однопользовательскую среду разработки на базе компиляторов ОККАМ и инструментальной ЭВМ типа IBM PS с встроенной транспьютерной платой. На плате расположен один или несколько транспьютеров и блок буферной памяти, симулирующий режим реального времени процессов обмена данными с внешней средой.

    Аналогичные по структуре и функциям инструментальные платформы поставляют производители ЦПОС и RISC -процессоров [70], при создании которых уже учтены базовые принципы разработки СБИС-архитектур: модульность, регулярность, локальность связей, массовый параллелизм и минимизированный ввод-вывод. Все эти принципы ориентированы на максимальное использование СБИС-технологий, для которых основным ограничением является аппаратно-временная стоимость межсоединений, которые занимают большую часть пространства кристалла и снижают тактовую частоту.

    В идеале ЦПОС, RISC - и систолические проекты должны заканчиваться созданием кристалла матричной СБИС или системы на целой пластине, для чего требуется интегрированная программная среда проектирования. Входом такой среды является вычислительный и, как правило, рекурсивный алгоритм с параметрами потоков преобразуемых данных: количество операндов, обрабатываемых в одном цикле, период и точность обработки и т. п. Основу интегрированный среды составляет кремниевый компилятор, а интеллектуальная надстройка над ним призвана отобразить исходный алгоритм на матричную или иную структуру, адекватную граф-потоку сигналов. Совокупность кремниевого и структурного компиляторов представляет собой матричный компилятор, который должен удовлетворять следующим требованиям:

  • дружественный интерфейс, основанный на графике;
  • вход, описывающий поведение на языке высокого уровня;
  • интерактивное взаимодействие с пользователем;
  • иерархическая итерационная методология нисходящего и/или восходящего проектирования;
  • согласованная база данных для всех уровней иерархии проекта;
  • многоуровневое и смешанное моделирование;
  • верифицируемость каждого уровня и конечный результат.
  • Иерархическая итерационная методология нисходящего и/или восходящего проектирования (рис. 2.3) предполагает выбор одной из альтернатив реализации: программируемые ЦПОС, RISC -процессоры или алгоритмически ориентированные систолические структуры и отображение алгоритма на структуру выбранных элементов, что требует методов и средств вскрытия и выражения векторно-конвейерного параллелизма, присущего исходному вычислительному алгоритму.

    (рис 2.3) Нисходящее иерархическое проектирование матричных структур [70]

    Методология поиска такого отображения сводится к следующему [70]. Сначала строится граф зависимости, что проще всего сделать для рекурсивных алгоритмов, для которых проще всего построить пространственно-временное индексное пространство, которое и отображается с помощью дуг. Затем строится граф-поток сигналов, который состоит из узлов обработки, ребер связи и задержек и который, как правило, представляет собой проекцию трехмерного индексного пространства на двумерный массив граф-потока сигналов. Для этого этапа разработана методология канонического и обобщенного отображения [70], отвечающая рис. 2.3. Наконец на завершающем этапе осуществляется разработка матричного процессора, где в силу вступает кремниевый компилятор, для которого синтезированный граф-поток сигналов является почти директивой.

    Разработка матричного процессора заканчивается созданием среды программирования для пользователя (рис. 2.4), главная задача которого - вскрыть потенциальный параллелизм выбранного рекурсивного алгоритма.

    (рис 2.4) Структура инструментальных платформ пользователя матричных процессоров [70]

    Как показал опыт, для представления потенциального параллелизма больше подходят функциональные языки, так как традиционные процедурные языки типа Фортран контекстно соответствуют фон-неймановской модели вычислений, основанной на операторе присваивания и связанного с ним состояния процессора. Поэтому на таких языках, даже снабженных оптимизирующими и векторизирующими компиляторами, трудно выразить параллелизм, что требует преобразования исходного алгоритма в форму с однократным присваиванием. В языках типа ОККАМ программист в явном виде должен указать процессы, выполняемые последовательно и параллельно. Поэтому для достаточно обширной предметной области типа цифровая обработка сигналов и изображений были разработаны специальные языки типа SISAL (Streams and Iterations in a Single Assignment Language - потоки и итерации в языке с однократным присваиванием). SISAL имеет две формы цикла $$for$$, одна из которых традиционна, а другая генерирует все пары индексов дл я вычисления как декартова, так и скалярного произведения.

    Однако для генерации кода возможностей функциональных языков недостаточно, так как граф зависимости оперирует абстрактными функциями узла, в то время как заранее изготовленные аппаратные платформы способны реализовать ограниченный список реальных функций. Для согласования затребованных и реализуемых функций в узле необходима информация о графе и о функциях, что требует синтеза графа структуры программы, в котором отражено распределение программных модулей между процессорами. Чтобы получить такое промежуточное представление вычислительного алгоритма, используются языки, основанные на ациклических графах типа IF1 [70].

    Таким образом, приведенные данные позволяют заключить:

  • Интерактивный режим является неотъемлемой частью как пользовательских инструментальных платформ, так и инструментальных платформ "собственных нужд", и он должен быть поддержан дружественным графическим интерфейсом.
  • Центральную проблему представления, извлечения и реализации потенциального параллелизма, заложенного в вычислительный алгоритм, можно решить поэтапно и с помощью характерных для этого формальных средств, заложенных в специализированные языки программирования.
  • Во всех без исключения параллельных вычислительных технологиях распределение нагрузки между процессорами или процессорными элементами фактически сводится к конструированию тела программы, загружаемой в целевую аппаратную платформу.
  • Чем шире алгоритмическая база предметной области, тем более интенсивно используется интерактивный режим (микро)программного конструирования, так как область применения специализированных формальных методов представления и извлечения сужается до более простых программных секций.
  • Ориентированные на максимально достижимый коэффициент распараллеливания вычислений алгоритмы с однократным присваиванием не учитывают ограничений целевой аппаратной платформы, так как предполагают организацию вычислений по типу "одна операция присваивания - один процессор требуемой структурно-функциональной сложности".
  • На практике даже вновь создаваемая целевая аппаратная платформа ограничена массо-габаритами и потребляемой мощностью и неспособна поддержать аппаратно каждый оператор присваивания и стоящую за ним вычислительную, коммутационную или управляющую функцию. В результате ее приходится использовать итеративно, что порождает синонимы одних и тех же переменных, устранить которые можно только на более высоком уровне организации вычислений.

    2.2. Средства поддержки параллелизма в системах программирования общего назначения

    Анализ стилей программирования показывает, что линейный порядок исполнения операторов программы определяется не прагматикой решаемой задачи, а семантикой языковых конструкций исполнителя и служит лишь базой для реализации в конкретной системе [268]. Данное положение является идеологической базой распределения функций между программистом и компилятором, в рамках которого первый задает линейный порядок исполнения операторов и только помечает потенциально распараллеливаемые фрагменты программы, а второй воплощает потенциальный параллелизм каждой программы в вычислительном процессе с учетом имеющегося динамически перераспределяемого аппаратного ресурса конкретной вычислительной системы.

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

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

    Если при решении задачи умножения матриц разбить матрицу произведения (k*m)*(m*n) на подматрицы размера m*n, то для вычисления каждой такой подматрицы достаточно иметь m строк первого сомножителя и n столбцов второго. Разделив вычисления по независимым процессорам, можно ценой дублирования исходных данных значительно сократить время получения результата.

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

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

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

    Распределенное исполнение. Данный вид параллелизма предполагает, что процессы исполняются не только параллельно, но и на разных, часто слабосвязанных исполнителях.

    Современные системы программирования для вычислительных систем общего назначения содержат механизмы, поддерживающие параллелизм на уровне потоков исполнения. Механизмы параллелизма, например, включены в интегрированные среды разработки программ, использующих в качестве инструментальных языков Java, C#, C++.

    В качестве примера рассмотрим средства реализации потоков исполнения в С# [270]. Многопоточная программа состоит из двух или больше частей, которые могут выполняться одновременно. Каждая часть такой программы рассматривается как поток исполнения ( thread ). Каждый поток определяет собственную трассу выполнения операций. Таким образом, мно-гопоточность представляет собой специальную форму многозадачности. Многопоточное программирование опирается на сочетание средств, предусмотренных языком С#, и классов, определенных в среде . NET Framework.

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

    Поток - это управляемая единица выполняемого кода. В многозадачной среде, ориентированной на потоки, все процессы имеют по крайней мере один поток.

    Процессно-ориентированная многозадачность обеспечивает одновременное выполнение программ, а поточно-ориентированная - одновременное выполнение частей одной и той же программы.

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

    Все процессы имеют по крайней мере один поток управления, который обычно называется основным ( main thread ), поскольку именно с этого потока начинается выполнение программы.

    Язык С# и среда . NET Framework поддерживают как процессно-, так и поточно-ориентированную многозадачность. Следовательно, используя С#, можно создавать как процессы, так и потоки, а затем ими управлять. Поскольку поддержка многопоточности является встроенной, С# значительно упрощает создание многопоточных программ по сравнению с C++.

    Многопоточная система встроена в класс Thread, который инкапсулирует поток управления. В классе Thread определены методы и свойства для управления потоками.

    Поток создается путем создания объекта типа Thread, а выполнение потока инициируется методом Start(). Для управления потоком предусмотрены средства проверки завершения потока и ожидания завершения потока:

  • свойство IsAlive возвращает значение true, если поток, для которого оно опрашивается, еще выполняется. В противном случае оно возвращает значение false.
  • метод Join(), вызов которого приводит к ожиданию завершения потока, для которого этот метод вызван.
  • Среда . NET Framework определяет два типа потоков: высокоприоритетные и фоновые. Единственное различие между ними состоит в том, что процесс не завершится до тех пор, пока не завершится выполнение всех его высокоприоритетных потоков, при этом фоновые потоки заканчиваются автоматически после завершения всех высокоприоритетных потоков. По умолчанию любой поток создается как высокоприоритетный. При необходимости его можно сделать фоновым с помощью свойства IsBackground.

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

    После старта дочерний поток получает стандартное значение приоритета. Его можно изменить с помощью свойства Priority.

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

    В основе синхронизации лежит понятие блокировки, то есть управление доступом к некоторому блоку кода в объекте. На то время, когда объект заблокирован одним потоком, никакой другой поток не может получить доступ к заблокированному блоку кода. Когда поток снимет блокировку, объект станет доступным для использования другим потоком.

    Средство блокировки встроено в язык С#, поэтому доступ ко всем объектам может быть синхронизирован. Синхронизация поддерживается ключевым словом lock.

    Для поддержки параллелизма используется механизм событий. Управление процессом выполнения программы на основе механизма событий иллюстрируется схемой, приведенной на рис. 2.5.

    (рис 2.5) Схема организации программы "Источник - Приемник"

    В классе-источнике определяется особый тип данных - событие. Источник может содержать определение нескольких типов событий. Кроме того, в классе-источнике определяются методы - генераторы событий. Назначение генераторов событий - создание событий в ответ на сообщение, полученное от других объектов, других программ или внешних устройств.

    В классе-приемнике определяются методы обработки событий. При создании объекта-приемника производится привязка метода обработки к типу события, определенному в классе-источнике. Объект-источник в ответ на сообщение генерирует событие и автоматически рассылает извещение о нем всем объектам, которые на него подписались. С точки зрения разработчика программы рассылка извещения равносильна вызову метода - обработчика события.

    Разработка класса-источника и класса-приемника производится независимо друг от друга. Это означает, что классу-приемнику неизвестны имена вызываемых методов. Указание конкретного вызываемого метода должно выполняться в ходе выполнения программы. Для решения этой проблемы в языке C# используются специальные объекты - делегаты. Делегат может хранить ссылку на любой метод определенной сигнатуры и вызывать этот метод. В этом смысле можно считать, что объект-делегат хранит вызов метода. Таким образом, делегат позволяет вызывать метод опосредованно, не по имени метода, а по имени делегата.

    Хотя вызов метода-обработчика производит объект-источник, но первопричиной этого вызова является не сам объект-источник, а объект, выдавший сообщение об изменении своего состояния. Другими словами, при разработке класса источника необходимы средства, позволяющие трансформировать сообщение в вызов метода-обработчика. Для решения этой проблемы в языке C# используются специальные объекты-события. Событие может хранить один или несколько однотипных делегатов и вызывать эти делегаты. Другими словами, событие позволяет опосредованно вызывать делегат, и как следствие, метод-обработчик.

    Рассмотрим пример программы, использующей события. Прежде всего, определим предметную область и укажем функциональность приложения.

    Человек выступает в роли сотрудника предприятия и клиента банка, в котором он хранит вклад. Текущий вклад составляет 200 условных единиц. Банк ежемесячно начисляет клиенту 5 % от суммы вклада и выдает клиенту справку о состоянии вклада. Человек работает на двух предприятиях. Ежемесячный оклад на обоих предприятиях одинаков и составляет 100 условных единиц. Каждое из предприятий ежемесячно выплачивает сотруднику оклад, произведя предварительно налоговые отчисления в размере 10 %. Полученную сумму на каждом предприятии человек полностью переводит на свой вклад в банке.

    Требуется получить справку о состоянии вклада в течение трех месяцев. Справка выдается после перевода на счет клиента суммы, полученной на обоих предприятиях.

    Программа организована по схеме "Источник - Приемник" и содержит 5 классов. Комментарии и имена переменных достаточно полно раскрывают смысл методов и объектов, поэтому остановимся только на основных моментах.

    class Человек {
    public string фамилия;
    public double оклад;
    public double сумма;
    public double вклад;
    public Человек(string фамилия, double оклад, double вклад)
    {
    this.фамилия = фамилия; this.оклад = оклад; this.вклад = вклад;
    }
    //Объявление делегата для представления методов, принимающих
    //в качестве аргумента любой объект класса Человек
    public delegate void Расчет(Человек чел); }

    Все операции с окладом и вкладом конкретного человека будут выполняться методами с одинаковой сигнатурой: void ИмяМетода (Человек чел). Представителем таких методов будет делегат типа " Расчет ", объявленный в классе " Человек ".

    class Предприятие {
    public static double налог = 10.0;
    //Обработчик события "Окончание месяца" - начисление зарплаты
    public void Начислить(Человек сотр)
    {
    сотр.сумма = сотр.оклад; }
    //Обработчик события "Окончание месяца" //вычет налоговых отчислений public void Вычесть(Человек сотр) {
    сотр.сумма = сотр.оклад * (100.0 - налог) / 100.0; } }

    Класс служит типом для создания объектов - приемников извещений о событии " Окончание месяца ". Класс реализует обработчики событий, выполняющие начисление заработной платы и налоговые вычеты.

    class Банк {
    public static double процент = 5.0;
    //Обработчик события "Окончание месяца" - внесение вклада //в размере месячной зарплаты public void Внести(Человек клиент) {
    клиент.вклад = клиент.вклад + клиент.сумма;
    клиент.сумма = 0; }
    //Обработчик события "Окончание месяца" - начисление процентов
    public void Пересчитать(Человек клиент)
    {
    клиент.вклад = клиент.вклад * (100.0 + процент) / 100.0; }
    //Обработчик события "Окончание месяца" - 
    //выдача справки клиенту о текущей величине вклада 
    public void Сообщить(Человек клиент) 
     {
       Console.WriteLine("Вклад {0}:{1:f2}", клиент.фамилия, клиент.вклад); 
      } }

    Класс служит типом для создания объектов - приемников извещений о событии "Окончание месяца". Класс реализует обработчики событий, выполняющие операции с вкладом и выдачу справки о текущемм состоянии вклада.

    //Класc, объявляющий событие и выполняющий рассылку извещения
    //о событии для всех объектов, которые подписались
    //на это событие
    class Извещение
    {
     Человек чел;
     public Извещение(Человек чел) { this.чел = чел; }
     //Событие, связанное с окончанием текущего месяца public event Человек.Расчет Месяц;
     //Рассылка ивещений о событии для всех подписавшихся
     public void Разослать()
     {
      if (Месяц != null) Месяц(чел); 
     } 
    }

    Класс служит типом для создания объекта - источника рассылки извещений о событии. Обратите внимание, что при объявлении события " Месяц " имя делегата " Расчет " расширяется именем класса " Человек ", в котором определен этот делегат. Метод " Разослать " будет вызываться источником сообщений (в данном примере это будет метод Main ) и вызывать через событие " Месяц " все делегаты, подписавшиеся на это событие. Генерации события предшествует проверка наличия подписки.

    class Program {
    static void Main(string[] args) {
    Человек   чел = new Человек("Иванов", 100.0, 200.0); 
    Предприятие пр1 = new Предприятие(), пр2 = new Предприятие(); 
    Банк     бнк = new Банк();
    //Создаем объект, который будет выполнять 
    //рассылку извещений о событии 
    Извещение изв = new Извещение(чел);
    //Подписываемся на рассылку
    изв.Месяц += new	Человек.Расчет(бнк.Пересчитать);
    изв.Месяц += new	Человек.Расчет(пр1.Начислить);
    изв.Месяц += new	Человек.Расчет(пр1.Вычесть);
    изв.Месяц += new	Человек.Расчет(бнк.Внести);
    изв.Месяц += new	Человек.Расчет(пр2.Начислить);
    изв.Месяц += new	Человек.Расчет(пр2.Вычесть);
    изв.Месяц += new	Человек.Расчет(бнк.Внести);
    изв.Месяц += new	Человек.Расчет(бнк.Сообщить);
    //Генерируем сообщение - "Конец месяца" 
    for(int i=0; i<3; i++) изв.Разослать(); } }

    При запуске программы в методе Main создается объект типа " Человек ". Этот объект будет использоваться в качестве аргумента во всех обработчиках.

    В качестве объекта - источника для рассылки извещений создается объект c именем изв и три объекта - приемника извещений: два предприятия ( пр1 и пр2 ) и один банк ( бнк ). Объекты производят подписку на события, регистрируя в событии изв.Месяц делегаты типа " Расчет ", которым поручено представлять методы - обработчики событий. Обратите внимание, что имя делегата " Расчет " расширено именем класса "Человек". Необходимость такого расширения вызвана тем, что определение делегата локализовано в классе " Человек ".

    Выполнение программы завершается трехкратной генерацией сообщения об окончании месяца, которое реализовано как вызов метода рассылки извещений " изв.Разослать() ".На каждое сообщение об окончании месяца будут разосланы извещения и вызваны обравботчики событий в следующей последовательности:

    бнк.Пересчитать(чел);
    пр1.Начислить(чел);
    пр1.Вычесть(чел);
    бнк.Внести(чел);
    пр2.Начислить(чел);
    пр2.Вычесть(чел);
    бнк.Внести(чел);
    бнк.Сообщить(чел);

    В результате клиент получит три справки о состоянии вклада:

    Вклад Иванов: 390,00 
    Вклад Иванов: 589,50 
    Вклад Иванов: 798,98

    По рассмотренной схеме реализуются приложения с графическим интерфейсом пользователя, например, приложения, создаваемые в среде Visual Studio.Net по шаблону Windows Application (рис. 2.6).

    (рис 2.6) Типовая структура Windows-приложения

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

    Технологически разработка программы в этом случае сводится к следующим процессам:

  • Автоматическая генерация каркаса программы по заданному разработчиком шаблону.
  • Конструирование формы пользовательского интерфейса в визуальном режиме, сводящееся к размещению на форме элементов из имеющегося набора и настройка их свойств.
  • Связывание элементов интерфейса в визуальном режиме с обработчиками событий.
  • Ручная разработка кода, необходимого для обработки событий.
  • На рис. 2.7 приведена форма приложения, выполняющего вывод сообщения в окно и очистку этого окна.

    (рис 2.7) Форма приложения и элементы управления

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

    using System;
    using System.Collections.Generic;
    using System.Windows.Forms;
    namespace Начало
    {
     static class Program
     {
      [STAThread]
      static void Main() 
      {
       Application.EnableVisualStyles(); 
       Application.SetCompatibleTextRenderingDefault(false); 
       Application.Run(new Form1();форма); 
      } 
     } 
    }

    В классе Program определен метод Main, с которого начинается выполнение программы. Статический метод Run класса Application принимает ссылку на объект-приемник и организует цикл опроса очереди сообщений.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    namespace Начало
    {
     public partial class Form1 : Form 
     {
      //Конструктор формы
      public Form1()
      {
       InitializeComponent();
      }
      //Обработчик щелчка мышью на кнопке Выдать (имя объекта
      //button1)
      private void button1_Click(object sender, EventArgs e)
      {
       textBox1.Text = "Привет";
      }
      //Обработчик щелчка мышью на кнопке Очистить(имя объекта
      //button2)
      private void button2_Click(object sender, EventArgs e)
      {
       textBox1.Text = "";
      }
      //Обработчик щелчка мышью на кнопке Выход(имя объекта
      //button3)
      private void button3_Click(object sender, EventArgs e)
      {
       Close(); 
      } 
     } 
    }
    using System.Drawing; 
    using System.Windows.Forms; 
    using System.ComponentModel; 
    using System;
    namespace Начало 
    {
     partial class Form1 
     {
      //Поля - ссылки на объекты
      private Button button1;
      private Button button2;
      private Button button3;
      private TextBox textBox1;
      private IContainer components = null;
      //Освобождение ресурсов при закрытии формы
      //(метод вызывается автоматически при закрытии формы)
      protected override void Dispose(bool disposing)
      {
       if (disposing  (components != null)) 
       {
        components.Dispose(); 
       }
       base.Dispose(disposing); 
      }
      //Инициализация полей формы и самой формы
      private void InitializeComponent()
      {
       //Создание объектов интерфейса
       button1 = new Button();
       button2 = new Button();
       button3 = new Button();
       textBox1 = new TextBox();
       //Запрещение приема извещений о событиях на время установки
       //свойств объектов интерфейса и формы
       SuspendLayout(); 
       //1.Установка свойств объекта button1 (кнопка Выдать)
       //1.1.Расположение элемента на форме (левый верхний угол)
       button1.Location = new Point(12, 57);
       //1.2.Имя объекта
       button1.Name = "button1";
       //1.3.Размер объекта
       button1.Size = new Size(75, 23);
       //1.4.Очередность передачи фокуса управления клавишей
       //табуляции
       button1.TabIndex = 0;
       //1.5.Текст, отображаемый на изображении объекта
       button1.Text = "Выдать";
       //1.6.Режим использования фонового цвета для элемента
       //(использовать фоновый цвет данного элемента)
       button1.UseVisualStyleBackColor = true;
       //1.7.Подписка на событие
       button1.Click += new EventHandler(button1_Click); 
       //2.Установка свойств объекта button2 (кнопка Очистить)
       //2.1.Расположение элемента на форме (левый верхний угол)
       button2.Location = new Point(110, 57);
       //2.2.Имя объекта
       button2.Name = "button2";
       //2.3.Размер объекта
       button2.Size = new Size(75, 23);
       //2.4.Очередность передачи фокуса управления клавишей
       //табуляции
       button2.TabIndex = 1;
       //2.5.Текст, отображаемый на изображении объекта
       button2.Text = "Очистить";
       //2.6.Режим использования фонового цвета для элемента
       //(использовать фоновый цвет данного элемента)
       button2.UseVisualStyleBackColor = true;
       //2.7.Подписка на событие
       button2.Click += new EventHandler(button2_Click); 
       //3.Установка свойств объекта button3 (кнопка Выход)
       //3.1.Расположение элемента на форме (левый верхний угол)
       button3.Location = new Point(205, 57);
       //3.2.Имя объекта
       button3.Name = "button3";
       //3.3.Размер объекта
       button3.Size = new Size(75, 23);
       //3.4.Очередность передачи фокуса управления клавишей
       //табуляции
       button3.TabIndex = 2;
       //3.5.Текст, отображаемый на изображении объекта
       button3.Text = "Выход";
       //3.6.Режим использования фонового цвета для элемента
       //(использовать фоновый цвет данного элемента)
       button3.UseVisualStyleBackColor = true;
       //3.7.Подписка на событие
       button3.Click += new EventHandler(button3_Click); 
       //4.Установка свойств объекта textBox1 (отображение текстового
       //сообщения)
       //4.1.Расположение элемента на форме (левый верхний угол)
       textBox1.Location = new Point(52, 12);
       //4.2.Имя объекта
       textBox1.Name = "textBox1";
       //4.3.Размер объекта
       textBox1.Size = new Size(167, 20);
       //4.4.Очередность передачи фокуса управления клавишей
       //табуляции
       textBox1.TabIndex = 3; 
       //5.Установка свойств объекта Form1
       //5.1.Установка единиц измерения и режима изменения размеров
       //в зависимости от выбранного шрифта
       AutoScaleDimensions = new SizeF(6F, 13F);
       this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
       //5.2.Имя объекта
       Name = "Form1";
       //5.3.Текст, отображаемый на изображении объекта
       Text = "Form1";
       //5.4.Размер клиентской части формы (без учета заголовка)
       ClientSize = new Size(292, 104);
       //5.5.Запоминание ссылок на элементы управления
       //в динамическом массиве Controls
       Controls.Add( textBox1);
       Controls.Add( button3);
       Controls.Add( button2);
       Controls.Add( button1);
       //Разрешение приема извещений о событиях
       ResumeLayout(false); 
       PerformLayout(); 
      } 
     } 
    }

    Жирным шрифтом выделен код обработчиков событий, который необходимо ввести вручную.

    Реально приложения будут иметь гораздо более сложную функциональность и объем вводимого вручную кода значительно возрастет. Поддержка генерации кода пользовательских классов предусмотрена во многих инструментальных средах, предназначенных для разработки моделей на основе диаграмм языка UML [269, 273].

    Примерами такого рода инструментальных сред являются Rational Rose и IBM Rational Software Architect. Среды разработки программ могут иметь встроенные средства трансформации логической структуры программы, представленной диаграммой классов, в исходный код и обратной трансформации и интегрировать в себя средства специализированных сред визуального моделирования на основе диаграмм UML. Так, встроенные средства среды Visual Studio.Net поддерживают постоянную синхронизацию программного кода и соответствующей ему диаграммы классов, выполняя прямую и обратную трансформацию при изменении визуального представления диаграммы классов или программного кода C#. Вид диаграммы классов в среде Visual Studio.Net приведен на рис. 2.8. Кроме того, в среду Visual Studio.Net могут быть интегрированы средства Retional XDE, что обеспечивает работу со всеми видами диаграмм UML.

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

    Традиционным подходом к моделированию дискретно-событийных систем является разработка библиотеки многократно используемых компонент, из которых могут быть собраны модели различных систем [271]. Примером такой библиотеки может служить библиотека AnyLogicTM Enterprise Library. Библиотека предоставляет высокоуровневый интерфейс для быстрого создания дискретно-событийных моделей с помощью блок-схем.

    (рис 2.8) Диаграмма классов в Visual Studio.Net

    Графическое представление систем с помощью блок-схем широко используется во многих сферах деятельности: производстве, логистике, системах обслуживания, бизнес-процессах, моделировании компьютерных и телекоммуникационных сетей и т. д. Поддержка графического представления моделируемой системы в виде, привычном для специалиста в предметной области, позволяет конструировать модель и выполнять ее параметризацию в стиле "перетащить и оставить" (drag-and-drop). На рис. 2.9 приведена блок-схема модели системы массового обслуживания, сконструированная из стандартных объектов: генераторов заявок, очередей, задержек, разветвителей и т. п.

    Сконструированная модель компилируется в код Java, что позволяет программным способом реализовать варианты управления моделируемыми процессами и варианты обработки результатов моделирования, отличные от принятых в библиотеке по умолчанию.

    Исполнение разработанной модели выполняется в искусственном модельном времени. Для каждого процесса определяется структура данных, достаточная для запоминания его состояния в дискретный момент времени. Запомненное состояние называется точкой возобновления. Состояние процесса запоминается в управляющем списке. Каждый процесс может находиться в одном из четырех состояний:

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

    Ход времени рассматривается как последовательность события в той же логической последовательности и с теми же относительными по отношению к реальному времени интервалами, что и в моделируемой сиcтеме. Моделирование реакции системы на событие сводится к перестройке управляющего списка.

    Реализация отмеченного механизма имитации реального времени создает структуру, для внешнего наблюдателя практически неотличимую от реального параллелизма, причем с неограниченным количеством процессов.

    (рис 2.9) Блок-схема модели массового обслуживания в обозначениях AnyLogic Enterprise Librar

    Таким образом, приведенные данные позволяют утверждать, что методы и средства программного конструирования стали неотъемлемым атрибутом не только параллельных, но и классических программных платформ, что приближает современные вычислительные технологии к "идеальному" распределению функций между программистом и компилятором, первый из которых "выражает" потенциально достижимый параллелизм в прикладной программе, написанной и отлаженной на последовательной ЭВМ, а второй "воплощает" этот параллелизм в реальном вычислительном процессе.

    Системотехнические выводы по лекции 2

  • Отправной точкой распараллеливания вычислений является алгоритм, откуда следует, что коэффициент распараллеливания программы не может быть выше, чем у вычислительного алгоритма, реализуемого этой программой. Сам вычислительный алгоритм является эквивалентной формой записи аналитической модели предметной области, в которой строго определены приоритеты, а значит, и порядок выполнения операций.
  • Методы и средства конструирования программ являются не только прерогативой параллельных нейрокомпьютерных технологий и технологий с микропрограммным уровнем поддержки, но и широко используются при создании программных продуктов для ВС общего назначения. При этом надо отличать инструментальные платформы, которые ориентированы на использование уже существующих аппаратных платформ, от инструментальных платформ, ориентированных на их создание. В первом случае допускается только специфицированная реконфигурация аппаратной платформы под требования вычислительного алгоритма пользователя, что было апробировано в транспьютерных и многопроцессорных ЦПОС -проектах, а во втором случае вычислительный алгоритм известен и требуется методами и средствами (полу)заказного проектирования синтезировать аппаратуру его поддержки, что характерно для систолических матриц.
  • Центральная проблема создания современных инструментальных платформ - это формализованные средства представления и поддержки параллелизма, использование которых требует углубленного знания возможностей целевой аппаратной платформы, призванной воплотить в жизнь затребованный программным конструктором параллелизм.
  • Решить в общем виде задачу формализованного представления параллелизма пока не удалось, что вынуждает:
  • использовать интерактивный режим (микро)программного конструирования либо на всех стадиях проекта, как это имеет место в систолических и бит-потоковых технологиях, либо на отдельных, но самых ответственных этапах декомпозиции проекта и распределения (конфигурирования) затребованных ресурсов между коллективом вычислителей, как это имеет место в транспьютерных и многопроцессорных ЦПОС- и RISC-технологиях;
  • строить по иерархическому принципу инструментальные платформы "собственных нужд", поэтапно решая задачи извлечения из вычислительного алгоритма потенциально достижимого коэффициента распараллеливания вычислений, представления граф-потока сигнала в "терминах" целевой аппаратной платформы, устранения синонимии, возникающей из-за ограниченных размеров целевой аппаратной платформы.
  • Вернуться к учебному плану