Сопрограммы в С++

395
08 августа 2017, 16:29

Недавно техническая спецификация сопрограмм, Coroutines TS, дошла до состояния "опубликована". Сопрограммы реализованы в MS VC++ 2017.
Что это такое и как их писать?

Answer 1

В С++ сопрограммой называется функция, в которой используются co_await, co_yield, co_return.

Future coroutine(X x) {
  Y y = co_await f(x);
  co_return y;
}

Компилятор переписывает тело сопрограммы, превращая ее в машину состояний.
Под данные сопрограммы выделяется память при помощи оператора new.

Future coroutine(X x) {
  struct CoroutineState {
    Future::promise_type p;
    X x;
    Y y;
    int state = 0;
    void run() {
      switch (state) {
      case 0:
        ...
        state = 1;  // приостановка
        return;
      case 1:       // точка возобновления
        ...
      };
    }
  };
  auto* s = new CoroutineState;
  auto result = s->p.get_return_object();
  s->x = x;
  s->run();
  return result;
}

При этом, хотя тело сопрограммы изменяется, она остается функцией.

promise_type (ч.1)

Компилятор добавляет неявную переменную с типом Future::promise_type.
Эта переменная используется для создания результата функции-сопрограммы (p.get_return_object), обработки исключений, реализации co_return. Также компилятор добавляет точки приостановки в начале и конце сопрограммы, оборачивая тело сопрограммы в следующий код:

  Future::promise_type p;
  co_await p.initial_suspend();
  try {
    // тело сопрограммы
    Y y = co_await f(x);
    co_return y;
    // конец тела сопрограммы
  } catch(...) { p.unhandled_exception(); }
final_suspend:
  co_await p.final_suspend();

co_return

В сопрограмме нельзя использовать return y;, вместо него используется co_return y;, который заменяется на

p.return_value(y);
goto final_suspend;

Если сопрограмма не предусматривает возврат значения по завершении, то используется co_return; (без выражения) и соответствующая ему функция p.return_void();.
При этом не обязательно писать co_return; в конце сопрограммы.

co_await

Приостановка сопрограммы происходит в операторе co_await.
Код Y y = co_await f(x); заменяется на

auto e = f(x);
if (!e.await_ready()) {
  ... приостановка ...
  std::experimental::coroutine_handle<> h = ...;
  if (e.await_suspend(h)) return;
resume:  // точка возобновления для h.resume()
  ... возобновление ...
}
Y y = e.await_resume();

Стандартная библиотека предоставляет класс coroutine_handle, который позволяет возобновить приостановленную сопрограмму.
Функция f принимает его через e.await_suspend(h). Когда значение y будет вычислено, она должна вызвать h.resume(), и вернуть вычисленное значение через e.await_resume().

Для stackless сопрограммы, функия f может быть написана следующим образом:

// Общие данные фонового потока и Awaiter.
struct SharedState {
  std::experimental::coroutine_handle<> h;
  Y value;
  std::atomic<bool> is_ready;
};
// Тип результата f
struct Awaiter {
  std::shared_ptr<SharedState> s;
  bool await_ready() { return false; }
  bool await_suspend(std::experimental::coroutine_handle<> h) {
    s->h = h;
    return !s->is_ready.exchange(true);  // True если фоновый поток уже завершился
                                         // и у нас есть s->value
  }
  Y await_resume() { return s->value; }
};
Awaiter f(X x) {
  auto s = std::make_shared<SharedState>();
  std::thread([=]{  // Запуск фонового потока для вычислений
    s->value = ...;
    if (s->is_ready.exchange(true)) {  // True, если await_suspend уже была вызвана
                                       // и у нас есть s->h
      s->h.resume();
    }
  }).detach();
  return Awaiter{s};
}

Для stackful сопрограммы (например Fibers в Windows), await_suspend должна сама замораживать поток (SwitchToFiber). Точка возобновления будет внутри await_suspend, поэтому онна должна возвращать false.

promise_type (ч.2)

Минимальный тип возвращаемого значения сопрограммы выглядит так:

struct Future {
  struct promise_type {
    Future get_return_object() { return {this}; }
    std::experimental::suspend_never initial_suspend() { return {}; }
    std::experimental::suspend_always final_suspend() { return {}; }
    void return_value(Y& y) { y_ptr = &y; }
    std::atomic<Y*> y_ptr = nullptr;
  };
  std::shared_ptr<promise_type> promise;
  static void Deleter(promise_type* p) {
    auto h = std::experimental::coroutine_handle<P>::from_promise(*p);
    h.destroy();  // удаляет CoroutineState
  }
  Future(promise_type* p) : promise(p, Deleter) {}
  Y BlockingGet() {
    while (promise->y_ptr == nullptr) Sleep(1);  // ждем
    return *promise->y_ptr;  // дождались вызова p.return_value(y)
  }
};

Future::promise_type должен иметь get_return_object, initial_suspend, final_suspend и либо return_void либо return_value.

Для реализации initial_suspend и final_suspend можно использовать стандартные suspend_never и suspend_always, которые возвращают в await_ready значения true и false соответственно.

Такая сопрограмма будет всегда засыпать в конце.

От самого Future требуется только чтобы он удалил сопрограмму через h.destroy().

Future может (но не обязан) повторять интерфейс Awaiter, чтобы быть совместимым с co_await.

await_transform и operator co_await

Для выражения a в co_await a могут применяться дополнительные преобразования:

  • если выражение p.await_transform(a) валидно, то a заменяется на p.await_transform(a);
  • если для типа a есть оператор operator co_await, то a заменяется на operator co_await(a);
  • если в результате получилось prvalue, то оно копируется во временную переменную, иначе используется как есть.

Таким образом возможен вариант

auto& e = operator co_await(p.await_transform(f(x)));
if (!e.await_ready()) { ... }

Например можно определить operator co_await(std::chrono::duration) и писать co_await 10ms;.

Выделение памяти

Объект CoroutineState создается при помощи new. Однако, если есть функция p.get_return_object_on_allocation_failure(), то будет сгенерирован следующий код:

auto* s = new(std::nothrow) CoroutineState;
if (!s) {
  return p.get_return_object_on_allocation_failure();
}
auto result = s->p.get_return_object();

Это позволяет обрабатывать ошибки выделения памяти.

Также, аргументы сопрограммы могут участвовать в выделении памяти.
Для сопрограммы Future coro(A1 a1, A2 a2), если есть функция operator new(std::size_t, A1, A2), то она будет вызвана вместо оператора new по умолчанию.

promise_type (ч.3), coroutine_traits

Компилятор использует класс coroutine_traits для получения promise_type.

Для сопрограммы Future coro(A1 a1, A2 a2) будет использован тип std::experimental::coroutine_traits<Future, A1, A2>::promise_type.

Реализация по-умолчанию выдает Future::promise_type, однако это может быть переопределено пользователем.

co_yield

co_yield e; эквивалентен co_await p.yield_value(e); и используется в генераторах - специальных сопрограммах которые предназначены для выдачи последовательности значений.

READ ALSO
GetModuleFileNameEx - ERROR_PARTIAL_COPY

GetModuleFileNameEx - ERROR_PARTIAL_COPY

GetModuleFileNameEx частенько возвращает 0GetLastError говорит об ошибке 299 (ERROR_PARTIAL_COPY - only part of a ReadProcessMemory or WriteProcessMemory request was completed)

505
В чём может быть проблема?

В чём может быть проблема?

Вот код на C++На этой строчке выдаются такие ошибки:

353
ретрансляция трафика

ретрансляция трафика

Как произвести ретрансляцию трафика через сокеты в Boost AsioПытаюсь реализовать что то вроде сервера, к которому подключаются клиенты с одной...

414
Нейросеть для распознавания чисел Qt C++ [требует правки]

Нейросеть для распознавания чисел Qt C++ [требует правки]

Здравствуйте! Появилась задача - сделать нейросеть, которая будет распознавать числа от 1 до 15 на С++ QtНикогда раньше не имел с этим дело

381