Сопрограммы — это компоненты компьютерной программы , которые позволяют приостанавливать и возобновлять выполнение, обобщая подпрограммы для совместной многозадачности . Сопрограммы хорошо подходят для реализации знакомых программных компонентов, таких как совместные задачи , исключения , циклы событий , итераторы , бесконечные списки и каналы .
Их описывают как «функции, выполнение которых можно приостановить». [1]
Мелвин Конвей ввёл термин «сопрограмма» в 1958 году, когда применил его к построению ассемблерной программы . [2] Первое опубликованное объяснение сопрограммы появилось позже, в 1963 году. [3]
Не существует единого точного определения сопрограммы. В 1980 году Кристофер Д. Марлин [4] суммировал две широко признанные фундаментальные характеристики сопрограммы:
Кроме того, реализация сопрограммы имеет 3 особенности:
yield
и resume
. Программисты не могут свободно выбирать, какому кадру уступить. Среда выполнения уступает только ближайшему вызывающему объекту текущей сопрограммы. С другой стороны, в симметричной сопрограмме программисты должны указать пункт назначения вывода.yield
.В журнале Revisiting Coroutines [5] , опубликованном в 2009 году, был предложен термин « Полная сопрограмма» для обозначения программы, которая поддерживает первоклассную сопрограмму и является стековой. Полные сопрограммы заслуживают собственного названия, поскольку они обладают той же выразительной силой , что и одноразовые продолжения и продолжения с разделителями. Полные сопрограммы бывают симметричными или асимметричными. Важно отметить, что то, является ли сопрограмма симметричной или асимметричной, не влияет на то, насколько выразительной она может быть, поскольку они одинаково выразительны, хотя полные сопрограммы более выразительны, чем неполные сопрограммы. Хотя их выразительная сила одинакова, асимметричные сопрограммы больше напоминают структуры управления, основанные на подпрограммах, в том смысле, что управление всегда передается обратно вызывающей стороне, что программистам может показаться более знакомым.
Подпрограммы — это частные случаи сопрограмм. [6] При вызове подпрограммы выполнение начинается с самого начала, и как только подпрограмма завершается, она завершается; экземпляр подпрограммы возвращает значение только один раз и не сохраняет состояние между вызовами. Напротив, сопрограммы могут завершиться, вызвав другие сопрограммы, которые позже могут вернуться к тому месту, где они были вызваны в исходной сопрограмме; с точки зрения сопрограммы, это не выход, а вызов другой сопрограммы. [6] Таким образом, экземпляр сопрограммы сохраняет состояние и меняется между вызовами; одновременно может существовать несколько экземпляров данной сопрограммы. Разница между вызовом другой сопрограммы посредством «уступки» ей и простым вызовом другой подпрограммы (которая затем также вернется в исходную точку) заключается в том, что отношения между двумя сопрограммами, которые уступают друг другу, не являются отношениями вызывающего -вызываемый, но вместо этого симметричный.
Любую подпрограмму можно преобразовать в сопрограмму, которая не вызывает выход . [7]
Вот простой пример того, чем могут быть полезны сопрограммы. Предположим, у вас есть отношения потребитель-производитель, где одна процедура создает элементы и добавляет их в очередь, а другая удаляет элементы из очереди и использует их. Из соображений эффективности вы хотите добавлять и удалять несколько элементов одновременно. Код может выглядеть так:
var q := новая очередьсопрограмма создает цикл , пока q не заполнен создать несколько новых предметов добавить элементы в q урожай , чтобы потреблятьцикл потребления сопрограммы , пока q не пуст удалить некоторые элементы из q использовать предметы урожайность , чтобы произвестипозвонить, произвести
Затем очередь полностью заполняется или очищается, прежде чем передать управление другой сопрограмме с помощью команды Выход . Дальнейшие вызовы сопрограмм начинаются сразу после выхода , во внешнем цикле сопрограммы.
Хотя этот пример часто используется как введение в многопоточность , два потока для этого не нужны: оператор урожайности можно реализовать путем перехода непосредственно из одной подпрограммы в другую.
Сопрограммы очень похожи на потоки . Однако сопрограммы являются многозадачными совместно , тогда как потоки обычно являются многозадачными с вытеснением . Сопрограммы обеспечивают параллелизм , поскольку позволяют выполнять задачи вне последовательности или в изменяемом порядке, без изменения общего результата, но они не обеспечивают параллелизм , поскольку не выполняют несколько задач одновременно. Преимущества сопрограмм перед потоками заключаются в том, что их можно использовать в контексте жесткого реального времени ( переключение между сопрограммами не требует каких-либо системных вызовов или каких-либо блокирующих вызовов), нет необходимости в примитивах синхронизации, таких как мьютексы , семафоры и т. д. для защиты критических разделов и отсутствия необходимости в поддержке со стороны операционной системы.
Можно реализовать сопрограммы с использованием потоков с упреждающим планированием таким образом, чтобы это было прозрачно для вызывающего кода, но некоторые преимущества (в частности, пригодность для работы в жестком реальном времени и относительная дешевизна переключения между ними) будут потеряны.
Генераторы, также известные как полукорутины, [8] представляют собой подмножество сопрограмм. В частности, хотя обе могут выполнять несколько раз, приостанавливая свое выполнение и разрешая повторный вход в нескольких точках входа, они различаются способностью сопрограмм контролировать, где выполнение продолжается сразу после завершения, в то время как генераторы не могут, вместо этого передавая управление обратно вызывающей стороне генератора. . [9] То есть, поскольку генераторы в основном используются для упрощения написания итераторов , yield
оператор в генераторе не определяет сопрограмму для перехода, а скорее передает значение обратно родительской подпрограмме.
Тем не менее, по-прежнему возможно реализовать сопрограммы поверх генератора с помощью диспетчерской процедуры верхнего уровня ( по сути, трамплина ), которая явно передает управление дочерним генераторам, идентифицируемым токенами, переданными обратно от генераторов:
var q := новая очередьгенератор создает цикл , пока q не заполнен создать несколько новых предметов добавить элементы в q урожайцикл потребления генератора , пока q не пуст удалить некоторые элементы из q использовать предметы урожайдиспетчер подпрограмм var d := новый словарь( генератор → итератор ) d[производить] := начать производить d[consume] := начало потребления var current := создание тока вызова цикла текущий := следующий d[текущий]позвонить диспетчеру
Ряд реализаций сопрограмм для языков с поддержкой генераторов, но без собственных сопрограмм (например, Python [10] до версии 2.5) используют эту или аналогичную модель.
Использование сопрограмм для конечных автоматов или параллелизма аналогично использованию взаимной рекурсии с хвостовыми вызовами , поскольку в обоих случаях элемент управления меняется на другой из набора подпрограмм. Однако сопрограммы более гибки и, как правило, более эффективны. Поскольку сопрограммы возвращают результат, а не возвращают его, а затем возобновляют выполнение, а не перезапускают его с самого начала, они могут удерживать состояние как переменных (как при замыкании), так и точки выполнения, а выходы не ограничиваются нахождением в хвостовой позиции; взаимно рекурсивные подпрограммы должны либо использовать общие переменные, либо передавать состояние в качестве параметров. Кроме того, каждый взаимно рекурсивный вызов подпрограммы требует нового кадра стека (если не реализовано устранение хвостового вызова ), тогда как передача управления между сопрограммами использует существующие контексты и может быть реализована простым переходом.
Сопрограммы полезны для реализации следующего:
Сопрограммы возникли как метод языка ассемблера , но поддерживаются некоторыми языками программирования высокого уровня .
Поскольку для реализации сопрограмм можно использовать продолжения , поддерживающие их языки программирования также могут довольно легко поддерживать сопрограммы.
По состоянию на 2003 год [обновлять]многие из наиболее популярных языков программирования, включая C и его производные, не имеют встроенной поддержки сопрограмм внутри языка или его стандартных библиотек. Во многом это связано с ограничениями реализации подпрограмм на основе стека . Исключением является библиотека C++ Boost.Context, входящая в состав библиотек boost, которая поддерживает замену контекста в ARM, MIPS, PowerPC, SPARC и x86 в POSIX, Mac OS X и Windows. Сопрограммы могут быть построены на Boost.Context.
В ситуациях, когда сопрограмма была бы естественной реализацией механизма, но недоступна, типичным ответом является использование замыкания – подпрограммы с переменными состояния ( статическими переменными , часто логическими флагами) для поддержания внутреннего состояния между вызовами и передать управление в нужную точку. Условные выражения внутри кода приводят к выполнению различных путей кода при последовательных вызовах в зависимости от значений переменных состояния. Другой типичный ответ — реализация явного конечного автомата в форме большого и сложного оператора переключения или с помощью оператора перехода , в частности вычисляемого перехода . Такие реализации считаются трудными для понимания и поддержки и являются мотивацией для поддержки сопрограмм.
Потоки и, в меньшей степени , волокна являются сегодня альтернативой сопрограммам в основных средах программирования. Потоки предоставляют средства для управления совместным взаимодействием одновременно выполняющихся фрагментов кода в реальном времени. Потоки широко доступны в средах, поддерживающих C (и поддерживаются изначально во многих других современных языках), знакомы многим программистам и обычно хорошо реализованы, хорошо документированы и хорошо поддерживаются. Однако, поскольку они решают большую и сложную проблему, они включают в себя множество мощных и сложных средств и, соответственно, требуют сложного обучения. Таким образом, когда все, что необходимо, — это сопрограмма, использование потока может оказаться излишним.
Одним из важных различий между потоками и сопрограммами является то, что потоки обычно планируются заранее, а сопрограммы — нет. Поскольку потоки могут быть перепланированы в любой момент и могут выполняться одновременно, программы, использующие потоки, должны быть осторожны с блокировкой . Напротив, поскольку сопрограммы можно перепланировать только в определенных точках программы и они не выполняются одновременно, программы, использующие сопрограммы, часто могут полностью избежать блокировки. Это свойство также считается преимуществом событийно-управляемого или асинхронного программирования.
Поскольку волокна планируются совместно, они обеспечивают идеальную основу для реализации описанных выше сопрограмм. [23] Однако системная поддержка волокон часто отсутствует по сравнению с поддержкой потоков.
Для реализации сопрограмм общего назначения необходимо получить второй стек вызовов , что не поддерживается языком C напрямую . Надежный (хотя и специфичный для платформы) способ добиться этого — использовать небольшой объем встроенной сборки для явного управления указателем стека во время первоначального создания сопрограммы. Это подход, рекомендованный Томом Даффом в обсуждении его относительных преимуществ по сравнению с методом, используемым Protothreads . [24] [ необходим неосновной источник ] На платформах, которые предоставляют системный вызов sigaltstack POSIX , второй стек вызовов можно получить, вызвав функцию трамплина из обработчика сигнала [25] [26] для достижения той же цели в переносимых C, ценой некоторой дополнительной сложности. Библиотеки C, соответствующие POSIX или Единой спецификации Unix (SUSv3), предоставляют такие процедуры, как getcontext, setcontext, makecontext и swapcontext , но эти функции были объявлены устаревшими в POSIX 1.2008. [27]
Как только второй стек вызовов будет получен с помощью одного из перечисленных выше методов, функции setjmp и longjmp в стандартной библиотеке C можно использовать для реализации переключения между сопрограммами. Эти функции сохраняют и восстанавливают, соответственно, указатель стека , программный счетчик , регистры , сохраненные вызываемым абонентом , и любое другое внутреннее состояние, как того требует ABI , так что возврат к сопрограмме после возврата восстанавливает все состояние, которое будет восстановлено при возврате. из вызова функции. Минималистские реализации, которые не используют функции setjmp и longjmp, могут достичь того же результата с помощью небольшого блока встроенного ассемблера , который меняет местами только указатель стека и программный счетчик и затирает все остальные регистры. Это может быть значительно быстрее, поскольку setjmp и longjmp должны консервативно хранить все регистры, которые могут использоваться в соответствии с ABI, тогда как метод clobber позволяет компилятору хранить (путем передачи в стек) только то, что, как он знает, действительно используется.
Из-за отсутствия прямой языковой поддержки многие авторы написали свои собственные библиотеки для сопрограмм, которые скрывают вышеуказанные детали. Библиотека libtask Расса Кокса [28] является хорошим примером этого жанра. Он использует функции контекста, если они предоставляются встроенной библиотекой C; в противном случае он предоставляет свои собственные реализации для ARM, PowerPC, Sparc и x86. Другие известные реализации включают libpcl, [29] coro, [30] lthread, [31] libCoroutine, [32] libconcurrency, [33] libcoro, [34] ребра2, [35] libdill., [36] libaco, [37] и Либко. [26]
В дополнение к общему подходу, описанному выше, было предпринято несколько попыток аппроксимировать сопрограммы в C комбинациями подпрограмм и макросов. Вклад Саймона Тэтэма [38] , основанный на устройстве Даффа , является ярким примером этого жанра и лежит в основе Protothreads и подобных реализаций. [39] В дополнение к возражениям Даффа, [24] собственные комментарии Тэтэма дают откровенную оценку ограничений этого подхода: «Насколько мне известно, это худший образец хакерства на языке C, когда-либо виденный в серьезном производственном коде». [38] Основные недостатки этого приближения заключаются в том, что из-за отсутствия отдельного кадра стека для каждой сопрограммы локальные переменные не сохраняются при выходе из функции, невозможно иметь несколько записей в функции, а управление может только быть получено из подпрограммы верхнего уровня. [24]
В C# 2.0 добавлена функциональность полусопрограммы ( генератора ) посредством шаблона и yield
ключевого слова итератора. [44] [45] C# 5.0 включает поддержку синтаксиса ожидания . Кроме того:
Cloroutine — сторонняя библиотека, обеспечивающая поддержку бесстековых сопрограмм в Clojure . Он реализован как макрос, статически разбивающий произвольный блок кода на произвольные вызовы var и выдающий сопрограмму как функцию с состоянием.
D реализует сопрограммы в качестве класса стандартной библиотеки. Генератор Fiber A упрощает представление функции волокна в качестве входного диапазона , делая любое волокно совместимым с существующими алгоритмами диапазона.
В Go есть встроенная концепция « горутин », которые представляют собой легкие, независимые процессы, управляемые средой выполнения Go. Новую горутину можно запустить с помощью ключевого слова «go». Каждая горутина имеет стек переменного размера, который можно расширять по мере необходимости. Горутины обычно взаимодействуют, используя встроенные каналы Go. [46] [47] [48] [49]
В Java существует несколько реализаций сопрограмм . Несмотря на ограничения, налагаемые абстракциями Java, JVM не исключает такой возможности. [50] Используются четыре общих метода, но два из них нарушают переносимость байт-кода среди JVM, соответствующих стандартам.
Kotlin реализует сопрограммы как часть собственной библиотеки.
Lua поддерживает первоклассные стековые асимметричные сопрограммы, начиная с версии 5.0 (2003 г.), [52] в стандартной библиотеке coroutine . [53] [54]
Modula-2 , как определено Виртом , реализует сопрограммы как часть стандартной библиотеки SYSTEM.
Процедура NEWPROCESS() заполняет контекст, заданный блоком кода и пространством для стека в качестве параметров, а процедура TRANSFER() передает управление сопрограмме, учитывая контекст сопрограммы в качестве параметра.
Среда выполнения Mono Common Language поддерживает продолжения, [55] из которых могут быть построены сопрограммы.
Во время разработки .NET Framework 2.0 компания Microsoft расширила дизайн API-интерфейсов хостинга Common Language Runtime (CLR) для управления планированием на основе оптоволокна с прицелом на его использование в режиме оптоволокна для SQL-сервера. [56] Перед выпуском поддержка перехватчика переключения задач ICLRTask::SwitchOut была удалена из-за нехватки времени. [57] Следовательно, использование Fiber API для переключения задач в настоящее время не является жизнеспособным вариантом в .NET Framework. [ нужно обновить ]
OCaml поддерживает сопрограммы через свой Thread
модуль. [58] Эти сопрограммы обеспечивают параллелизм без параллелизма и заранее планируются для одного потока операционной системы. Начиная с OCaml 5.0, также доступны зеленые потоки ; обеспечивается различными модулями.
Сопрограммы встроены во все серверные части Raku . [59]
Racket предоставляет собственные продолжения с тривиальной реализацией сопрограмм, представленных в официальном каталоге пакетов. Реализация С. Де Габриэль
Поскольку Scheme обеспечивает полную поддержку продолжений, реализация сопрограмм почти тривиальна и требует только поддержания очереди продолжений.
Поскольку в большинстве сред Smalltalk стек выполнения является первоклассным гражданином, сопрограммы могут быть реализованы без дополнительной поддержки библиотек или виртуальных машин.
Начиная с версии 8.6, язык команд инструментов поддерживает сопрограммы на основном языке.[62]
Vala реализует встроенную поддержку сопрограмм. Они предназначены для использования с основным циклом Gtk, но могут использоваться отдельно, если позаботиться о том, чтобы конечный обратный вызов никогда не вызывался перед выполнением хотя бы одного выхода.
Машинно-зависимые языки ассемблера часто предоставляют прямые методы для выполнения сопрограмм. Например, в MACRO-11 , ассемблере семейства миникомпьютеров PDP-11 , «классическое» переключение сопрограммы осуществляется с помощью инструкции «JSR PC,@(SP)+», которая переходит по адресу, полученному из стек и помещает в стек адрес текущей ( т. е. следующей ) инструкции. В VAXen (в VAX MACRO ) сопоставимой инструкцией является «JSB @(SP)+». Даже на Motorola 6809 есть инструкция "JSR[,S++]"; обратите внимание на «++», поскольку 2 байта (адреса) извлекаются из стека. Эта инструкция часто используется в (стандартном) «мониторном» Assist 09.
6. Симметрия — это концепция снижения сложности (сопрограммы включают подпрограммы);
ищи это везде