Сопрограмма: различия между версиями
[отпатрулированная версия] | [непроверенная версия] |
Mercury (обсуждение | вклад) |
→Потребность в сопрограммах: Поправил перевод. |
||
Строка 53: | Строка 53: | ||
Видно, что тот алгоритм, который «снаружи», будет компактнее, потому что у него есть ещё один способ хранения своего состояния — положение в потоке выполнения (то есть [[счётчик команд]]). Тот, который внутри, вынужден по выходу сохранять это положение во вре́менные переменные («i» в первом случае и «запомненное» во втором). |
Видно, что тот алгоритм, который «снаружи», будет компактнее, потому что у него есть ещё один способ хранения своего состояния — положение в потоке выполнения (то есть [[счётчик команд]]). Тот, который внутри, вынужден по выходу сохранять это положение во вре́менные переменные («i» в первом случае и «запомненное» во втором). |
||
Если допустить, что у каждого алгоритма будет свой счётчик команд, а командой '''yield''' можно передавать информацию из алгоритма в алгоритм, то можно взять лучшее из обоих методов. |
Если допустить, что у каждого алгоритма будет свой счётчик команд, а командой '''сцедить''' ('''yield)''' можно передавать информацию из алгоритма в алгоритм, то можно взять лучшее из обоих методов. |
||
// на сопрограммах |
// на сопрограммах |
||
Строка 63: | Строка 63: | ||
если пара (x,y) «хорошая» |
если пара (x,y) «хорошая» |
||
(a,b) := преобразоватьВНормальное(x,y) |
(a,b) := преобразоватьВНормальное(x,y) |
||
если не ''' |
если не '''сцедить'''(точкаВхода, a) или не '''сцедить'''(точкаВхода, b) |
||
стоп |
стоп |
||
|
|
||
Строка 69: | Строка 69: | ||
алг точкаВхода() |
алг точкаВхода() |
||
для i := 1..N |
для i := 1..N |
||
вывести(''' |
вывести('''сцедить'''(датчик, ИСТИНА)) |
||
''' |
'''сцедить'''(датчик, ЛОЖЬ) // алгоритм «датчик» закончится |
||
Если в нашем распоряжении два процессорных ядра и [[неблокирующая синхронизация]], получается отличный способ распараллелить генерацию чисел и вывод таблицы. Однако, если нет того или другого, ничего хорошего не будет: большую часть времени займут [[переключение контекста|переключения контекста]]. |
Если в нашем распоряжении два процессорных ядра и [[неблокирующая синхронизация]], получается отличный способ распараллелить генерацию чисел и вывод таблицы. Однако, если нет того или другого, ничего хорошего не будет: большую часть времени займут [[переключение контекста|переключения контекста]]. |
||
Впрочем, передавать управление из модуля в модуль можно как в [[Windows 3.x]], без многопоточности и синхронизации: команда '''yield''' («уступить») перезаписывает [[регистровый файл]] процессора, заодно передавая из модуля в модуль какую-то информацию. Без двух стеков в общем случае нельзя, однако некоторые компиляторы ([[C Sharp|C#]]) в простейших случаях способны автоматически преобразовать код в методику 2. |
Впрочем, передавать управление из модуля в модуль можно как в [[Windows 3.x]], без многопоточности и синхронизации: команда '''yield''' («сцедить, уступить») перезаписывает [[регистровый файл]] процессора, заодно передавая из модуля в модуль какую-то информацию. Без двух стеков в общем случае нельзя, однако некоторые компиляторы ([[C Sharp|C#]]) в простейших случаях способны автоматически преобразовать код в методику 2. |
||
== Сравнение и примеры == |
== Сравнение и примеры == |
Версия от 10:41, 5 июля 2018
Сопрограммы (англ. coroutines) — методика связи программных модулей друг с другом по принципу кооперативной многозадачности: модуль приостанавливается в определённой точке, сохраняя полное состояние (включая стек вызовов и счётчик команд), и передаёт управление другому. Тот, в свою очередь, выполняет задачу и передаёт управление обратно, сохраняя свои стек и счётчик.
Сопрограммы являются более гибкими и обобщёнными, чем подпрограммы, но реже используются на практике. Применение сопрограмм являлось методикой ещё ассемблера, практиковалось лишь в некоторых высокоуровневых языках (Simula, Modula-2). Сопрограммы хорошо пригодны для реализации многих похожих компонентов программ (итераторов, бесконечных списков, каналов, совместных задач).
Потребность в сопрограммах
Состояние некоторых видов вычислений очень удобно задавать местом в потоке выполнения. Например, один из датчиков нормальных случайных чисел генерирует пару равномерных случайных чисел и при определённых условиях превращает её в нормальную пару. Есть два способа вывести таблицу нормальных чисел.
// способ 1: снаружи датчик
// датчик
алг точкаВхода()
бесконечный цикл
x := равномерноеСлучайное()
y := равномерноеСлучайное()
если пара (x,y) «хорошая»
(a,b) := преобразоватьВНормальное(x,y)
если не табуляция(a) или не табуляция(b)
стоп
// табуляция
i := 0
алг табуляция(a : дробное) : булевское
если i >= N
вернуть ЛОЖЬ
i := i + 1
вывести a
вернуть ИСТИНА
// способ 2: снаружи табуляция
// датчик
запомненное := NaN
алг датчик() : дробное
если запомненное — не NaN
a := запомненное
запомненное := NaN
иначе
цикл
x := равномерноеСлучайное()
y := равномерноеСлучайное()
до (x,y) — «хорошая» пара
(а,запомненное) := преобразоватьВНормальное(x,y)
вернуть a
// табуляция
алг точкаВхода()
для i := 1..N
вывести датчик()
Видно, что тот алгоритм, который «снаружи», будет компактнее, потому что у него есть ещё один способ хранения своего состояния — положение в потоке выполнения (то есть счётчик команд). Тот, который внутри, вынужден по выходу сохранять это положение во вре́менные переменные («i» в первом случае и «запомненное» во втором).
Если допустить, что у каждого алгоритма будет свой счётчик команд, а командой сцедить (yield) можно передавать информацию из алгоритма в алгоритм, то можно взять лучшее из обоих методов.
// на сопрограммах // датчик алг датчик() бесконечный цикл x := равномерноеСлучайное() y := равномерноеСлучайное() если пара (x,y) «хорошая» (a,b) := преобразоватьВНормальное(x,y) если не сцедить(точкаВхода, a) или не сцедить(точкаВхода, b) стоп // табуляция алг точкаВхода() для i := 1..N вывести(сцедить(датчик, ИСТИНА)) сцедить(датчик, ЛОЖЬ) // алгоритм «датчик» закончится
Если в нашем распоряжении два процессорных ядра и неблокирующая синхронизация, получается отличный способ распараллелить генерацию чисел и вывод таблицы. Однако, если нет того или другого, ничего хорошего не будет: большую часть времени займут переключения контекста.
Впрочем, передавать управление из модуля в модуль можно как в Windows 3.x, без многопоточности и синхронизации: команда yield («сцедить, уступить») перезаписывает регистровый файл процессора, заодно передавая из модуля в модуль какую-то информацию. Без двух стеков в общем случае нельзя, однако некоторые компиляторы (C#) в простейших случаях способны автоматически преобразовать код в методику 2.
Сравнение и примеры
Сопрограммы являются более обобщёнными, чем подпрограммы. Подпрограмма имеет всегда одну входную точку, сопрограмма имеет стартовую точку входа и размещённые внутри последовательность возвратов и следующих за ними точек входа. Подпрограмма может возвращаться только однажды, сопрограмма может возвращать управление несколько раз. Время работы подпрограммы определяется принципом LIFO (последняя вызванная подпрограмма завершается первой), время работы сопрограммы определяется её использованием и необходимостью.
Пример
Здесь приведён простой пример, когда сопрограмма может быть полезна. Предположим, необходимо реализовать отношение между потребителем и производителем, где одна часть программы производит элементы и помещает их в очередь, а другая удаляет их и использует. Следующий код выполняет это:
var q := new queue coroutine produce loop while q is not full create some new items add the items to q yield to consume coroutine consume loop while q is not empty remove some items from q use the items yield to produce
Каждая сопрограмма передаёт управление при помощи директивы yield
. Несмотря на то, что данный пример нередко используется для иллюстрации принципов работы многопоточного кода, реализация описываемого способа передачи управления не требует наличия нескольких потоков: директива yield
может быть реализована как прямая передача управления другой сопрограмме — внутри одного потока выполнения.
Для сопрограммы можно иметь несколько входных и выходных точек, и любую подпрограмму можно реализовать как сопрограмму. Действительно, по утверждению Кнута, «подпрограмма является частным случаем сопрограммы».
Каждый раз при вызове подпрограммы выполнение начинается со стартовой точки. Аналогично, при первом вызове сопрограммы выполнение начинается со стартовой точки сопрограммы. Однако при последующих вызовах выполнение продолжается с точки последнего возврата.
Подпрограмма возвращает значение только однажды, возврат нескольких значений требует использовать в качестве возвращаемого значения коллекции. Если в некоторых языках это приемлемо, например Forth и Перл, то другие языки, например Си, поддерживают только одно возвращаемое значение, и поэтому необходимо возвращать ссылку на коллекцию. В противовес, сопрограмма может возвращать результат несколько раз, возвращение множества значений требует только вызова сопрограммы несколько раз.
Сопрограммы, которые возвращают множество значений, часто называют генераторами.
Подпрограммы требуют один стек для работы, который может быть выделен в начале выполнения программы.
Сопрограмма же, имеющая возможность вызывать другие сопрограммы одного с ней уровня, лучше реализуется с использованием продолжений. Тогда может потребоваться выделять дополнительные стеки, и поэтому это (в основном) реализуется в высокоуровневых языках с поддержкой сборщика мусора. Создание сопрограмм может быть условно-эффективным только при использовании заранее выделенных стеков и кэшировании ранее созданных.
Сопрограммы могут быть полезны для реализации следующего:
- Конечный автомат внутри одной подпрограммы, где состояние определяется по текущей точке входа/выхода (повышает читабельность текста).
- Модель акторов.
- Генераторы, которые являются полезными для ввода-вывода и обобщённого обхода структур данных.
Языки программирования, поддерживающие сопрограммы
Список примеров в этой статье не основывается на авторитетных источниках, посвящённых непосредственно предмету статьи. |
- AngelScript
- C# (операторы yield и await)
- Forth (изначально не поддерживает, но поддержка легко реализуется средствами самого Форта)
- Io
- JavaScript (начиная с ES6)
- Lua
- Modula-2
- Perl 5 (Coro)
- Perl 6
- Python
- Ruby
- Simula
- Tcl (начиная с 8.6)
- PHP (начиная с 5.5)
- Scala[источник не указан 3868 дней]
- Kotlin
- Go
Так как продолжения используются для реализации сопрограмм, языки поддерживающие их, могут легко реализовать поддержку сопрограмм.
Альтернативы и реализации
Большинство современных популярных языков программирования, включая Си и производные, не имеют прямой поддержки сопрограмм в языке или стандартной библиотеке (это обусловлено, большей частью, требованиями к стековой реализации подпрограмм).
В ситуации, когда сопрограммы, как естественный способ реализации компонентов, недоступны, типичным решением является создание сопрограмм с использованием набора булевских флагов и других состояний переменных для поддержки внешнего состояния между вызовами. Условия внутри кода приводят к выполнению различных последовательностей команд при последовательных вызовах в соответствии со значениями переменных состояния. Другим типичным решением является самостоятельная реализация конечного автомата с помощью большой инструкции switch. Такие реализации являются сложными для поддержки и сопровождения.
Потоки являются подходящей альтернативой для сопрограмм в большинстве современных разработок. Потоки предоставляют возможности для управления взаимодействием «одновременно» выполняющихся участков кода. Поэтому это решение больших и сложных проблем, оно включает мощные комплексные возможности и имеет сопутствующую сложность для обучения. Однако, несмотря на другие альтернативы, потоки широко доступны в окружении Си, являются близкими для большинства программистов, и обычно реализованы, документированы и сопровождаются.
Реализации
Различные попытки реализовать сопрограммы на Си имели переменный успех. Наиболее заметные:
- Coroutines in C by Simon Tatham (Саймон Тэтхем) (англ.) — с применением «устройства Даффа»
- Protothreads
- Portable Coroutine Library (PCL) (англ.)
- Coro (англ.)
- Reentrant coroutines in C++ (англ.)
- Boost.Coroutine (англ.)
На других языках:
- C# использует сопрограммы как внутренние детали реализации операторов yield (функция-генератор последовательности) и await (асинхронная функция)
- Python
- Io
- Coroutine (англ.)
- Ruby
- Coroutine (яп.);
- Perl
- Coro (англ.);
- Lua имеет встроенную поддержку сопрограмм;
- Функциональные языки программирования зачастую реализуют сопрограммы, например Scheme, Lisp, Haskell.
В Windows API сопрограммами являются «волокна» (fibers, игра слов, основанная на том, что поток выполнения — thread, «нить»).
См. также
Литература
- Дональд Кнут. Искусство программирования, том 1. Основные алгоритмы = The Art of Computer Programming, vol.1. Fundamental Algorithms. — 3-е изд. — М.: «Вильямс», 2006. — С. 720. — ISBN 0-201-89683-4. Раздел 1.4.2: Сопрограммы, стр. 229—236
Для улучшения этой статьи желательно:
|
![]() | В другом языковом разделе есть более полная статья coroutine (англ.). |