C++1z контекст потоковой подпрограммы и планирование сопрограмм
В соответствии с этим последним C++ TS: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4628.pdf и основываясь на понимании языковой поддержки C# async/await, я интересно, что такое "контекст выполнения" (терминология, заимствованная из C#) сопрограмм C++?
Мой простой тестовый код в Visual C++ 2017 RC показывает, что сопрограммы, кажется, всегда выполняются в потоке пула потоков, и разработчику приложения предоставляется небольшой контроль над тем, в каком контексте потока могут быть выполнены сопрограммы - например, может ли приложение вызывать все сопрограммы (с кодом конечного автомата, сгенерированным компилятором), который должен выполняться только в основном потоке, без привлечения какого-либо потока пула потоков
В C# SynchronizationContext - это способ указать "контекст", в котором будут размещаться и выполняться все половинки сопрограммы (сгенерированный компилятором код конечного автомата), как показано в этом сообщении: https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/, хотя текущая реализация сопрограммы в Visual C++ 2017 RC, похоже, всегда полагается на среду выполнения параллелизма, которая по умолчанию выполняет сгенерированный код конечного автомата в поток пула потоков. Существует ли аналогичная концепция контекста синхронизации, которую пользовательское приложение может использовать для привязки выполнения сопрограммы к определенному потоку?
Кроме того, каково текущее поведение сопрограмм по умолчанию для "планировщика", реализованное в Visual C++ 2017 RC? т.е. 1) как именно указано условие ожидания? и 2) когда условие ожидания выполнено, кто вызывает "нижнюю половину" приостановленной сопрограммы?
Мое (наивное) предположение относительно планирования задач в C# состоит в том, что C# "реализует" условие ожидания исключительно путем продолжения задачи - условие ожидания синтезируется задачей, принадлежащей TaskCompletionSource, и любая логика кода, которая должна ждать, будет связана как продолжение это так, если условие ожидания удовлетворяется, например, если полное сетевое сообщение получено от низкоуровневого обработчика, выполняется TaskCompletionSource.SetValue, который переводит базовую задачу в завершенное состояние, эффективно позволяя логике цепочечного продолжения начать выполнение (перевод задачи в состояние / список готовности из предыдущего созданного состояния) - В сопрограмме C++ я предполагаю, что std::future и std::promise будут использоваться в качестве аналогичного механизма (std::future является задачей, а std:: обещание является TaskCompletionSource, и использование также удивительно похоже!) - так ли планировщик сопрограмм C++, если таковой имеется, полагается на некоторый подобный механизм для выполнения поведения?
[РЕДАКТИРОВАТЬ]: после некоторых дальнейших исследований я смог написать очень простую, но очень мощную абстракцию, называемую awaitable, которая поддерживает однопотоковую и кооперативную многозадачность и имеет простой планировщик на основе thread_local, который может выполнять сопрограммы в потоке корневую сопрограмму запущен Код можно найти в репозитории github: https://github.com/llint/Awaitable
Awaitable может быть составлен таким образом, что он поддерживает правильное упорядочение вызовов на вложенных уровнях, и он имеет примитивную выдачу, синхронизированное ожидание и настройку, готовые откуда-то еще, и из этого может быть получен очень сложный шаблон использования (такой как бесконечные циклические сопрограммы, которые только просыпаться, когда происходят определенные события), модель программирования тесно следует шаблону асинхронного / ожидающего выполнения C# Task. Пожалуйста, не стесняйтесь оставлять свои отзывы.
1 ответ
Противоположный!
Сопрограмма C++ - это все о контроле. ключевым моментом здесь являетсяvoid await_suspend(std::experimental::coroutine_handle<> handle)
функция.
Иви co_await
ожидает ожидаемого типа. Короче говоря, ожидаемый тип - это тип, который обеспечивает следующие три функции:
bool await_ready()
- должна ли программа остановить выполнение сопрограммы?void await_suspend(handle)
- программа передает вам контекст продолжения для этого фрейма сопрограммы. если вы активируете ручку (например, позвонивoperator ()
что дескриптор обеспечивает - текущий поток немедленно возобновляет сопрограмму).T await_resume()
- сообщает потоку, который возобновляет сопрограмму, что делать при возобновлении сопрограммы и что возвращать сco_await
,
поэтому, когда вы звоните co_await
на ожидаемом типе, программа спрашивает ожидающего, если сопрограмма должна быть приостановлена (если await_ready
возвращает false) и если да - вы получите ручку сопрограммы, в которой вы можете делать все что угодно.
например, вы можете передать дескриптор сопрограммы в пул потоков. в этом случае поток пула потоков возобновит сопрограмму.
Вы можете передать ручку сопрограммы к простому std::thread
- ваш собственный поток создания возобновит сопрограмму.
вы можете прикрепить дескриптор сопрограммы в производный класс OVERLAPPED
и возобновить сопрограмму после завершения асинхронного ввода-вывода.
как вы можете видеть - вы можете контролировать, где и когда сопрограмма приостанавливается и возобновляется - управляя ручкой сопрограммы, переданной в await_suspend
, "Планировщик по умолчанию" отсутствует - как вы реализуете свой ожидаемый тип, будет зависеть от того, как будет планироваться сопрограмма.
Итак, что происходит в VC++? к несчастью, 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();
}
Так почему же вы видели поток потоков win32, если сопрограммы запускаются в обычном режиме? std::thread
? это потому что это не была сопрограмма. std::async
за кулисами concurrency::create_task
, concurrency::task
по умолчанию запускается под пулом потоков win32. в конце концов, вся цель std::async
это запустить вызываемый в другом потоке.