Сопрограммы — это компоненты компьютерной программы , которые позволяют приостанавливать и возобновлять выполнение, обобщая подпрограммы для кооперативной многозадачности . Сопрограммы хорошо подходят для реализации знакомых компонентов программы, таких как кооперативные задачи , исключения , циклы событий , итераторы , бесконечные списки и каналы .
Их описывают как «функции, выполнение которых можно приостановить» [1] .
Мелвин Конвей ввел термин сопрограмма в 1958 году, когда применил его к построению ассемблерной программы . [2] Первое опубликованное объяснение сопрограммы появилось позже, в 1963 году. [3]
Не существует единого точного определения сопрограммы. В 1980 году Кристофер Д. Марлин [4] обобщил две широко признанные фундаментальные характеристики сопрограммы:
Помимо этого, реализация сопрограммы имеет 3 особенности:
yield
и resume
. Программисты не могут свободно выбирать, какому кадру уступить управление. Среда выполнения уступает управление только ближайшему вызывающему объекту текущей сопрограммы. С другой стороны, в симметричных сопрограммах программисты должны указать назначение уступки.yield
.В статье «Revisiting Coroutines» [5], опубликованной в 2009 году, был предложен термин «полная сопрограмма» для обозначения той, которая поддерживает первоклассную сопрограмму и является стековой. Полные сопрограммы заслуживают своего собственного названия, поскольку они обладают той же выразительной силой , что и одноразовые продолжения и продолжения с разделителями. Полные сопрограммы бывают либо симметричными, либо асимметричными. Важно отметить, что симметричность или асимметричность сопрограммы не влияет на ее выразительность, поскольку они одинаково выразительны, хотя полные сопрограммы более выразительны, чем неполные сопрограммы. Хотя их выразительная сила одинакова, асимметричные сопрограммы больше похожи на структуры управления на основе подпрограмм в том смысле, что управление всегда передается обратно вызывающему, что может показаться программистам более знакомым.
Подпрограммы являются особыми случаями сопрограмм. [6] Когда вызываются подпрограммы, выполнение начинается с начала, и как только подпрограмма завершается, она завершается; экземпляр подпрограммы возвращается только один раз и не сохраняет состояние между вызовами. Напротив, сопрограммы могут завершаться, вызывая другие сопрограммы, которые позже могут вернуться в точку, где они были вызваны в исходной сопрограмме; с точки зрения сопрограммы это не выход, а вызов другой сопрограммы. [6] Таким образом, экземпляр сопрограммы сохраняет состояние и изменяется между вызовами; может быть несколько экземпляров данной сопрограммы одновременно. Разница между вызовом другой сопрограммы посредством «уступки» ей и простым вызовом другой подпрограммы (которая затем также вернется в исходную точку), заключается в том, что отношение между двумя сопрограммами, которые уступают друг другу, не является отношением вызывающего-вызываемого, а является симметричным.
Любая подпрограмма может быть преобразована в сопрограмму, которая не вызывает yield . [7]
Вот простой пример того, как сопрограммы могут быть полезны. Предположим, у вас есть отношение «потребитель-производитель», где одна процедура создает элементы и добавляет их в очередь, а другая удаляет элементы из очереди и использует их. Из соображений эффективности вы хотите добавлять и удалять несколько элементов одновременно. Код может выглядеть так:
var q := новая очередьсопрограмма производит цикл , пока q не заполнен создать несколько новых элементов добавить элементы в q выход на потреблениесопрограмма потребляет цикл , пока q не пуст удалить некоторые элементы из q использовать предметы выход на производствовызов производить
Затем очередь полностью заполняется или опустошается перед передачей управления другой сопрограмме с помощью команды yield . Дальнейшие вызовы сопрограмм начинаются сразу после yield , во внешнем цикле сопрограммы.
Хотя этот пример часто используется в качестве введения в многопоточность , для этого не нужны два потока: оператор yield можно реализовать путем прямого перехода из одной процедуры в другую.
Coroutines очень похожи на threads . Однако coroutines кооперативно многозадачны , тогда как threads обычно вытесняюще многозадачны . Coroutines обеспечивают concurrency , поскольку они позволяют выполнять задачи вне порядка или в изменяемом порядке, не меняя общий результат, но они не обеспечивают parallelism , поскольку они не выполняют несколько задач одновременно. Преимущества coroutines перед threads заключаются в том, что они могут использоваться в контексте жесткого реального времени ( переключение между coroutines не требует каких-либо системных вызовов или блокирующих вызовов вообще), нет необходимости в примитивах синхронизации, таких как мьютексы , семафоры и т. д. для защиты критических секций , и нет необходимости в поддержке со стороны операционной системы.
Можно реализовать сопрограммы с использованием потоков с упреждающим планированием таким образом, чтобы это было прозрачно для вызывающего кода, но некоторые преимущества (в частности, пригодность для работы в режиме жесткого реального времени и относительная дешевизна переключения между ними) будут потеряны.
Генераторы, также известные как полусопрограммы, [8] являются подмножеством сопрограмм. В частности, хотя обе могут уступать несколько раз, приостанавливая свое выполнение и допуская повторный вход в нескольких точках входа, они отличаются способностью сопрограмм контролировать, где выполнение продолжается сразу после того, как они уступают, в то время как генераторы не могут, вместо этого передавая управление обратно вызывающему генератору. [9] То есть, поскольку генераторы в основном используются для упрощения написания итераторов , yield
оператор в генераторе не указывает сопрограмму для перехода, а скорее передает значение обратно родительской подпрограмме.
Однако все еще возможно реализовать сопрограммы поверх генератора с помощью диспетчерской процедуры верхнего уровня ( по сути, батута ), которая явно передает управление дочерним генераторам, идентифицированным токенами, переданными обратно от генераторов:
var q := новая очередьгенератор производит цикл , пока q не заполнен создать несколько новых элементов добавить элементы в q урожайгенератор потребляет цикл , пока q не пуст удалить некоторые элементы из q использовать предметы урожайдиспетчер подпрограмм var d := новый словарь( генератор → итератор ) d[produce] := начать потреблять d[consum] := start produce var current := produce loop call current текущий := следующий d[текущий]вызов диспетчера
Ряд реализаций сопрограмм для языков с поддержкой генераторов, но без собственных сопрограмм (например, Python [10] до версии 2.5) используют эту или подобную модель.
Использование сопрограмм для конечных автоматов или параллелизма похоже на использование взаимной рекурсии с хвостовыми вызовами , так как в обоих случаях управление переходит к другой подпрограмме из набора. Однако сопрограммы более гибкие и, как правило, более эффективные. Поскольку сопрограммы уступают, а не возвращаются, а затем возобновляют выполнение, а не перезапускаются с самого начала, они могут удерживать состояние, обе переменные (как в замыкании) и точку выполнения, а уступки не ограничиваются нахождением в хвостовой позиции; взаимно рекурсивные подпрограммы должны либо использовать общие переменные, либо передавать состояние в качестве параметров. Кроме того, каждый взаимно рекурсивный вызов подпрограммы требует нового стекового кадра (если не реализовано исключение хвостового вызова ), в то время как передача управления между сопрограммами использует существующие контексты и может быть реализована просто с помощью перехода.
Корутины полезны для реализации следующего:
Сопрограммы возникли как метод языка ассемблера , но поддерживаются в некоторых языках программирования высокого уровня .
Поскольку продолжения могут использоваться для реализации сопрограмм, языки программирования, которые их поддерживают, также могут довольно легко поддерживать сопрограммы.
По состоянию на 2003 год [обновлять]многие из самых популярных языков программирования, включая C и его производные, не имеют встроенной поддержки сопрограмм в языке или их стандартных библиотеках. Это во многом связано с ограничениями реализации подпрограмм на основе стека . Исключением является библиотека C++ Boost.Context, часть библиотек boost, которая поддерживает подкачку контекста на ARM, MIPS, PowerPC, SPARC и x86 на POSIX, Mac OS X и Windows. Сопрограммы могут быть построены на основе Boost.Context.
В ситуациях, когда сопрограмма была бы естественной реализацией механизма, но недоступна, типичным ответом является использование замыкания — подпрограммы с переменными состояния ( статическими переменными , часто булевыми флагами) для поддержания внутреннего состояния между вызовами и передачи управления в правильную точку. Условные операторы в коде приводят к выполнению различных путей кода при последовательных вызовах на основе значений переменных состояния. Другим типичным ответом является реализация явной конечной машины в форме большого и сложного оператора switch или через оператор goto , в частности, вычисляемый goto . Такие реализации считаются сложными для понимания и поддержки, и это мотивирует поддержку сопрограмм.
Потоки и в меньшей степени волокна являются альтернативой сопрограммам в современных средах программирования. Потоки предоставляют возможности для управления совместным взаимодействием в реальном времени одновременно выполняемых фрагментов кода. Потоки широко доступны в средах, которые поддерживают C (и поддерживаются изначально во многих других современных языках), знакомы многим программистам и обычно хорошо реализованы, хорошо документированы и хорошо поддерживаются. Однако, поскольку они решают большую и сложную задачу, они включают в себя множество мощных и сложных средств и имеют соответственно сложную кривую обучения. Таким образом, когда сопрограмма — это все, что нужно, использование потока может быть излишним.
Одно важное различие между потоками и сопрограммами заключается в том, что потоки обычно планируются с вытеснением, а сопрограммы — нет. Поскольку потоки могут быть перепланированы в любой момент и могут выполняться одновременно, программы, использующие потоки, должны быть осторожны с блокировкой . Напротив, поскольку сопрограммы могут быть перепланированы только в определенных точках программы и не выполняются одновременно, программы, использующие сопрограммы, часто могут полностью избежать блокировки. Это свойство также упоминается как преимущество событийно-управляемого или асинхронного программирования.
Поскольку волокна кооперативно планируются, они обеспечивают идеальную базу для реализации сопрограмм, описанных выше. [23] Однако системная поддержка волокон часто отсутствует по сравнению с поддержкой потоков.
Для реализации сопрограмм общего назначения необходимо получить второй стек вызовов , что является функцией, которая напрямую не поддерживается языком C. Надежным (хотя и специфичным для платформы) способом достижения этого является использование небольшого количества встроенного ассемблера для явного манипулирования указателем стека во время первоначального создания сопрограммы. Этот подход рекомендовал Том Дафф в обсуждении его относительных достоинств по сравнению с методом, используемым Protothreads . [24] [ необходим неосновной источник ] На платформах, которые предоставляют системный вызов POSIX sigaltstack, второй стек вызовов можно получить, вызвав функцию springboard из обработчика сигналов [25] [26] для достижения той же цели в переносимом C, ценой некоторой дополнительной сложности. Библиотеки C, соответствующие POSIX или Single Unix Specification (SUSv3), предоставляли такие процедуры, как getcontext, setcontext, makecontext и swapcontext , но эти функции были объявлены устаревшими в POSIX 1.2008. [27]
После получения второго стека вызовов одним из перечисленных выше методов функции setjmp и longjmp в стандартной библиотеке C могут быть использованы для реализации переключений между сопрограммами. Эти функции сохраняют и восстанавливают, соответственно, указатель стека , счетчик программ , сохраненные вызываемым объектом регистры и любое другое внутреннее состояние, требуемое ABI , так что возврат к сопрограмме после выполнения yield восстанавливает все состояние, которое было бы восстановлено при возврате из вызова функции. Минималистские реализации, которые не используют функции 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] ribs2, [35] libdill., [36] libaco, [37] и libco. [26]
В дополнение к общему подходу, описанному выше, было сделано несколько попыток аппроксимировать сопрограммы в C с помощью комбинаций подпрограмм и макросов. Вклад Саймона Тэтхэма [38], основанный на устройстве Даффа , является ярким примером жанра и является основой для Protothreads и подобных реализаций. [39] В дополнение к возражениям Даффа, [24] собственные комментарии Тэтхэма дают откровенную оценку ограничений этого подхода: «Насколько мне известно, это худший пример хакерства на C, когда-либо виденный в серьезном производственном коде». [38] Главные недостатки этого приближения заключаются в том, что при отсутствии поддержки отдельного стекового фрейма для каждой сопрограммы локальные переменные не сохраняются между выходами из функции, невозможно иметь несколько записей в функцию, и управление может быть получено только из процедуры верхнего уровня. [24]
C# 2.0 добавил функциональность полукорутины ( генератора ) через шаблон итератора и yield
ключевое слово. [44] [45] C# 5.0 включает поддержку синтаксиса await . Кроме того:
Cloroutine — сторонняя библиотека, обеспечивающая поддержку стековых сопрограмм в Clojure . Она реализована как макрос, статически разделяющий произвольный блок кода на произвольные вызовы var и выдающий сопрограмму как функцию с сохранением состояния.
D реализует сопрограммы в качестве стандартного библиотечного класса Fiber. Генератор упрощает представление функции волокна в качестве входного диапазона , делая любое волокно совместимым с существующими алгоритмами диапазона.
Go имеет встроенную концепцию « goroutines », которые являются легкими, независимыми процессами, управляемыми средой выполнения Go. Новая goroutine может быть запущена с помощью ключевого слова «go». Каждая goroutine имеет стек переменного размера, который может быть расширен по мере необходимости. Goroutines обычно взаимодействуют с помощью встроенных каналов Go. [46] [47] [48] [49] Однако goroutines не являются coroutines (например, локальные данные не сохраняются между последовательными вызовами). [50]
Существует несколько реализаций сопрограмм в Java . Несмотря на ограничения, накладываемые абстракциями Java, JVM не исключает эту возможность. [51] Существует четыре общих метода, но два из них нарушают переносимость байт-кода среди JVM, соответствующих стандартам.
Начиная с ECMAScript 2015 , JavaScript поддерживает генераторы , которые являются особым случаем сопрограмм. [53]
Kotlin реализует сопрограммы как часть библиотеки собственного производства.
Lua поддерживает первоклассные стековые асимметричные сопрограммы с версии 5.0 (2003), [54] в стандартной библиотеке coroutine . [55] [56]
Modula-2, как определено Виртом, реализует сопрограммы как часть стандартной библиотеки SYSTEM.
Процедура NEWPROCESS() заполняет контекст, используя блок кода и пространство для стека в качестве параметров, а процедура TRANSFER() передает управление сопрограмме, используя контекст сопрограммы в качестве параметра.
Среда выполнения Mono Common Language Runtime поддерживает продолжения, [57] из которых можно строить сопрограммы.
Во время разработки .NET Framework 2.0 компания Microsoft расширила дизайн API-интерфейсов хостинга Common Language Runtime (CLR) для обработки планирования на основе волокон с прицелом на его использование в режиме волокон для SQL-сервера. [58] Перед выпуском поддержка хука переключения задач ICLRTask::SwitchOut была удалена из-за ограничений по времени. [59] Следовательно, использование API-интерфейса волокон для переключения задач в настоящее время не является жизнеспособным вариантом в .NET Framework. [ требуется обновление ]
OCaml поддерживает сопрограммы через свой Thread
модуль. [60] Эти сопрограммы обеспечивают параллелизм без параллелизма и планируются с упреждением в одном потоке операционной системы. Начиная с OCaml 5.0, также доступны зеленые потоки ; предоставляются различными модулями.
Сопрограммы изначально реализованы во всех бэкэндах Raku . [61]
Racket предоставляет собственные продолжения с тривиальной реализацией сопрограмм, представленной в официальном каталоге пакетов. Реализация от S. De Gabrielle
Поскольку Scheme обеспечивает полную поддержку продолжений, реализация сопрограмм практически тривиальна и требует только поддержания очереди продолжений.
Поскольку в большинстве сред Smalltalk стек выполнения является важнейшим компонентом, сопрограммы можно реализовать без дополнительной поддержки библиотек или виртуальных машин.
Начиная с версии 8.6, язык команд инструментов поддерживает сопрограммы в базовом языке. [64]
Vala реализует собственную поддержку сопрограмм. Они предназначены для использования с основным циклом Gtk, но могут использоваться и отдельно, если принять меры, чтобы гарантировать, что обратный вызов end никогда не придется вызывать до выполнения хотя бы одного yield.
Машинно-зависимые языки ассемблера часто предоставляют прямые методы для выполнения сопрограмм. Например, в MACRO-11 , языке ассемблера семейства мини-компьютеров PDP-11 , «классическое» переключение сопрограмм осуществляется инструкцией «JSR PC,@(SP)+», которая переходит на адрес, извлеченный из стека, и помещает текущий ( т. е. адрес следующей ) инструкции в стек. На VAXen (в VAX MACRO ) сопоставимая инструкция — «JSB @(SP)+». Даже на Motorola 6809 есть инструкция «JSR [,S++]»; обратите внимание на «++», поскольку 2 байта (адреса) извлекаются из стека. Эта инструкция часто используется в (стандартном) «мониторе» Assist 09.
6. Симметрия — это концепция снижения сложности (сопрограммы включают подпрограммы); ищите ее везде
{{cite web}}
: CS1 maint: неподходящий URL ( ссылка )