Linearizando código assíncrono com corutin

imagem

Além de usar a corotina para criar geradores, você pode tentar usá-los para linearizar o código assíncrono existente. Vamos tentar fazer isso com um pequeno exemplo. Pegue o código escrito na estrutura do ator e reescreva uma função desse código nas corotinas. Para construir o projeto, usaremos o gcc do ramo das corotinas .

Nosso objetivo é obter retornos de chamada de macarrão:

    abActor.getA(ABActor::GetACallback([this](int a) {
        abActor.getB(ABActor::GetBCallback([a, this](int b) {
            abActor.saveAB(a - b, a + b, ABActor::SaveABCallback([this](){
                abActor.getA(ABActor::GetACallback([this](int a) {
                    abActor.getB(ABActor::GetBCallback([a, this](int b) {
                        std::cout << "Result " << a << " " << b << std::endl;
                    }));
                }));
            }));
        }));
    }));

Tipo de:

const int a = co_await actor.abActor.getAAsync();
const int b = co_await actor.abActor.getBAsync();
co_await actor.abActor.saveABAsync(a - b, a + b);
const int newA = co_await actor.abActor.getAAsync();
const int newB = co_await actor.abActor.getBAsync();
std::cout << "Result " << newA << " " << newB << std::endl;

Então vamos começar.

Atores


Para começar, precisamos criar uma estrutura simples de ator. Criar uma estrutura de ator completa é uma tarefa difícil e grande, portanto, apenas implementamos algum tipo dela.
Primeiro, crie uma classe base:

class Actor {
public:
    using Task = std::function<void()>;
public:
    virtual ~Actor();
public:
    void addTask(const Task &task);
    void tryRunTask();
private:
    std::queue<Task> queue;
    mutable std::mutex mutex;
};

A idéia é basicamente simples: colocamos tarefas que são objetos funcionais em uma fila e, quando tentamos o RunTask, tentamos concluir essa tarefa. A implementação da classe confirma nossas intenções:

Actor::~Actor() = default;

void Actor::addTask(const Task &task) {
    std::lock_guard lock(mutex);
    queue.push(task);
}

void Actor::tryRunTask() {
    std::unique_lock lock(mutex);
    if (queue.empty()) {
        return;
    }

    const Task task = queue.front();
    queue.pop();
    lock.unlock();

    std::invoke(task);
}

A próxima aula é o "segmento" ao qual nossos atores pertencerão:

class Actor;

class ActorThread {
public:
    ~ActorThread();
public:
    void addActor(Actor &actor);
    void run();
private:
    std::vector<std::reference_wrapper<Actor>> actors;
};

Tudo é simples aqui também: no início do programa, "vinculamos" nossos atores ao encadeamento usando o método addActor e, em seguida, iniciamos o encadeamento usando o método run.

ActorThread::~ActorThread() = default;

void ActorThread::addActor(Actor &actor) {
    actors.emplace_back(actor);
}

void ActorThread::run() {
    while (true) {
        for (Actor &actor: actors) {
            actor.tryRunTask();
        }
    }
}

Ao iniciar o encadeamento, inserimos um loop infinito e tentamos executar uma tarefa de cada ator. Não é a melhor solução, mas servirá para uma demonstração.

Agora, vejamos o representante da classe de atores:

class ABActor: public Actor {
public:
    using GetACallback = Callback<void(int result)>;
    using GetBCallback = Callback<void(int result)>;
    using SaveABCallback = Callback<void()>;
public:
    void getA(const GetACallback &callback);
    void getB(const GetBCallback &callback);
    void saveAB(int a, int b, const SaveABCallback &callback);
private:
    void getAProcess(const GetACallback &callback);
    void getBProcess(const GetBCallback &callback);
    void saveABProcess(int a, int b, const SaveABCallback &callback);
private:
    int a = 10;
    int b = 20;
};

Esta classe armazena 2 números em si - aeb, e, mediante solicitação, retorna seus valores ou os substitui.

Como retorno de chamada, ele aceita um objeto funcional com os parâmetros necessários. Mas vamos prestar atenção ao fato de que diferentes atores podem ser lançados em segmentos diferentes. E, portanto, se no final do trabalho apenas chamarmos o retorno de chamada passado para o método, esse retorno de chamada será chamado no thread executável atual, e não no thread que chamou nosso método e criou esse retorno de chamada. Portanto, precisamos criar um wrapper sobre o retorno de chamada que resolverá esta situação:

template<typename C>
class Callback {
public:
    template<typename Functor>
    Callback(Actor &sender, const Functor &callback)
        : sender(sender)
        , callback(callback)
    {}
public:
    template<typename ...Args>
    void operator() (Args&& ...args) const {
        sender.addTask(std::bind(callback, std::forward<Args>(args)...));
    }
private:
    Actor &sender;
    std::function<C> callback;
};

Esse invólucro lembra o ator original e, quando você tenta se executar, simplesmente adiciona um retorno de chamada real à fila de tarefas do ator original.
Como resultado, a implementação da classe ABActor é assim:

void ABActor::getA(const GetACallback &callback) {
    addTask(std::bind(&ABActor::getAProcess, this, callback));
}

void ABActor::getAProcess(const ABActor::GetACallback &callback) {
    std::invoke(callback, a);
}

void ABActor::getB(const GetBCallback &callback) {
    addTask(std::bind(&ABActor::getBProcess, this, callback));
}

void ABActor::getBProcess(const ABActor::GetBCallback &callback) {
    std::invoke(callback, b);
}

void ABActor::saveAB(int a, int b, const SaveABCallback &callback) {
    addTask(std::bind(&ABActor::saveABProcess, this, a, b, callback));
}

void ABActor::saveABProcess(int a, int b, const ABActor::SaveABCallback &callback) {
    this->a = a;
    this->b = b;
    std::invoke(callback);
}

No método de interface da classe, simplesmente vinculamos os argumentos passados ​​ao "slot" correspondente da classe, criando uma tarefa e colocando-a na fila de tarefas dessa classe. Quando o encadeamento da tarefa começa a executar a tarefa, ele chamará o “slot” correto, que executará todas as ações necessárias e chamará o retorno de chamada, que por sua vez enviará o retorno de chamada real para a fila da tarefa que o causou.

Vamos escrever um ator que use a classe ABActor:

class ABActor;

class WokrerActor: public Actor {
public:
    WokrerActor(ABActor &actor)
        : abActor(actor)
    {}
public:
    void work();
private:
    void workProcess();
private:
    ABActor &abActor;
};

void WokrerActor::work() {
    addTask(std::bind(&WokrerActor::workProcess, this));
}

void WokrerActor::workProcess() {
    abActor.getA(ABActor::GetACallback(*this, [this](int a) {
        std::cout << "Result " << a << std::endl;
    }));
}

E junte tudo:

int main() {
    ABActor abActor;
    WokrerActor workerActor(abActor);

    ActorThread thread;
    thread.addActor(abActor);
    thread.addActor(workerActor);

    workerActor.work();

    thread.run();
}

Vamos seguir toda a cadeia de código.

No começo, criamos os objetos necessários e estabelecemos conexões entre eles.
Em seguida, adicionamos a tarefa workProcess à fila de tarefas do ator Worker.
Quando o encadeamento iniciar, ele encontrará nossa tarefa na fila e começará a executá-lo.
No processo de execução, chamamos o método getA da classe ABActor, colocando assim a tarefa correspondente na fila da classe ABActor e concluímos a execução.
Em seguida, o encadeamento pegará a tarefa recém-criada da classe ABActor e a executará, o que levará à execução do código getAProcess.
Esse código chamará um retorno de chamada, passando o argumento necessário para ele - a variável a. Mas como o retorno de chamada que ele possui é um invólucro, na verdade, um retorno de chamada real com parâmetros preenchidos será colocado na fila da classe Worker.
E quando, na próxima iteração do ciclo, o encadeamento sair e executar nosso retorno de chamada da classe Worker, veremos a saída da linha "Resultado 10"

A estrutura do ator é uma maneira bastante conveniente de interagir classes espalhadas por diferentes fluxos físicos entre si. A peculiaridade do design de classe, como você deveria estar convencido disso, é que, dentro de cada ator, todas as ações são executadas inteiramente e em um único segmento. O único ponto de sincronização dos fluxos é estabelecido nos detalhes de implementação da estrutura do ator e não é visível para o programador. Assim, um programador pode escrever código de thread único sem se preocupar em agrupar mutexes e rastrear situações de corrida, conflitos e outras dores de cabeça.

Infelizmente, esta solução tem um preço. Como o resultado da execução de outro ator só pode ser acessado a partir do retorno de chamada, mais cedo ou mais tarde o código do ator se tornará algo assim:

    abActor.getA(ABActor::GetACallback(*this, [this](int a) {
        abActor.getB(ABActor::GetBCallback(*this, [a, this](int b) {
            abActor.saveAB(a - b, a + b, ABActor::SaveABCallback(*this, [this](){
                abActor.getA(ABActor::GetACallback(*this, [this](int a) {
                    abActor.getB(ABActor::GetBCallback(*this, [a, this](int b) {
                        std::cout << "Result " << a << " " << b << std::endl;
                    }));
                }));
            }));
        }));
    }));

Vamos ver se podemos evitar isso usando a inovação do C ++ 20 - corotinas.

Mas primeiro, especificaremos as limitações.

Naturalmente, não podemos mudar o código da estrutura do ator. Além disso, não podemos alterar as assinaturas de métodos públicos e privados de instâncias da classe Actor - ABActor e WorkerActor. Vamos ver se conseguimos sair dessa situação.

Corotinas. Parte 1. Despertador


A idéia principal do corutin é que, ao criar a corotina, um quadro de pilha separado seja criado no heap, do qual podemos "sair" a qualquer momento, mantendo a posição de execução atual, os registros do processador e outras informações necessárias. Em seguida, também podemos a qualquer momento retornar à execução da corotina suspensa e concluí-la até o final ou até a próxima suspensão.

O objeto std :: coroutine_handle <> é responsável por gerenciar esses dados, que representam essencialmente um ponteiro para o quadro da pilha (e outros dados necessários) e que possui um método de resumo (ou seu análogo, o operador ()), que nos devolve à execução da corotina .

Com base nesses dados, primeiro escrevamos a função getAAsync e depois tentamos generalizar.

Então, suponha que já tenhamos uma instância da classe std :: coroutine_handle <> coro, o que precisamos fazer?

Você deve chamar o método já existente ABActor :: getA, que resolverá a situação conforme necessário, mas primeiro você precisará criar um retorno de chamada para o método getA.

Lembremos que um retorno de chamada é retornado ao retorno de chamada do método getA - o resultado do método getA. Além disso, esse retorno de chamada é chamado no segmento Worker do segmento. Assim, a partir desse retorno de chamada, podemos continuar a executar com segurança a rotina, criada apenas a partir do encadeamento Worker e que continuará a executar sua sequência de ações. Mas também, em algum lugar, devemos salvar o resultado retornado no retorno de chamada, é claro que será útil ainda mais para nós.

auto callback = GetACallback(returnCallbackActor, [&value, coro](int result) {
        value = result;
        std::invoke(coro);
 });
getA(callback);

Portanto, agora você precisa pegar uma instância do objeto coroutine_handle de algum lugar e um link para salvar nosso resultado.

No futuro, veremos que coroutine_handle é passado para nós como resultado da chamada da função. Consequentemente, tudo o que podemos fazer é passar para outra função. Vamos preparar esta função como uma lambda. (Vamos passar o link para a variável em que o resultado do retorno de chamada será armazenado na empresa).

auto storeCoroToQueue = [&returnCallbackActor, this](auto &value, std::coroutine_handle<> coro) {
    auto callback=GetACallback(returnCallbackActor, [&value, coro](int result){
        value = result;
        std::invoke(coro);
    });
    getA(callback);
};

Salvaremos essa função na próxima aula.

struct ActorAwaiterSimple {
    int value;

    std::function<void(int &value,std::coroutine_handle<>)> forwardCoroToCallback;

    ActorAwaiterSimple(
        const std::function<void(int &value, std::coroutine_handle<>)> &forwardCoroToCallback
    )
        : forwardCoroToCallback(forwardCoroToCallback)
    {}

    ActorAwaiterSimple(const ActorAwaiterSimple &) = delete;
    ActorAwaiterSimple& operator=(const ActorAwaiterSimple &) = delete;
    ActorAwaiterSimple(ActorAwaiterSimple &&) = delete;
    ActorAwaiterSimple& operator=(ActorAwaiterSimple &&) = delete;

// ...

Além do objeto funcional, também manteremos aqui a memória (na forma do valor da variável) para o valor que nos espera no retorno de chamada.

Como mantemos a memória abaixo do valor aqui, dificilmente queremos que a instância dessa classe seja copiada ou movida para algum lugar. Imagine, por exemplo, que alguém copiou essa classe, salvou o valor sob o valor da variável na instância antiga da classe e, em seguida, tentou lê-lo na nova instância. E, naturalmente, não existe, pois a cópia ocorreu antes de salvar. Desagradável. Portanto, nos protegemos contra esse problema proibindo construtores e copiando e movendo operadores.

Vamos continuar escrevendo esta aula. O próximo método que precisamos é:

    bool await_ready() const noexcept {
        return false;
    }

Ele responde à questão de saber se nosso significado está pronto para ser divulgado. Naturalmente, na primeira ligação, nosso valor ainda não está pronto e, no futuro, ninguém nos perguntará sobre isso, então retorne falso.

A instância coroutine_handle será passada para nós no método void waitit_suspend (std :: coroutine_handle <> coro), então vamos chamar nosso functor preparado, passando lá também um link de memória com valor:

    void await_suspend(std::coroutine_handle<> coro) noexcept {
        std::invoke(forwardCoroToCallback, std::ref(value), coro);
    }

O resultado da execução da função será solicitado no momento certo, chamando o método waitit_resume. Não recusaremos o solicitante:

    int await_resume() noexcept {
        return value;
    }

Agora, nosso método pode ser chamado usando a palavra-chave co_await:

const int a = co_await actor.abActor.getAAsync(actor);

O que vai acontecer aqui, já estamos representando aproximadamente.

Primeiro, será criado um objeto do tipo ActorAwaiterSimple, que será transferido para a "entrada" de co_await. Ele primeiro se interessa (chamando waititready) por termos acidentalmente um resultado final (não o temos), depois chama waitit_suspend, passando em um contexto (na verdade, um ponteiro para o atual quadro de pilha de corotina) e interrompe a execução.

No futuro, quando o ator do ABActor concluir seu trabalho e chamar o retorno de chamada do resultado, esse resultado (já no segmento de thread do Worker) será salvo na única instância (restante da pilha de rotinas) de ActorAwaiterSimple e a continuação da rotina será iniciada.

Corutin continuará a execução, obterá o resultado salvo chamando o método waitit_resume e passará esse resultado para a variável a

No momento, a limitação do Awaiter atual é que ele só pode funcionar com retornos de chamada com um parâmetro do tipo int. Vamos tentar expandir a aplicação do Awaiter:

template<typename... T>
struct ActorAwaiter {

    std::tuple<T...> values;

    std::function<void(std::tuple<T...> &values, std::coroutine_handle<>)> storeHandler;

    ActorAwaiter(const std::function<void(std::tuple<T...> &values, std::coroutine_handle<>)> &storeHandler)
        : storeHandler(storeHandler)
    {}

    ActorAwaiter(const ActorAwaiter &) = delete;
    ActorAwaiter& operator=(const ActorAwaiter &) = delete;
    ActorAwaiter(ActorAwaiter &&) = delete;
    ActorAwaiter& operator=(ActorAwaiter &&) = delete;

    bool await_ready() const noexcept {
        return false;
    }

    void await_suspend(std::coroutine_handle<> coro) noexcept {
        std::invoke(storeHandler, std::ref(values), coro);
    }

    //   bool B  ,
    //   sfinae      
    template<
        bool B=true,size_t len=sizeof...(T),std::enable_if_t<len==0 && B, int>=0
    >
    void await_resume() noexcept {

    }

    //   bool B  ,
    //   sfinae      
    template<
        bool B=true,size_t len=sizeof...(T),std::enable_if_t<len==1 && B, int>=0
    >
    auto await_resume() noexcept {
        return std::get<0>(values);
    }

    //   bool B  ,
    //   sfinae      
    template<
        bool B=true,size_t len=sizeof...(T),std::enable_if_t<len!=1 && len!=0 && B, int>=0
    >
    std::tuple<T...> await_resume() noexcept {
        return values;
    }
};

Aqui usamos std :: tuple para poder salvar várias variáveis ​​ao mesmo tempo.

Sfinae é imposta no método waitit_resume, para que seja possível não retornar uma tupla em todos os casos, mas dependendo do número de valores contidos na tupla, retorne nulo, exatamente 1 argumento ou toda a tupla.

Os wrappers para a criação do próprio Awaiter agora têm a seguinte aparência:

template<typename MakeCallback, typename... ReturnArgs, typename Func>
static auto makeCoroCallback(const Func &func, Actor &returnCallback) {
    return [&returnCallback, func](auto &values, std::coroutine_handle<> coro) {
        auto callback = MakeCallback(returnCallback, [&values, coro](ReturnArgs&& ...result) {
            values = std::make_tuple(std::forward<ReturnArgs>(result)...);
            std::invoke(coro);
        });
        func(callback);
    };
}

template<typename MakeCallback, typename... ReturnArgs, typename Func>
static ActorAwaiter<ReturnArgs...> makeActorAwaiter(const Func &func, Actor &returnCallback) {
    const auto storeCoroToQueue = makeCoroCallback<MakeCallback, ReturnArgs...>(func, returnCallback);
    return ActorAwaiter<ReturnArgs...>(storeCoroToQueue);
}

ActorAwaiter<int> ABActor::getAAsync(Actor &returnCallback) {
    return makeActorAwaiter<GetACallback, int>(std::bind(&ABActor::getA, this, _1), returnCallback);
}

ActorAwaiter<int> ABActor::getBAsync(Actor &returnCallback) {
    return makeActorAwaiter<GetBCallback, int>(std::bind(&ABActor::getB, this, _1), returnCallback);
}

ActorAwaiter<> ABActor::saveABAsync(Actor &returnCallback, int a, int b) {
    return makeActorAwaiter<SaveABCallback>(std::bind(&ABActor::saveAB, this, a, b, _1), returnCallback);
}

Agora vamos descobrir como usar o tipo criado diretamente na corotina.

Corotinas. Parte 2. Resumeble


Do ponto de vista do C ++, uma função que contém as palavras co_await, co_yield ou co_return é considerada como rotina. Mas também essa função deve retornar um determinado tipo. Concordamos que não alteraremos a assinatura das funções (aqui, quero dizer que o tipo de retorno também se refere à assinatura), portanto teremos que sair de alguma forma.

Vamos criar uma rotina lambda e chamá-la de nossa função:

void WokrerActor::workProcess() {
    const auto coroutine = [](WokrerActor &actor) -> ActorResumable {
        const int a = co_await actor.abActor.getAAsync(actor);
        const int b = co_await actor.abActor.getBAsync(actor);
        co_await actor.abActor.saveABAsync(actor, a - b, a + b);
        const int newA = co_await actor.abActor.getAAsync(actor);
        const int newB = co_await actor.abActor.getBAsync(actor);
        std::cout << "Result " << newA << " " << newB << std::endl;
    };

    coroutine(*this);
}

(Por que não capturar isso na lista de capturas de lambdas? Então todo o código interno ficaria um pouco mais fácil. Mas aconteceu que, aparentemente, as lambda-coroutines no compilador ainda não são totalmente suportadas, portanto esse código não funcionará.)

Como você pode ver, nosso o código de retorno de chamada assustador agora se transformou em um código linear bastante agradável. Tudo o que resta para nós é inventar a classe ActorResumable,

vamos dar uma olhada.

struct ActorResumable {
    struct promise_type {
        using coro_handle = std::coroutine_handle<promise_type>;

        auto get_return_object() { 
            //  ,    ActorResumable   promise_type
            return coro_handle::from_promise(*this);
        }

        auto initial_suspend() {
            //      
            return std::suspend_never();
        }

        auto final_suspend() {
            //      . 
            // ,     
            return std::suspend_never();
        }

        void unhandled_exception() {
            //   ,       
            std::terminate();
        }
    };

    ActorResumable(std::coroutine_handle<promise_type>) {}
};

O pseudocódigo da corutina gerada de nossa lambda se parece com isso:

ActorResumable coro() {
    promise_type promise;
    ActorResumable retobj = promise.get_return_object();
    auto intial_suspend = promise.initial_suspend();
    if (initial_suspend == std::suspend_always)  {
          // yield
    }
    try { 
        //  .
        const int a = co_await actor.abActor.getAAsync(actor);
        std::cout << "Result " << a << std::endl;
    } catch(...) { 
        promise.unhandled_exception();
    }
final_suspend:
    auto final_suspend = promise.final_suspend();
    if (final_suspend == std::suspend_always)  {
         // yield
    } else {
         cleanup();
    }

Isso é apenas pseudo-código, algumas coisas são intencionalmente simplificadas. No entanto, vamos ver o que acontece.

Primeiro, criamos uma promessa e o ActorResumable.

Depois de initial_suspend (), não pausamos, mas seguimos em frente. Começamos a realizar a parte principal do programa.

Quando chegamos a co_await, entendemos que precisamos fazer uma pausa. Já examinamos essa situação na seção anterior, você pode retornar a ela e revisá-la.

Depois de continuarmos a execução e exibir o resultado na tela, a execução da rotina termina. Verificamos final_suspend e limpamos todo o contexto da corotina.

Corotinas. Parte 3. Tarefa


Vamos lembrar em que estágio chegamos agora.

void WokrerActor::workProcess() {
    const auto coroutine = [](WokrerActor &actor) -> ActorResumable {
        const int a = co_await actor.abActor.getAAsync(actor);
        const int b = co_await actor.abActor.getBAsync(actor);
        co_await actor.abActor.saveABAsync(actor, a - b, a + b);
        const int newA = co_await actor.abActor.getAAsync(actor);
        const int newB = co_await actor.abActor.getBAsync(actor);
        std::cout << "Result " << newA << " " << newB << std::endl;
    };

    coroutine(*this);
}

Parece bom, mas é fácil ver que o código:

        const int a = co_await actor.abActor.getAAsync(actor);
        const int b = co_await actor.abActor.getBAsync(actor);

repetido 2 vezes. É possível refatorar esse momento e colocá-lo em uma função separada?

Vamos esboçar como isso pode parecer:

CoroTask<std::pair<int, int>> WokrerActor::readAB() {
    const int a = co_await abActor.getAAsync2(*this);
    const int b = co_await abActor.getBAsync2(*this);
    co_return std::make_pair(a, b);
}

void WokrerActor::workCoroProcess() {
    const auto coroutine = [](WokrerActor &actor) -> ActorResumable {
        const auto [a, b] = co_await actor.readAB();
        co_await actor.abActor.saveABAsync2(actor, a - b, a + b);
        const auto [newA, newB] = co_await actor.readAB();
        std::cout << "Result " << newA << " " << newB << " " << a << " " << b << std::endl;
    };

    coroutine(*this);
}

Tudo o que resta para nós é inventar o tipo CoroTask. Vamos pensar sobre isso. Primeiro, co_return é usado dentro da função readAB, o que significa que o CoroTask deve satisfazer a interface Resumable. Mas também, um objeto dessa classe é usado para inserir co_await de outra corotina. Isso significa que a classe CoroTask também deve satisfazer a interface Awaitable. Vamos implementar essas duas interfaces na classe CoroTask:

template <typename T = void>
struct CoroTask {
    struct promise_type {
        T result;
        std::coroutine_handle<> waiter;

        auto get_return_object() {
            return CoroTask{*this};
        }

        void return_value(T value) {
            result = value;
        }

        void unhandled_exception() {
            std::terminate();
        }

        std::suspend_always initial_suspend() {
            return {};
        }

        auto final_suspend() {
            struct final_awaiter {
                bool await_ready() {
                    return false;
                }
                void await_resume() {}
                auto await_suspend(std::coroutine_handle<promise_type> me) {
                    return me.promise().waiter;
                }
            };
            return final_awaiter{};
        }
    };

    CoroTask(CoroTask &&) = delete;
    CoroTask& operator=(CoroTask&&) = delete;
    CoroTask(const CoroTask&) = delete;
    CoroTask& operator=(const CoroTask&) = delete;

    ~CoroTask() {
        if (h) {
            h.destroy();
        }
    }

    explicit CoroTask(promise_type & p)
        : h(std::coroutine_handle<promise_type>::from_promise(p))
    {}

    bool await_ready() {
        return false;
    }

    T await_resume() {
        auto &result = h.promise().result;
        return result;
    }

    void await_suspend(std::coroutine_handle<> waiter) {
        h.promise().waiter = waiter;
        h.resume();
    }
private:
    std::coroutine_handle<promise_type> h;
};

(Eu recomendo fortemente abrir a imagem de plano de fundo deste post. No futuro, isso ajudará bastante.)

Então, vamos ver o que acontece aqui.

1. Vá para a corotina lambda e crie imediatamente a corotina WokrerActor :: readAB. Mas, depois de criar essa corotina, não começamos a executá-la (initial_suspend == suspend_always), o que nos obriga a interromper e retornar à corotina lambda.

2. co_await lambda verifica se o readAB está pronto. O resultado não está pronto (Waitit_ready == false), o que o força a passar seu contexto para o método CoroTask :: waitit_suspend. Esse contexto é salvo no CoroTask e o resumo das corotinas readAB

3. É iniciado . Depois que a coroutine readAB conclui todas as ações necessárias, ela atinge a linha:

co_return std::make_pair(a, b);

como resultado, o método CoroTask :: promessa_tipo :: retorno_valor é chamado e, dentro do método CoroTask :: promessa_tipo, o par de números criado

é salvo 4. Como o método co_return foi chamado, a execução da corotina chega ao fim, o que significa que é hora de chamar o método CoroTask :: promessa_tipo :: final_suspend . Este método retorna uma estrutura auto-escrita (não se esqueça de olhar para a figura), que obriga a chamar o método final_awaiter :: waitit_suspend, que retorna o contexto lambda de rotina armazenado na etapa 2.

Por que não pudemos retornar o suspend_sempre aqui? Afinal, no caso de initial_suspend desta classe, conseguimos? O fato é que, em initial_suspend, obtivemos sucesso porque essa corotina foi chamada pela nossa lambda corotina e voltamos a ela. Mas, no momento em que alcançamos a chamada final_suspend, nossa corotina provavelmente continuou de outra pilha (especificamente, do lambda que a função makeCoroCallback preparou) e, se retornássemos suspend_always aqui, retornaríamos a ela, e não ao método workCoroProcess.

5. Como o método final_awaiter :: waitit_suspend retornou o contexto para nós, isso força o programa a continuar executando o contexto retornado, ou seja, a corotina lambda. Desde que a execução retornou ao ponto:

const auto [a, b] = co_await actor.readAB();

precisamos isolar o resultado salvo chamando o método CoroTask :: waitit_resume. O resultado é recebido, passado para as variáveis ​​aeb, e agora a instância do CoroTask é destruída.

6. A instância do CoroTask foi destruída, mas o que aconteceu com o contexto WokrerActor :: readAB? Se nós do CoroTask :: promessa_tipo :: final_suspend retornássemos suspend_never (mais precisamente, retornaríamos à pergunta aguardar_já retornaria true), nesse momento o contexto da corotina seria limpo. Mas, como não o fizemos, a obrigação de limpar o contexto é transferida para nós. Limparemos esse contexto no destruidor do CoroTask; neste momento, ele já é seguro.

7. A coroutine readAB é executada, o resultado é obtido, o contexto é limpo, a coroutine lambda continua em execução ...

Ufa, meio que resolvi isso. Você se lembra que, a partir dos métodos ABActor :: getAAsync () e similares, retornamos uma estrutura auto-escrita? De fato, o método getAAsync também pode ser transformado em uma rotina combinando o conhecimento adquirido com a implementação das classes CoroTask e ActorAwaiter e obtendo algo como:

CoroTaskActor<int> ABActor::getAAsync(Actor &returnCallback) {
    co_return makeCoroCallback<GetACallback, int>(std::bind(&ABActor::getA, this, _1), returnCallback);
}

mas isso deixarei para auto-análise.

achados


Como você pode ver, com a ajuda da corotina, é possível linearizar o código de retorno de chamada assíncrono. É verdade que o processo de escrever tipos e funções auxiliares ainda não parece muito intuitivo.

Todo o código está disponível no repositório.Eu

também recomendo que você analise essas palestras para obter uma imersão mais completa no tópico
.
Um grande número de exemplos sobre o tema da corotina do mesmo autor está aqui .
E você também pode assistir a esta palestra.

All Articles