Что это означает для бесстековой сопрограммы только процедура верхнего уровня может быть приостановлена.

Я нашел это заявление здесь. Сначала я был удивлен, потому что считаю, что это делает бесстековые сопрограммы почти бесполезными (а TS сопрограмм C ++ не имеет стека). Итак, я написал демонстрацию (в Visual Studio с использованием C ++ сопрограммы TS):

#include<experimental/coroutine>
#include<iostream>
#include<thread>
#include<mutex>
#include<future>
#include<chrono>

using namespace std;
using namespace std::chrono;
using namespace std::experimental;

class AsyncQueue {
public:
    class Awaitable {
        friend AsyncQueue;
        AsyncQueue& mQueue;
        coroutine_handle<> mCoroutineHandle;
        Awaitable* mNext = nullptr;
    public:
        Awaitable(AsyncQueue& queue):mQueue(queue){}

        bool await_ready() const noexcept {
            return false;
        }

        bool await_suspend(coroutine_handle<> coroutineHandle) noexcept
        {
            mCoroutineHandle = coroutineHandle;
            mQueue.enqueue(this);
            return true;
        }

        void await_resume() noexcept {}
    };
private:
    mutex mMutex;
    Awaitable* mHead = nullptr;
    Awaitable* mTail = nullptr;
    void enqueue(Awaitable* awaitable){
        lock_guard<mutex> g{ mMutex };
        if (mTail) {
            mTail->mNext = awaitable;
            mTail = awaitable;
        }
        else {
            mTail = awaitable;
            mHead = mTail;
        }
    }

    Awaitable* dequeue() {
        lock_guard<mutex> g{ mMutex };
        Awaitable* result = mHead;
        mHead = nullptr;
        mTail = nullptr;
        return result;
    }

public:
    Awaitable operator co_await() noexcept {
        return Awaitable{ *this };
    }

    bool poll() {
        Awaitable* awaitables = dequeue();
        if (!awaitables) {
            return false;
        }
        else {
            while (awaitables) {
                awaitables->mCoroutineHandle.resume();
                awaitables = awaitables->mNext;
            }
            return true;
        }
    }
};


AsyncQueue toBackgroundThread;
AsyncQueue toMainThread;

std::future<void> secondLevel(int id)
{
    co_await toBackgroundThread;
    cout << id << " run on " << this_thread::get_id() << endl;
    co_await toMainThread;
    cout << id << " run on " << this_thread::get_id() << endl;
}

std::future<void> topLevel() {
    co_await secondLevel(1);
    co_await secondLevel(2);
}

void listen(AsyncQueue& queue) {
    while (true) {
        if (!queue.poll()) {
            this_thread::sleep_for(100ms);
        }
    }
}

int main() {
    thread([]() {
        listen(toBackgroundThread);
    }).detach();

    topLevel();

    listen(toMainThread);
}

сопрограмма topLevel вызывает две secondLevel (которые, как я считаю, являются приостанавливаемыми подпрограммами не верхнего уровня), и она отлично работает. Приведенный выше код печатает:

1 run on 16648
1 run on 3448
2 run on 16648
2 run on 3448

Из этого ответа утверждается, что This prohibits providing suspend/resume operations in routines within a general-purpose library. я не вижу здесь запретов.


person Wei Hsieh    schedule 22.10.2018    source источник
comment
Мне сложно понять этот фрагмент кода. Где же здесь подвешивается нижняя бесстековая сопрограмма? Нельзя ли сократить этот пример, удалив потоки и очереди (которые кажутся совершенно не имеющими отношения к теме)?   -  person user7860670    schedule 22.10.2018
comment
@VTT Это самый короткий промежуток времени, которого я могу достичь, потому что у меня нет готовых сопрограмм для использования. Вы можете игнорировать поток и очередь. просто сосредоточьтесь на функциях topLevel и secondLevel. Код после co_await toBackgroundThread; выполняется в фоновом потоке, а код после co_await toMainThread; выполняется в основном потоке.   -  person Wei Hsieh    schedule 22.10.2018


Ответы (1)


При каждом вызове co_await приостанавливается только сопрограмма верхнего уровня. Чтобы приостановить более низкий уровень, этот уровень должен явно приостановить себя. И на тот момент это текущий «верхний уровень». Таким образом, в каждом случае приостанавливается только текущий верхний уровень.

Сравните это с чисто гипотетической библиотекой стековых сопрограмм:

//This function will always print the same thread ID.
void secondLevel(int id)
{
    while(!toBackgroundThread.poll())
      suspend_coroutine();

    cout << id << " run on " << this_thread::get_id() << endl;

    while(!toBackgroundThread.poll())
      suspend_coroutine();

    cout << id << " run on " << this_thread::get_id() << endl;
}

void topLevel() {
    secondLevel(1);
    secondLevel(2);
}

void listen(AsyncQueue& queue) {
    while (true) {
        if (!queue.poll()) {
            this_thread::sleep_for(100ms);
        }
    }
}

int main() {
    thread([]() {
        listen(toBackgroundThread);
    }).detach();

    auto coro = create_coroutine(topLevel);
    coro.switch_to();

    toMainThread.ready(); //Notes that the main thread is waiting
    while (true) {
        if (!toMainThread.poll()) {
            coro.switch_to();
        }
    }
};

topLevel не имеет явного механизма приостановки. Однако его выполнение приостанавливается всякий раз, когда любая вызываемая им функция приостанавливает выполнение. Весь стек вызовов, определенный функцией, заданной create_coroutine, и все, что он вызывает, приостанавливается. Вот как работает стековая сопрограмма.

Это то, что отличает, когда дело доходит до бесстековых сопрограмм. В версии без стека каждая функция, которую необходимо приостановить, должна быть специально закодирована для этого. И, таким образом, больше не является «универсальным назначением»; теперь он предназначен для сценариев приостановки.

person Nicol Bolas    schedule 22.10.2018
comment
Но зачем явно приостанавливать работу prohibits providing suspend/resume operations in routines within a general-purpose library? - person Wei Hsieh; 24.10.2018
comment
@JohnSmith: Потому что универсальная библиотека ничего не знает о co_await. Рассмотрим такой алгоритм, как std::generate. Будет ли он вызывать co_await для возвращаемого значения функтора, прежде чем вставлять его в итератор вывода? Нет. Следовательно, вы не можете заставить std::generate приостановить работу с помощью этого функтора. В то время как в стековой версии вы можете. - person Nicol Bolas; 24.10.2018
comment
Или, другими словами, вы получите версию generate, которая co_awaits на значениях функтора, и версию generate, которая этого не делает. Это будут две разные функции с разным кодом, и вам нужно будет выбрать, какую из них вызывать, в зависимости от того, нужна ли вам функция, которая может приостанавливаться или нет. Версия co_await больше не является универсальной, поскольку она не может обрабатывать непостоянные функторы. - person Nicol Bolas; 24.10.2018
comment
@NicolBolas Есть ли причина, по которой версия co_await не может обрабатывать непостоянные функторы? Я не знаю об одном (кроме, может быть, текущего синтаксиса), а в общем коде непостоянный co_await может иметь нулевую стоимость. - person Yakk - Adam Nevraumont; 25.10.2018
comment
@ Yakk-AdamNevraumont: Потому что co_await требует, чтобы тип выражения предоставлял механизм ожидания. co_await 5; и co_await thing_that_returns_5(); неправильно сформированы. - person Nicol Bolas; 25.10.2018
comment
@NicolBolas Конечно, но это синтаксис, а не что-то важное. Правило, указывающее в универсальном коде, co_await thing_that_returns_5(); есть thing_that_returns_5();, не является чем-то неслыханным; C ++ сделал то же самое для функций, возвращающих void и т.п. Черт возьми, auto x = maybe_await( [&]{ return /* code */; }) должен быть доступен для записи без каких-либо изменений языка, где maybe_await проверяет возвращаемое значение лямбды, и если оно ожидается, делает co_await, а otherise просто вызывает его. - person Yakk - Adam Nevraumont; 25.10.2018
comment
@ Yakk-AdamNevraumont: Ну, вы не можете этого сделать, потому что co_await не переживает функцию, в которой находится. maybe_await не может заставить функцию, которая вызывает ее, ждать. Вы должны делать if constexpr гимнастику, чтобы это сработало. Кроме того, что произойдет, если вы хотите сгенерировать контейнер ожидаемых типов, но не хотите await для них? Скажем, вы создаете vector<future<int>>. Ваша гипотетическая generate функция обнаружит, что возвращаемый тип является ожидаемым и, следовательно, co_await по значению. - person Nicol Bolas; 25.10.2018
comment
@ Yakk-AdamNevraumont: Общая суть такова: co_await принципиально вирусный. Такова его природа. Если вы хотите полностью приостановить функцию, эта функция должна быть явно написана для этого. Это аргумент №1 против сопрограмм в стиле co_await. - person Nicol Bolas; 25.10.2018