Сопрограммы в C++: понимание оператора co_await

задняя часть Программа перевода самородков C++ Promise

Сопрограммы 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 есть много шагов, чтобы получить ожидание.

Предположим, объект обещания, ожидающий сопрограммы, имеет типPpromiseявляется ссылкой l-значения на объект обещания текущей сопрограммы.

если тип обещанияPEсть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,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.