- Оригинальный адрес:C++ Coroutines: Understanding operator co_await
- Оригинальный автор:lewissbaker
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:7Ethan
- Корректор:razertory,noahziheng
Сопрограммы C++: пониманиеco_await
оператор
перед примерноБлог о теории сопрограмм, я рассмотрел некоторые высокоуровневые различия между функциями и сопрограммами, но не стал вдаваться в подробности спецификации сопрограмм C++ (N4680), как описано в синтаксисе и семантике.
Ключевой новой функцией, добавленной в C++ в спецификации сопрограммы, является возможность приостанавливать сопрограмму и возобновлять ее позже. Механизм, предусмотренный технической спецификацией для этого, заключается в новомco_await
оператор для реализации.
пониматьco_await
То, как работают операторы, может помочь нам демистифицировать поведение сопрограмм и понять, как они приостанавливаются и приостанавливаются. В этой статье я объяснюco_await
механизм оператора и введение вAwaitableиAwaiterТип связанных понятий.
объяснить подробноco_await
Перед этим я хотел бы кратко рассказать о технической спецификации сопрограмм, чтобы дать некоторый фон.
Что дает нам спецификация сопрограммы?
- Три новых ключевых слова:
co_await
,co_yield
иco_return
-
std::experimental
Несколько новых типов пространств имен:coroutine_handle<P>
coroutine_traits<Ts...>
suspend_always
suspend_never
- Общий механизм, позволяющий авторам библиотек взаимодействовать с сопрограммами и настраивать их поведение.
- Языковой инструмент, упрощающий асинхронный код!
Инструменты, предоставляемые спецификацией сопрограммы C++ в языке, можно понимать как средства сопрограммы.язык ассемблера низкого уровня. Эти инструменты трудно использовать напрямую безопасным образом, и они в первую очередь предназначены для использования авторами библиотек для создания абстракций более высокого уровня, которые могут безопасно использовать разработчики приложений.
Эти новые низкоуровневые инструменты будут включены в будущий языковой стандарт (вероятно, C++20) вместе с некоторыми сопутствующими высокоуровневыми типами в стандартной библиотеке, которые инкапсулируют эти низкоуровневые строительные блоки, и разработчики приложений быть в состоянии легко получить доступ к сопрограммам безопасным способом.
Взаимодействие компилятора и библиотеки
Интересно, что спецификация сопрограмм на самом деле не определяет семантику сопрограмм. Он не определяет, как сгенерировать значение, возвращаемое вызывающей стороне, он не определяет, как обрабатывать передачуco_return
Возвращаемое значение оператора, как обрабатывать исключения, переданные из сопрограммы, также не определяет поток, который должен возобновить сопрограмму.
Вместо этого он определяет общий механизм библиотечного кода для настройки поведения сопрограмм путем реализации типов, соответствующих конкретным интерфейсам. Затем компилятор генерирует код, который вызывает методы для экземпляров типов, предоставленных библиотекой. Этот подход похож на то, как авторы библиотек определяютbegin()
/ end()
метод илиiterator
Введите, чтобы настроить реализацию циклов for на основе диапазона.
Спецификация сопрограмм не предписывает какой-либо конкретной семантики для механики сопрограмм, что делает ее мощным инструментом. Это позволяет авторам библиотек определять множество различных видов сопрограмм для различных целей.
Например, вы можете определить сопрограмму, которая асинхронно генерирует одно значение, или сопрограмму, которая лениво генерирует последовательность значений, или, если вы столкнулись сnullopt
значение, упростите поток управления, выйдя раньше, чтобы потреблятьoptional <T>
значение сопрограммы.
Спецификация сопрограммы определяет два интерфейса:Promiseинтерфейс иAwaitableинтерфейс.
PromiseИнтерфейс определяет методы для настройки поведения самой сопрограммы. Авторы библиотек могут настраивать события, которые происходят при вызове сопрограммы, например, когда сопрограмма возвращается (либо обычными средствами, либо через необработанное исключение), или при настройке любогоco_await
илиco_yield
поведение выражения.
AwaitableИнтерфейс Обозначение Управлениеco_await
Методы семантики выражений. когда значениеco_await
, код транслируется в серию вызовов методов ожидаемого объекта. Он может указать: приостанавливать ли текущую сопрограмму, приостанавливать сопрограмму планирования для выполнения некоторой логики после возобновления позже и выполнять некоторую логику после возобновления сопрограммы для получения результата.co_await
результат выражения.
Я расскажу в будущем блогеPromiseДетали интерфейса, теперь давайте посмотримAwaitableИзвинение.
Awaiters и Awaitables: интерпретация операторовco_await
co_await
оператор — это новый унарный оператор, который можно применять к значению. Например:co_await someValue
.
co_await
Операторы можно использовать только в контексте сопрограммы. Это небольшое семантическое дублирование, потому что по определению любой содержащийco_await
Тело оператора будет скомпилировано как сопрограмма.
служба поддержкиco_await
Тип оператора называетсяAwaitableтип.
Уведомление,co_await
Можно ли использовать оператор в качестве типа, зависит отco_await
Контекст, в котором появляется выражение. Тип обещания, используемый для сопрограмм, может быть передан через егоawait_transform
изменения метода в сопрограммеco_await
Значение выражения (об этом позже).
Чтобы быть более конкретным там, где это необходимо, я предпочитаю использовать терминNormally Awaitableописать, что в типе сопрограммы нетawait_transform
Члены контекста сопрограммы поддерживаютсяco_await
Тип оператора. мне нравится использовать терминыContextually Awaitableдля описания типа, который поддерживается только в контексте определенных типов сопрограммco_await
оператор, потому что существует тип обещания сопрограммыawait_transform
метод. (Я открыт для лучших предложений для этих имен...)
AwaiterТип — это тип, который реализует три специальных метода, которые называютсяco_await
часть выражения:await_ready
,await_suspend
иawait_resume
.
Обратите внимание, что я на С#async
Термин «Официант» «заимствован» из механизма ключевого слова, основанного наGetAwaiter()
Реализуется методом, который возвращает объект, интерфейс которого такой же, как у C++.AwaiterПонятия поразительно похожи. Дополнительные сведения об ожидающих C# см.этот пост в блоге.
Обратите внимание, что тип может бытьAwaitableтип иAwaiterтип.
Когда компилятор встречаетco_await <expr>
выражение, на самом деле его можно преобразовать во множество возможных вещей в зависимости от задействованных типов.
Получить ожидающий
Первое, что делает компилятор, это генерирует код для получения значения ожидания.Awaiterобъект. В главе 5.3.8(3) N4680 есть много шагов, чтобы получить ожидание.
Предположим, объект обещания, ожидающий сопрограммы, имеет типP
,иpromise
является ссылкой l-значения на объект обещания текущей сопрограммы.
если тип обещанияP
Eстьawait_transform
членов, то<expr>
сначала передается наpromise.await_transform(<expr>)
чтобы получитьAwaitableзначение . В противном случае, если тип промиса не имеетawait_transform
члены, то мы используем прямую оценку<expr>
результат какAwaitableобъект.
Тогда, еслиAwaitableобъект с доступным операторомco_await()
перегружен, затем вызовите его, чтобы получитьAwaiterобъект. в противном случае,awaitable
Объект используется в качестве объекта ожидания.
Если мы закодируем эти правила вget_awaitable()
иget_awaiter()
функцию, они могут выглядеть так:
template<typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{
if constexpr (has_any_await_transform_member_v<P>)
return promise.await_transform(static_cast<T&&>(expr));
else
return static_cast<T&&>(expr);
}
template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{
if constexpr (has_member_operator_co_await_v<Awaitable>)
return static_cast<Awaitable&&>(awaitable).operator co_await();
else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)
return operator co_await(static_cast<Awaitable&&>(awaitable));
else
return static_cast<Awaitable&&>(awaitable);
}
В ожидании официанта
Поэтому предположим, что мы инкапсулировали<expr>
Результат преобразуется вAwaiterвозражайте против логики в приведенной выше функции, тогдаco_await <expr>
Семантика может быть (примерно) преобразована следующим образом:
{
auto&& value = <expr>;
auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
if (!awaiter.await_ready())
{
using handle_t = std::experimental::coroutine_handle<P>;
using await_suspend_result_t =
decltype(awaiter.await_suspend(handle_t::from_promise(p)));
<suspend-coroutine>
if constexpr (std::is_void_v<await_suspend_result_t>)
{
awaiter.await_suspend(handle_t::from_promise(p));
<return-to-caller-or-resumer>
}
else
{
static_assert(
std::is_same_v<await_suspend_result_t, bool>,
"await_suspend() must return 'void' or 'bool'.");
if (awaiter.await_suspend(handle_t::from_promise(p)))
{
<return-to-caller-or-resumer>
}
}
<resume-point>
}
return awaiter.await_resume();
}
когдаawait_suspend()
Когда звонок возвращается,await_suspend()
Возвращаемое значениеvoid
Вариант безоговорочной передачи выполнения обратно вызывающему/возобновляющему сопрограмме, а возвращаемое значение равноbool
, позволяет объекту ожидания возвращаться условно и немедленно возобновлять сопрограмму, не возвращая вызывающий объект/резюме.
await_suspen()
изbool
Возвращаемая версия полезна в ситуациях, когда ожидающий может запустить асинхронную операцию, которая иногда может завершиться синхронно. Сделав это синхронно,await_suspend()
метод может возвращатьfalse
чтобы указать, что сопрограмма должна немедленно возобновить и продолжить выполнение.
существует<suspend-coroutine>
, компилятор генерирует некоторый код для сохранения текущего состояния сопрограммы и подготовки ее к восстановлению. Это включает в себя хранение<resume-point>
, и перенесите любые значения, которые в данный момент хранятся в регистрах, в память моментального снимка сопрограммы.
существует<suspend-coroutine>
После завершения операции текущая сопрограмма считается приостановленной. Вы можете заметить, что первая точка останова приостановленной сопрограммы находится наawait_suspend()
в вызове. После приостановки сопрограммы ее можно возобновить или уничтожить.
Когда операция завершена,await_suspend()
Метод отвечает за планирование и возобновление (или уничтожение) сопрограммы в какой-то момент в будущем. Обратите внимание, что изawait_suspend()
вернуться вfalse
Считается планированием немедленного возобновления сопрограммы в текущем потоке.
await_ready()
Цель метода — позволить вам избежать ситуаций, когда известно, что операция завершается синхронно без необходимости приостановки.<suspend-coroutine>
стоимость операции.
существует<return-to-caller-or-resumer>
Выполнение в точке останова передается обратно вызывающей стороне или возобновлению, удаляя кадр локального стека, но сохраняя кадр сопрограммы.
Когда (или если) приостановленная сопрограмма наконец возобновится, выполнение начнется в<resume-point>
перезапустить в точке останова. то есть сразу после вызоваawait_resume()
метод, чтобы получить результат операции раньше.
await_resume()
Возвращаемое значение вызова метода становитсяco_await
результат выражения.await_resume()
Методы также могут генерировать исключения, и в этом случае исключениеco_await
бросил в выражение.
Обратите внимание, что если исключение начинается сawait_suspen()
брошено, сопрограмма автоматически возобновится, и исключение будет возвращено изco_await
выражение бросает без вызоваawait_resume()
.
дескриптор сопрограммы
вы могли заметитьcoroutine_handle <P>
использование типа, который передаетсяco_await
выразительныйawait_suspend()
перечислить.
Этот тип представляет невладеющий дескриптор кадра сопрограммы, который можно использовать для возобновления выполнения сопрограммы или уничтожения кадра сопрограммы. Его также можно использовать для доступа к объекту обещания сопрограммы.
coroutine_handle
Типы имеют следующие интерфейсы:
namespace std::experimental
{
template<typename Promise>
struct coroutine_handle;
template<>
struct coroutine_handle<void>
{
bool done() const;
void resume();
void destroy();
void* address() const;
static coroutine_handle from_address(void* address);
};
template<typename Promise>
struct coroutine_handle : coroutine_handle<void>
{
Promise& promise() const;
static coroutine_handle from_promise(Promise& promise);
static coroutine_handle from_address(void* address);
};
}
в реализацииAwaitableтип, вы будете вcoroutine_handle
Основной метод, используемый на.resume()
, который следует вызывать, когда операция завершена и вы хотите возобновить выполнение ожидаемой сопрограммы. существуетcoroutine_handle
звонить.resume()
будет<resume-point>
Разбудить приостановленную сопрограмму. Когда сопрограмма в следующий раз встречает<return-to-caller-or-resumer>
когда, да.resume()
вернусь.
.destroy()
Метод уничтожает фрейм сопрограммы, вызывая деструктор любых переменных в области видимости и освобождая память, используемую фреймом сопрограммы. Обычно вам не нужно (и на самом деле следует избегать) вызова.destroy()
, если только вы не являетесь автором библиотеки, реализующей типы промисов сопрограмм. Как правило, кадр сопрограммы будет принадлежать некоторому типу RAII, возвращаемому из вызова сопрограммы. Так что звоните без взаимодействия с объектом RAII.destroy()
Ошибка, которая могла вызвать двойное уничтожение.
.promise()
Метод возвращает ссылку на объект обещания сопрограммы. Однако, как.destroy()
Таким образом, обычно это полезно, только если вы создаете типы обещаний сопрограммы. Вы должны рассматривать объект обещания сопрограммы как внутреннюю деталь реализации сопрограммы. для большинстваОбычный Ожидаемыйтип, вы должны использоватьcoroutine_handle <void>
в видеawait_suspend()
тип параметра метода, а неcoroutine_handle <Promise>
.
coroutine_handle <P> :: from_promise(P&promise)
Функции позволяют реконструировать дескриптор сопрограммы из ссылки на объект обещания сопрограммы. Обратите внимание, что вы должны убедиться, что типP
Точно соответствует конкретному типу обещания, используемому для фрейма сопрограммы; когда конкретный тип обещанияDerived
при попытке построитьcoroutine_handle <Base>
Возникает неопределенная ошибка поведения.
.address()
/from_address()
функция позволяет конвертировать дескрипторы сопрограммы вvoid*
указатель. В основном это делается для того, чтобы разрешить передачу в качестве параметра «контекст» в существующие API-интерфейсы в стиле C, поэтому вы можете обнаружить, что в некоторых случаях реализацияAwaitableТипы полезны. Однако в большинстве случаев я считаю необходимым передать дополнительную информацию обратному вызову в этом параметре «контекст», поэтому я обычно получаюcoroutine_handle
сохранить в структуре и передать указатель на структуру в параметре «контекст» вместо использования.address()
возвращаемое значение.
Асинхронный код без синхронизации
co_await
Мощной конструктивной особенностью операторов является возможность выполнять код после приостановки сопрограммы, но до того, как выполнение вернется к вызывающей стороне/возобновлению.
Это позволяет объекту Awaiter инициировать асинхронные операции после того, как сопрограмма была приостановлена, (дескриптор) сопрограммы, которая будет приостановленаcoroutine_handle
Передано оператору, когда операция завершается (возможно, в другом потоке) он может спокойно возобновить операцию без какой-либо дополнительной синхронизации.
Например, когда сопрограмма уже приостановлена, вawait_suspend()
Инициирование асинхронной операции чтения внутри означает, что мы можем возобновить работу сопрограммы после завершения операции без какой-либо синхронизации потоков для координации потока, начавшего операцию, и потока, завершившего операцию.
Time Thread 1 Thread 2
| -------- --------
| .... Call OS - Wait for I/O event
| Call await_ready() |
| <supend-point> |
| Call await_suspend(handle) |
| Store handle in operation |
V Start AsyncFileRead ---+ V
+-----> <AsyncFileRead Completion Event>
Load coroutine_handle from operation
Call handle.resume()
<resume-point>
Call to await_resume()
execution continues....
Call to AsyncFileRead returns
Call to await_suspend() returns
<return-to-caller/resumer>
при использовании этого методаобращать вниманиеВо-первых, если вы запускаете операцию, которая публикует дескриптор сопрограммы в другом потоке, тогда другой поток можетawait_suspend()
Возобновите сопрограмму в другом потоке перед возвратом, продолжая сawait_suspend()
Остальная часть метода выполняется одновременно.
Первое, что нужно сделать, когда сопрограмма возобновит работу, это вызватьawait_resume()
чтобы получить результат, который зачастую сразу уничтожаетсяAwaiterобъект (т.await_suspend()
называетсяthis
указатель). существуетawait_suspend()
Перед возвратом сопрограмма может завершиться, уничтожив сопрограмму и объект обещания.
так вawait_suspend()
метод, если сопрограмма может быть возобновлена одновременно в другом потоке, вам необходимо убедиться, что вы избегаете доступаthis
указатель или сопрограмма.promise()
объект, так как оба, возможно, уже были уничтожены. Вообще говоря, после запуска операции и планирования возобновления сопрограммы единственное, к чему можно безопасно получить доступ, этоawait_suspend()
локальные переменные в .
Сравнение с сопрограммами Stackful
Я хотел бы сделать еще немного пояснений и сравнить способность бесстековых сопрограмм в спецификации сопрограммы выполнять логику после приостановки сопрограммы с некоторыми существующими распространенными инструментами сопрограммы (такими как волокна Win32 или boost::context ).
Для многих фреймворков со стеком сопрограмм операция приостановки одной сопрограммы сочетается с операцией возобновления другой сопрограммы, чтобы сформировать операцию «переключения контекста». При таком виде операции «переключение контекста», обычно после приостановки текущей сопрограммы, нет возможности выполнить логику до передачи выполнения другой сопрограмме.
Это означает, что если мы хотим реализовать аналогичные асинхронные операции чтения файлов поверх стековых сопрограмм, то мы должны приостановить сопрограмму.ДоНачать операцию. Поэтому можно приостановить сопрограммуДоОперация завершена в другом потоке и может быть возобновлена. Эта потенциальная гонка между операцией, выполняемой в другом потоке, и приостановкой сопрограммы требует какой-то синхронизации потоков для арбитража и определения победителя.
Эту проблему можно решить, используя контекст батута, который может инициировать операции от имени контекста запуска после того, как контекст инициализации был приостановлен. Однако для этого потребуется дополнительная инфраструктура и дополнительное переключение контекста, а связанные с этим накладные расходы могут перевесить затраты на попытку избежать синхронизации.
избегать выделения памяти
Асинхронным операциям часто требуется хранить некоторое состояние каждой операции, чтобы отслеживать ход операции. Это состояние обычно должно сохраняться в течение всей операции и освобождается только после завершения операции.
Например, вызов асинхронных функций ввода-вывода Win32 требует выделения и передачи указателя наOVERLAPPED
Указатель на структуру. Ответственность за то, чтобы этот указатель оставался действительным до завершения операции, лежит на вызывающем объекте.
В традиционных API на основе обратных вызовов это состояние обычно необходимо размещать в куче, чтобы обеспечить соответствующее время жизни. Если вы выполняете много операций, вам может потребоваться выделить и освободить это состояние для каждой операции. Если производительность становится проблемой, эти объекты состояния могут быть выделены из пула памяти с помощью специального распределителя.
В то же время при использовании сопрограммы мы можем избежать выделения памяти в куче для состояния операции, воспользовавшись тем, что локальные переменные в кадре сопрограммы останутся активными после приостановки сопрограммы.
Помещая каждое рабочее состояние вAwaiterобъект, мы можем эффективно «одолжить» память у фрейма сопрограммы для использования вco_await
Каждое рабочее состояние сохраняется на время выражения. После завершения операции сопрограмма возобновляется и уничтожается.Awaiterобъект, тем самым освобождая память в кадре сопрограммы для использования другими локальными переменными.
В конечном счете, кадры сопрограммы все еще могут быть размещены в куче. Однако после выделения кадр сопрограммы может использовать это выделение кучи для выполнения многих асинхронных операций.
Если подумать, фреймы сопрограммы подобны высокопроизводительному распределителю памяти арены. Компилятор вычисляет во время компиляции общий размер арены, необходимой для всех локальных переменных, а затем может выделять память локальным переменным по мере необходимости с нулевыми накладными расходами! Попробуйте побить его с помощью специального распределителя;)
Пример: реализация простых примитивов синхронизации потоков
Теперь, когда мы представилиco_await
Многие механизмы операторов, я хотел показать, как применить эти знания на практике, реализуя базовый примитив ожидаемой синхронизации: асинхронные события ручного сброса.
Основное требование этого события состоит в том, что оно должно быть выполнено несколькими одновременно выполняющимися сопрограммами, чтобы статьAwaitableСостояние при ожидании должно приостановить ожидающую сопрограмму, пока поток не вызовет.set()
метод, после чего все ожидающие сопрограммы возобновятся. Если поток вызвал.set()
, то сопрограмма должна продолжаться, а не приостанавливаться.
В идеале мы также хотели бы установить его вnoexcept
, не требует выделения памяти в куче и реализации без блокировки.
2017/11/23 Обновление: Добавленоasync_manual_reset_event
Пример
Пример использования показан ниже:
T value;
async_manual_reset_event event;
// A single call to produce a value
void producer()
{
value = some_long_running_computation();
// Publish the value by setting the event.
event.set();
}
// Supports multiple concurrent consumers
task<> consumer()
{
// Wait until the event is signalled by call to event.set()
// in the producer() function.
co_await event;
// Now it's safe to consume 'value'
// This is guaranteed to 'happen after' assignment to 'value'
std::cout << value << std::endl;
}
Давайте сначала рассмотрим состояния, в которых может существовать это событие:not set
иset
.
Когда он находится в состоянии «не установлено», существует очередь (возможно, пустых) сопрограмм, ожидающих перехода в состояние «установлено».
Когда он находится в состоянии «установлено», не будет никаких ожидающих сопрограмм, потому чтоco_wait
События в состоянии могут продолжаться без паузы.
Это состояние действительно может быть использовано сstd :: atomic <void *>
Представлять.
- Для состояния «установлено» зарезервировано специальное значение указателя. В этом случае мы будем использовать событие
this
указатель, как мы знаем, не может иметь тот же адрес, что и любой элемент списка. - В противном случае событие находится в состоянии «не установлено», а значение является указателем на начало односвязного списка ожидающих структур сопрограммы.
Мы можем избежать дополнительного вызова для выделения узла для связанного списка в куче, сохранив узел в объекте «ожидающий», помещенном внутри фрейма сопрограммы.
Начнем с интерфейса класса, подобного этому:
class async_manual_reset_event
{
public:
async_manual_reset_event(bool initiallySet = false) noexcept;
// No copying/moving
async_manual_reset_event(const async_manual_reset_event&) = delete;
async_manual_reset_event(async_manual_reset_event&&) = delete;
async_manual_reset_event& operator=(const async_manual_reset_event&) = delete;
async_manual_reset_event& operator=(async_manual_reset_event&&) = delete;
bool is_set() const noexcept;
struct awaiter;
awaiter operator co_await() const noexcept;
void set() noexcept;
void reset() noexcept;
private:
friend struct awaiter;
// - 'this' => set state
// - otherwise => not set, head of linked list of awaiter*.
mutable std::atomic<void*> m_state;
};
У нас довольно простой и понятный интерфейс. На данный момент беспокоит то, что он имеетoperator co_await()
метод, который возвращает еще не определенныйawaiter
тип.
Теперь давайте определимawaiter
тип
Определите тип ожидающего
Во-первых, ему нужно знать, какой из них он будет ждать.async_manual_reset_event
объект, поэтому для его инициализации требуется приложение этого события и соответствующий конструктор.
Он также должен действовать какawaiter
Узел в связанном списке значений, поэтому он должен содержать указатель на следующий в списке.awaiter
указатель на объект.
Это также требует, чтобы хранилище выполнялосьco_await
ожидаемая сопрограмма выраженияcoroutine_handle
, чтобы событие могло возобновить сопрограмму, когда событие переходит в состояние «установлено». Нам все равно, какой тип промиса у сопрограммы, поэтому мы просто используемcoroutine_handle <>
(Этоcoroutine_handle <void>
стенограмма).
Наконец, необходимо реализоватьAwaiterинтерфейс, и поэтому требует три специальных метода:await_ready
,await_suspend
иawait_resume
. нам не нужноco_await
выражение возвращает значение, поэтомуawait_resume
может вернутьсяvoid
.
Когда мы сложим все вместе,awaiter
Интерфейс базового класса выглядит так:
struct async_manual_reset_event::awaiter
{
awaiter(const async_manual_reset_event& event) noexcept
: m_event(event)
{}
bool await_ready() const noexcept;
bool await_suspend(std::experimental::coroutine_handle<> awaitingCoroutine) noexcept;
void await_resume() noexcept {}
private:
const async_manual_reset_event& m_event;
std::experimental::coroutine_handle<> m_awaitingCoroutine;
awaiter* m_next;
};
Теперь, когда мы выполняемco_await
Когда происходит событие, мы не хотим ждать, пока сопрограмма приостановится, если событие уже установлено. Итак, если событие уже установлено, мы можем определитьawait_ready()
возвращатьсяtrue
.
bool async_manual_reset_event::awaiter::await_ready() const noexcept
{
return m_event.is_set();
}
Далее давайте посмотрим наawait_suspend()
метод. Обычно именно здесь происходят необъяснимые вещи с ожидаемыми типами.
Во-первых, ему нужно сохранить дескриптор ожидающей сопрограммы вm_awaitingCoroutine
элемент, чтобы событие можно было позже вызвать для него.resume()
.
Затем, когда мы закончим с этим, нам нужно попытаться автоматически добавить ожидающего в список ожидающих. Если мы присоединяемся к нему успешно, то возвращаемсяtrue
, чтобы указать, что мы не хотим немедленно возобновлять сопрограмму, в противном случае, если мы обнаружим, что событие одновременно изменилось наset
состояние, то мы возвращаемсяfalse
, чтобы указать, что сопрограмма должна возобновиться немедленно.
bool async_manual_reset_event::awaiter::await_suspend(
std::experimental::coroutine_handle<> awaitingCoroutine) noexcept
{
// Special m_state value that indicates the event is in the 'set' state.
const void* const setState = &m_event;
// Remember the handle of the awaiting coroutine.
m_awaitingCoroutine = awaitingCoroutine;
// Try to atomically push this awaiter onto the front of the list.
void* oldValue = m_event.m_state.load(std::memory_order_acquire);
do
{
// Resume immediately if already in 'set' state.
if (oldValue == setState) return false;
// Update linked list to point at current head.
m_next = static_cast<awaiter*>(oldValue);
// Finally, try to swap the old list head, inserting this awaiter
// as the new list head.
} while (!m_event.m_state.compare_exchange_weak(
oldValue,
this,
std::memory_order_release,
std::memory_order_acquire));
// Successfully enqueued. Remain suspended.
return true;
}
Обратите внимание, что при загрузке старого состояния мы используем «acquire», чтобы увидеть порядок памяти, и если мы прочитаем специальное значение «set», то сможем увидеть записи, которые произошли до вызова «set()».
Если сравнение-обмен выполнено успешно, нам нужно состояние 'release', чтобы последующие вызовы 'set()' видели нашу запись в m_awaitingconoutine, а также предыдущую запись в состояние сопрограммы.
Завершение остальной части класса событий
Теперь мы определилиawaiter
типа, давайте вернемся и посмотримasync_manual_reset_event
реализация метода.
Во-первых, это конструктор. Его необходимо инициализировать до состояния «не установлено» и пустого списка ожидающих (т.е.nullptr
) или инициализируется в состояние «установлено» (т.е.this
).
async_manual_reset_event::async_manual_reset_event(
bool initiallySet) noexcept
: m_state(initiallySet ? this : nullptr)
{}
Следующий,is_set()
Метод очень простой - если он имеет особое значениеthis
, затем «установить»:
bool async_manual_reset_event::is_set() const noexcept
{
return m_state.load(std::memory_order_acquire) == this;
}
Послеreset()
метод, если он находится в состоянии «установлено», мы хотим, чтобы он перешел в состояние «не установлено», в противном случае оставьте его как есть.
void async_manual_reset_event::reset() noexcept
{
void* oldValue = this;
m_state.compare_exchange_strong(oldValue, nullptr, std::memory_order_acquire);
}
использоватьset()
метод, мы хотим использовать специальное значение 'set' (this
), чтобы перевести текущее состояние в состояние «установить», а затем проверить исходное значение. Если есть какие-либо ожидающие сопрограммы, мы хотим возобновить их последовательно перед возвратом.
void async_manual_reset_event::set() noexcept
{
// Needs to be 'release' so that subsequent 'co_await' has
// visibility of our prior writes.
// Needs to be 'acquire' so that we have visibility of prior
// writes by awaiting coroutines.
void* oldValue = m_state.exchange(this, std::memory_order_acq_rel);
if (oldValue != this)
{
// Wasn't already in 'set' state.
// Treat old value as head of a linked-list of waiters
// which we have now acquired and need to resume.
auto* waiters = static_cast<awaiter*>(oldValue);
while (waiters != nullptr)
{
// Read m_next before resuming the coroutine as resuming
// the coroutine will likely destroy the awaiter object.
auto* next = waiters->m_next;
waiters->m_awaitingCoroutine.resume();
waiters = next;
}
}
}
Наконец, нам нужно реализоватьoperator co_await()
метод. Для этого требуется только построитьawaiter
объект.
async_manual_reset_event::awaiter
async_manual_reset_event::operator co_await() const noexcept
{
return awaiter{ *this };
}
Мы, наконец, закончили с ожидаемым событием асинхронного ручного сброса, без блокировок, без выделения памяти,noexcept
выполнить.
Если вы хотите попробовать код или посмотреть, как он компилируется в MSVC и Clang ниже, посмотритеgodboltПосмотреть выше.
вы также можетеcppcoroРеализации этого класса можно найти в библиотеке, как и многие другие полезные ожидаемые типы, такие какasync_mutex
иasync_auto_reset_event
.
Эпилог
В этой статье описывается, какAwaitableиAwaiterРеализация концепции и определение операторовco_await
.
В нем также описывается, как реализовать ожидаемый примитив синхронизации асинхронного потока, который использует тот факт, что объекты ожидания размещаются в кадрах сопрограммы, чтобы избежать дополнительных выделений кучи.
Надеюсь, эта статья помогла вамco_await
Этот новый оператор лучше понят.
В следующем блоге я буду исследоватьPromiseКонцепции и то, как авторы типов сопрограмм могут настраивать поведение своих сопрограмм.
Спасибо
Я особенно хотел бы поблагодарить Гора Нишанова за то, что он терпеливо и с энтузиазмом отвечал на мои многочисленные вопросы о сопрограммах в течение последних нескольких лет.
Кроме того, Эрик Ниблер просмотрел и предоставил отзывы о ранних черновиках этой статьи.
Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.