O que são corrotinas em C ++ 20?

104

O que são corrotinas em ?

De que forma é diferente de "Paralelismo2" ou / e "Simultaneidade2" (veja a imagem abaixo)?

A imagem abaixo é do ISOCPP.

https://isocpp.org/files/img/wg21-timeline-2017-03.png

insira a descrição da imagem aqui

Pavan Chandaka
fonte
3
Para responder "De que maneira o conceito de corrotinas é diferente de paralelismo e simultaneidade ?" - en.wikipedia.org/wiki/Coroutine
Ben Voigt
relacionado: stackoverflow.com/q/35121078/103167
Ben Voigt
3
Uma introdução muito boa e fácil de seguir para a corrotina é a apresentação de James McNellis "Introdução às corrotinas C ++" (Cppcon2016).
philsumuru
2
Finalmente, também seria bom cobrir "Como as corrotinas em C ++ são diferentes das implementações de corrotinas e funções recuperáveis ​​de outras linguagens?" (que o artigo da Wikipedia com link acima, sendo agnóstico de linguagem, não aborda)
Ben Voigt,
1
quem mais leu esta "quarentena em C ++ 20"?
Sahib Yar

Respostas:

199

Em um nível abstrato, as co-rotinas dividem a ideia de ter um estado de execução da ideia de ter um thread de execução.

SIMD (instrução única, dados múltiplos) tem vários "threads de execução", mas apenas um estado de execução (funciona apenas em vários dados). Algoritmos indiscutivelmente paralelos são um pouco assim, em que você tem um "programa" rodando em dados diferentes.

Threading tem vários "threads de execução" e vários estados de execução. Você tem mais de um programa e mais de um thread de execução.

As corrotinas têm vários estados de execução, mas não possuem um thread de execução. Você tem um programa e o programa tem estado, mas não tem thread de execução.


O exemplo mais fácil de co-rotinas são geradores ou enumeráveis ​​de outras linguagens.

Em pseudocódigo:

function Generator() {
  for (i = 0 to 100)
    produce i
}

O Generatoré chamado e, na primeira vez, ele retorna 0. Seu estado é lembrado (o quanto o estado varia com a implementação de co-rotinas) e, da próxima vez que você chamá-lo, ele continuará de onde parou. Portanto, ele retorna 1 na próxima vez. Em seguida, 2.

Finalmente, ele atinge o final do loop e sai do final da função; a co-rotina está concluída. (O que acontece aqui varia de acordo com a linguagem da qual estamos falando; em python, isso gera uma exceção).

As corrotinas trazem esse recurso para C ++.

Existem dois tipos de corrotinas; empilhados e sem pilha.

Uma co-rotina sem pilha armazena apenas variáveis ​​locais em seu estado e seu local de execução.

Uma co-rotina empilhável armazena uma pilha inteira (como um thread).

As co-rotinas sem pilha podem ser extremamente leves. A última proposta que li envolvia basicamente reescrever sua função em algo um pouco como lambda; todas as variáveis ​​locais vão para o estado de um objeto, e rótulos são usados ​​para pular de / para o local onde a co-rotina "produz" resultados intermediários.

O processo de produção de um valor é chamado de "rendimento", pois as co-rotinas são quase como multithreading cooperativo; você está devolvendo o ponto de execução ao chamador.

Boost tem uma implementação de corrotinas empilháveis; ele permite que você chame uma função para produzir para você. Corrotinas empilháveis ​​são mais poderosas, mas também mais caras.


As co-rotinas são mais do que um simples gerador. Você pode esperar uma co-rotina em uma co-rotina, o que permite compor co-rotinas de uma maneira útil.

Corrotinas, como if, loops e chamadas de função, são outro tipo de "goto estruturado" que permite expressar certos padrões úteis (como máquinas de estado) de uma maneira mais natural.


A implementação específica de corrotinas em C ++ é um pouco interessante.

Em seu nível mais básico, ele adiciona algumas palavras-chave ao C ++:, co_return co_await co_yieldjunto com alguns tipos de biblioteca que funcionam com elas.

Uma função se torna uma co-rotina por ter uma delas em seu corpo. Portanto, de acordo com sua declaração, eles são indistinguíveis de funções.

Quando uma dessas três palavras-chave é usada em um corpo de função, ocorre algum exame obrigatório padrão do tipo de retorno e dos argumentos e a função é transformada em uma co-rotina. Esse exame informa ao compilador onde armazenar o estado da função quando a função está suspensa.

A co-rotina mais simples é um gerador:

generator<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}

co_yieldsuspende a execução das funções, armazena esse estado em generator<int>e retorna o valor de currentpor meio de generator<int>.

Você pode fazer um loop sobre os inteiros retornados.

co_awaitentretanto, permite emendar uma co-rotina em outra. Se você está em uma co-rotina e precisa dos resultados de uma coisa esperada (geralmente uma co-rotina) antes de progredir, você está co_awaitnela. Se eles estiverem prontos, você prossegue imediatamente; caso contrário, você suspende até que o aguardado que você está esperando esteja pronto.

std::future<std::expected<std::string>> load_data( std::string resource )
{
  auto handle = co_await open_resouce(resource);
  while( auto line = co_await read_line(handle)) {
    if (std::optional<std::string> r = parse_data_from_line( line ))
       co_return *r;
  }
  co_return std::unexpected( resource_lacks_data(resource) );
}

load_dataé uma co-rotina que gera um std::futurequando o recurso nomeado é aberto e conseguimos analisar até o ponto onde encontramos os dados solicitados.

open_resourcee read_lines são provavelmente corrotinas assíncronas que abrem um arquivo e leem linhas dele. O co_awaitconecta o estado de suspensão e pronto de load_dataao seu progresso.

As co-rotinas C ++ são muito mais flexíveis do que isso, pois foram implementadas como um conjunto mínimo de recursos de linguagem além dos tipos de espaço do usuário. Os tipos de espaço do usuário definem efetivamente o que co_return co_awaite o co_yield significado - tenho visto pessoas usá-los para implementar expressões opcionais monádicas, de modo que um co_awaitopcional vazio propaga automaticamente o estado vazio para o opcional externo:

modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
  return (co_await a) + (co_await b);
}

ao invés de

std::optional<int> add( std::optional<int> a, std::optional<int> b ) {
  if (!a) return std::nullopt;
  if (!b) return std::nullopt;
  return *a + *b;
}
Yakk - Adam Nevraumont
fonte
26
Esta é uma das explicações mais claras do que são corrotinas que já li. Compará-los e distingui-los do SIMD e dos fios clássicos foi uma excelente ideia.
Omnifário de
2
Eu não entendo o exemplo de add-options. std :: optional <int> não é um objeto aguardável.
Jive Dadson de
1
@mord sim, é suposto retornar 1 elemento. Pode precisar de polimento; se quisermos mais de uma linha, precisamos de um fluxo de controle diferente.
Yakk - Adam Nevraumont
1
@ Se desculpe, era para ser ;;.
Yakk - Adam Nevraumont
1
@LF para uma função tão simples talvez não haja diferença. Mas a diferença que vejo em geral é que uma co-rotina lembra o ponto de entrada / saída (execução) em seu corpo, enquanto uma função estática inicia a execução desde o início a cada vez. A localização dos dados "locais" é irrelevante, eu acho.
avp
21

Uma co-rotina é como uma função C que tem várias instruções de retorno e, quando chamada uma segunda vez, não inicia a execução no início da função, mas na primeira instrução após o retorno executado anteriormente. Este local de execução é salvo junto com todas as variáveis ​​automáticas que viveriam na pilha em funções não co-rotinas.

Uma implementação de co-rotina experimental anterior da Microsoft usava pilhas copiadas para que você pudesse até mesmo retornar de funções aninhadas profundas. Mas esta versão foi rejeitada pelo comitê C ++. Você pode obter essa implementação, por exemplo, com a biblioteca de fibra Boosts.

Lothar
fonte
1

supõe-se que as corrotinas sejam (em C ++) funções que são capazes de "esperar" a conclusão de alguma outra rotina e fornecer o que for necessário para que a rotina suspensa, em pausa, em espera continue. o recurso mais interessante para o pessoal do C ++ é que as corrotinas idealmente não ocupariam espaço na pilha ... C # já pode fazer algo assim com await e yield, mas o C ++ pode ter que ser reconstruído para obtê-lo.

a simultaneidade é fortemente focada na separação de interesses, onde um interesse é uma tarefa que o programa deve concluir. essa separação de interesses pode ser realizada por vários meios ... geralmente por delegação de algum tipo. a ideia de simultaneidade é que vários processos poderiam ser executados independentemente (separação de interesses) e um 'ouvinte' direcionaria tudo o que é produzido por esses interesses separados para onde deveria ir. isso depende muito de algum tipo de gerenciamento assíncrono. Existem várias abordagens para a concorrência, incluindo programação orientada a Aspect e outros. C # tem o operador 'delegate' que funciona muito bem.

paralelismo soa como simultaneidade e pode estar envolvido, mas na verdade é uma construção física envolvendo muitos processadores dispostos de forma mais ou menos paralela com software que é capaz de direcionar partes do código para diferentes processadores onde ele será executado e os resultados serão recebidos de volta sincronizadamente.

Dr t
fonte
9
Concorrência e separação de interesses são totalmente independentes. As corrotinas não são para fornecer informações para a rotina suspensa, são as rotinas retomáveis.
Ben Voigt de