С++ 1z coroutine контекст потока и расписание сопрограммы

В этом последнем С++ TS: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4628.pdf, и на основе понимания поддержки языка С# async/await мне интересно что такое "контекст выполнения" (терминология, заимствованная из С#) сопрограмм С++?

Мой простой тестовый код в Visual С++ 2017 RC показывает, что сопрограммы, как представляется, всегда выполняются в потоке пула потоков, и разработчику приложения не уделяется мало внимания, в котором контекст потоковой передачи может выполняться сопрограммами - например, Может ли приложение заставлять все сопрограммы (с кодом сгенерированного машинного кода компилятора) выполняться только в основном потоке, без привлечения потока нитей потока?

В С# SynchronizationContext - это способ указать "контекст", где все "половинки" coroutine (компилятор сгенерированный код конечного автомата) будут размещены и выполнены, как показано в этом сообщении: https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/, а текущая реализация coroutine в Visual С++ 2017 RC всегда полагается на среду выполнения concurrency, которая по умолчанию выполняет сгенерированный код конечного автомата в пуле потоков нить. Существует ли аналогичная концепция контекста синхронизации, которую пользовательское приложение может использовать для привязки выполнения coroutine к определенному потоку?

Кроме того, каков текущий по умолчанию "планировщик" поведения сопрограмм, реализованных в Visual С++ 2017 RC? т.е. 1) как точно указано условие ожидания? и 2) когда условие ожидания выполнено, кто вызывает "нижнюю половину" приостановленной сопрограммы?

Мое (наивное) предположение о планировании задач в С# заключается в том, что С# "реализует" условие ожидания чисто путем продолжения задачи - условие ожидания синтезируется задачей, принадлежащей TaskCompletionSource, и любая логика кода, которая должна ждать, будет скована как это продолжение, поэтому, если условие ожидания выполнено, например если полное сообщение получено от низкоуровневого сетевого обработчика, оно выполняет TaskCompletionSource.SetValue, которое переводит базовую задачу в завершенное состояние, эффективно позволяя логике продолжения цепочек запускаться (помещая задачу в состояние готовности/список из предыдущее созданное состояние). В С++ coroutine я предполагаю, что std:: future и std:: prom будут использоваться в качестве аналогичного механизма (std:: future - это задача, в то время как std:: prom - это объект TaskCompletionSource и использование тоже удивительно похоже!) - так ли планировщик С++ сопрограммы, если таковой имеется, полагается на какой-то аналогичный механизм для выполнения поведения?

[EDIT]: после некоторых дальнейших исследований я смог кодировать очень простую, но очень мощную абстракцию, которая называется awaitable, которая поддерживает однопоточную и совместную многозадачность, и имеет простой планировщик на основе thread_local, который может выполнять сопрограммы в потоке запускается корневая консоль. Код можно найти из этого github repo: https://github.com/llint/Awaitable

Ожидаемый композит таким образом, что он поддерживает правильное упорядочение вызовов на вложенных уровнях, и он имеет примитивный доход, время ожидания и готовность к настройке из другого места, и из этого может быть получен очень сложный шаблон использования (например, бесконечный цикл сопрограммы, которые только разбужаются, когда происходят определенные события), модель программирования следует за С# Task на основе async/await pattern. Пожалуйста, не стесняйтесь давать свои отзывы.

Ответ 1

Противоположность!

С++ coroutine - все о контроле. ключевым моментом здесь является
void await_suspend(std::experimental::coroutine_handle<> handle) функция.

evey co_await ожидает ожидаемого типа. в двух словах, ожидаемый тип - это тип, который обеспечивает эти три функции:

  • bool await_ready() - должна ли программа останавливать выполнение сопрограммы?
  • void await_suspend(handle) - программа передает вам контекст продолжения для этого кадра сопрограммы. если вы активируете дескриптор (например, вызывая operator (), который предоставляет дескриптор - текущий поток немедленно возобновляет сопрограмму).
  • T await_resume() - сообщает потоку, в котором возобновляется сопрограмма, что нужно делать при возобновлении сопрограммы и что вернуться с co_await.

поэтому, когда вы вызываете co_await в ожидаемом типе, программа запрашивает ожидание, если сопрограмма должна быть приостановлена ​​(если await_ready возвращает false), и если это так - вы получите ручку сопрограммы, в которой вы можете делать все, что вам нравится.

например, вы можете передать ручку coroutine в пул потоков. в этом случае поток потока пула возобновит сопрограмму.

вы можете передать дескриптор coroutine в простой std::thread - ваш поток собственный будет возобновлять сопрограмму.

вы можете привязать дескриптор сопрограммы к производному классу OVERLAPPED и возобновить сопрограмму при завершении асинхронного ввода-вывода.

как вы можете видеть - вы можете контролировать, где и когда сопрограмма приостановлена ​​и возобновляется, - управляя дескриптором сопрограммы в await_suspend. нет "планировщика по умолчанию" - как вы реализуете свой ожидаемый тип, выберете, как раскроется сопрограмма.

Итак, что происходит в VС++? К сожалению, std::future до сих пор не имеет функции then, поэтому не может передать дескриптор сопрограммы в std::future. если вы ждете в std::future - программа просто откроет новый поток. посмотрите исходный код, заданный заголовком future:

template<class _Ty>
    void await_suspend(future<_Ty>& _Fut,
        experimental::coroutine_handle<> _ResumeCb)
    {   // change to .then when future gets .then
    thread _WaitingThread([&_Fut, _ResumeCb]{
        _Fut.wait();
        _ResumeCb();
    });
    _WaitingThread.detach();
    } 

Итак, почему вы увидели threadflow-поток win32, если сопрограммы запущены в обычном std::thread? потому что это не сопрограмма. std::async вызывает за кулисами concurrency::create_task. a concurrency::task запускается под win32 threadpool по умолчанию. в конце концов, цель std::async - запустить вызываемый в другом потоке.