Сопрограммы (Coroutines)
Хотя наш следующий пример и не является полностью параллельным (по крайней мере в его первоначальном виде), но он важен как способ проверки применимости нашего параллельного механизма.
Первым (и, возможно, единственным) из главных языков программирования, включившим конструкцию сопрограмм, был также и первый ОО-язык Simula 67; мы будем рассматривать его механизм сопрограмм при его описании в лекции 17. Там же будут приведены примеры практического использования сопрограмм. |
Сопрограммы моделируют параллельность на последовательном компьютере. Они представляют собой программные единицы, отражающие симметричную форму взаимодействия:
- При вызове обычной подпрограммы имеется хозяин и раб. Хозяин запускает подпрограмму, ожидает ее завершения и продолжает с того места, где закончился вызов; однако подпрограмма при вызове всегда начинает работу с самого начала. Хозяин вызывает, а подпрограмма-раб возвращает.
- Отношения между сопрограммами - это отношения равных. Когда сопрограмма a застревает в процессе своей работы, то она призывает сопрограмму b на помощь; b запускается с того места, где последний раз остановилась, и продолжает выполнение до тех пор, пока сама не застрянет или не выполнит все, что от нее в данный момент требуется; затем a возобновляет свое вычисление. Вместо разных механизмов вызова и возврата здесь имеется одна операция возобновления вычисления resume c, означающая: запусти сопрограмму c с того места, в котором она последний раз была прервана, а я буду ждать, пока кто-нибудь не возобновит (resumes) мою работу.
Рис. 12.12. Последовательность выполнения сопрограмм
Все это происходит строго последовательно и предназначено для выполнения в одном процессе (задании) одного компьютера. Но сама идея получена из параллельных вычислений; например, операционная система, выполняемая на одном ЦПУ, будет внутри себя использовать механизм сопрограмм для реализации разделения времени, многозадачности и многопоточности.
Можно рассматривать сопрограммы как некоторый ограниченный вид параллельности: бедный суррогат параллельного вычисления, которому доступна лишь одна ветвь управления.
Обычно полезно проверять общие механизмы на их способность элегантно упрощаться для работы в ограниченных ситуациях, поэтому давайте посмотрим, как можно представить сопрограммы. Эта цель достигается с помощью следующих двух классов:
separate class COROUTINE creation make feature {COROUTINE} resume (i: INTEGER) is -- Разбудить сопрограмму с идентификатором i и пойти спать do actual_resume (i, controller) end feature {NONE} -- Implementation controller: COROUTINE_CONTROLLER identifier: INTEGER actual_resume (i: INTEGER; c: COROUTINE_CONTROLLER) is -- Разбудить сопрограмму с идентификатором i и пойти спать. -- (Реальная работа resume). do c.set_next (i); request (c) end request (c: COROUTINE_CONTROLLER) is -- Запрос возможного повторного пробуждения от c require c.is_next (identifier) do -- Действия не нужны end feature {NONE} -- Создание make (i: INTEGER; c: COROUTINE_CONTROLLER) is -- Присвоение i идентификатору и c котроллеру do identifier := i controller := c end end separate class COROUTINE_CONTROLLER feature {NONE} next: INTEGER feature {COROUTINE} set_next (i: INTEGER) is -- Выбор i в качестве следующей пробуждаемой сопрограммы do next := i end is_next (i: INTEGER): BOOLEAN is -- Является ли i индексом следующей пробуждаемой сопрограммы? do Result := (next = i) end endОдна или несколько сопрограмм будут разделять один контроллер сопрограмм, создаваемый не приведенной здесь однократной функцей (см. У12.10). У каждой сопрограммы имеется целочисленный идентификатор. Чтобы возобновить сопрограмму с идентификатором i, процедура resume с помощью actual_resume установит атрибут next контроллера в i, а затем приостановится, ожидая выполнения предусловия next = j, в котором j - это идентификатор самой сопрограммы. Это и обеспечит требуемое поведение.
Хотя это выглядит как обычная параллельная программа, данное решение гарантирует (в случае, когда у всех сопрограмм разные идентификаторы), что в каждый момент сможет выполняться лишь одна сопрограмма, что делает ненужным назначение более одного физического ЦПУ. (Контроллер мог бы использовать собственное ЦПУ, но его действия настолько просты, что этого не следует делать.)
Обращение к целочисленным идентификаторам необходимо, поскольку передача resume аргумента типа COROUTINE, т. е. сепаратного типа, вызвала бы блокировку. На практике можно воспользоваться объявлениями unique, чтобы не задавать эти идентификаторы вручную. Такое использование целых чисел имеет еще одно интересное следствие: если мы допустим, чтобы две или более сопрограмм имели одинаковые идентификаторы, то при наличии одного ЦПУ получим механизм недетерминированности: вызов resume (i) позволит перезапустить любую сопрограмму с идентификатором i. Если же ЦПУ будет много, то вызов resume (i) позволит параллельно выполняться всем сопрограммам с идентификатором i.
Таким образом, у приведенной схемы двойной эффект: в случае одного ЦПУ она обеспечивает работу механизма сопрограмм, а в случае нескольких ЦПУ - механизма управления максимальным числом одновременно активных процессов определенного вида.