Unidades e Modelos de Concorrência
Os próximos capítulos desse livro são organizados ao redor de vários modelos de concorrência. Esses modelos definem uma unidade de concorrência, utilizada para representar uma “tarefa”, e as regras sobre como essas unidades interagem e são escalonadas. Esse capítulo explica o que são unidades e modelos de concorrência, e dá exemplos de cada um, que serão estudados ao longo do livro.
Unidade de Concorrência
Uma unidade de concorrência é a abstração usada para representar as tarefas concorrentes de um programa e que serão escalonadas. Essa abstração costuma ser implementada como um tipo que pode ser instanciado pelo programador. Certas unidades não se parecem com um tipo, podendo ser uma função, mas por trás dos panos elas ainda são implementadas como um objeto.
Alguns exemplos que iremos explorar nos próximos capítulos:
-
Uma thread do SO é a unidade de concorrência mais primitiva de todas, escalonada pelo próprio sistema operacional.
Implementações:
threadingem Python;java.lang.Threadna JVM;System.Threading.Threadem .NET;std::threadem C++;<pthread.h>/pthread_te<threads.h>/thrd_tem C/C++; etc. -
Uma green thread (também chamada de user-space thread ou thread leve) é uma unidade de concorrência implementada e escalonada no user-space, apresentando uma semântica similar a de uma thread (memória compartilhada).
Uma green thread pode ser executada em uma de \( N \) threads do SO possíveis, e uma thread do SO pode executar uma de \( M \) green threads possível, definindo assim um mapeamento \( N:M \) entre ambas as unidades de concorrência.
Implementações: goroutines em Go; virtual threads em Java;
ThreadIdem Haskell;greenletem Python; etc. -
Um processo do SO é uma unidade de concorrência escalonada pelo próprio sistema operacional, com isolamento de memória entre dois ou mais processos. A comunicação entre processos é feita usando técnicas de Inter-Process Communication (IPC), como signals, pipes e filas.
Implementações:
multiprocessingem Python,std::processeipc-channelem Rust;<unistd.h>e<spawn.h>em C/C++; Boost.Process e Boost.Interprocess em C++;java.lang.Processna JVM;oseos/execem Go;System.Diagnostics.ProcesseSystem.IO.Pipesem .NET;child_processeclusterem Node.js;pcntl_*,posix_*,proc_*emsg_*em PHP;Foundation.ProcesseFoundation.Pipeem Swift; etc. -
Um green process (também chamado de user-space process, ator ou actor)1 é uma unidade de concorrência implementada e escalonada no user-space, apresentando uma semântica similar a de um processo do SO (memória isolada).
Implementações: processos da BEAM,
lunatic::processem WebAssembly,dart:isolateem Dart, Cloudflare Workers, etc. -
Uma corotina é uma unidade de concorrência implementada e escalonada no user-space, e sempre escalonada de maneira cooperativa (daí o nome).
Uma corotina é uma generalização de uma rotina/função: enquanto uma rotina/função tradicional tem um ponto de entrada e um ponto de saída, uma corotina pode ter vários. Como são escalonadas cooperativamente, o próprio programador decide quando suspender uma corotina, usando uma função ou keyword geralmente chamada
yield/await.Podemos dividir corotinas entre stackful e stackless.
-
Uma corotina stackful é uma corotina que possui uma pilha de chamadas (call stack) completa. O runtime aloca memória (entre 4 KB-1MB) para construir a pilha, e gerencia ela ao longo da vida da corotina. Isso permite que a corotina seja suspensa em qualquer profundidade na cadeia de chamadas de funções, e não apenas a partir da função mais externa. Uma corotina stackful é equivalente a uma green thread cooperativa.
Implementações:
coroutine.*em Lua (a implementação mais famosa), goroutines em Go (quando cooperativas), virtual threads em Java,<ucontext.h>em C/C++. -
Uma corotina stackless é uma corotina que não carrega sua própria pilha de chamadas. Invés disso, o compilador transforma a corotina num struct, que funciona como uma máquina de estados. Cada chamada de
yield/awaité transformada numa transição de estado.Apenas as variáveis locais que persistem entre pontos de suspensão (isto é, que são usadas em ambos os lados de um
yield/await) são salvas em memória, mais especificamente num pequeno frame do heap (entre 512 e 1024 bytes).Por não possuir uma pilha de chamadas, uma corotina stackless só pode ser suspensa no nível mais alto do seu próprio corpo, nunca de dentro de uma chamada de função aninhada. Para mais detalhes de como corotinas stackless funcionam por baixo dos panos, veja Em Detalhe: Corotinas.
Corotinas stackless são a principal forma de implementar a sintaxe
async/awaitno modelo de programação assíncrono.Implementações:
asyncio/coroutineem Python, corotinas em C++20,async/awaitem várias linguagens (C#, Rust, JavaScript, Dart, Swift, Python, Zig), generators em várias linguagens (Python, JavaScript, PHP),AmpeReactPHPem PHP.
-
-
Uma fiber é uma unidade de concorrência, geralmente equivalente a uma corotina stackful.
Implementações:
Swoole\Coroutinee\Fiberem PHP,Fiberem Ruby,Boost.Fiberem C++, funções de fibers (ConvertThreadToFiber,CreateFiber,SwitchToFiber, etc) expostas na API Win32 em C/C++ (<windows.h>),node-fibersem Node.js, etc.
Modelos de Concorrência
Um modelo de concorrência é uma combinação de três coisas:
- Uma unidade de concorrência (representando tarefas)
- A forma como essas tarefas serão escalonadas
- As regras de como duas ou mais tarefas podem coordenar entre si
Uma linguagem de programação pode tranquilamente oferecer mais de um modelo para o programador, que poderá escolher um ou mais modelos para resolver algum problema.
Vamos ver dois modelos de concorrência para ilustrar essa definição. Essas descricões estão longe de serem detalhadas, pois o detalhamento nos capítulos dos respectivos modelos.
Threads e Locks
Esse modelo é composto de:
- Unidade de Concorrência: Threads do SO
- Escalonamento: Preemptivo e realizado no kernel-space.
- Coordenação: Memória compartilhada + Locks
As threads se comunicam através de memória compartilhada e, para controlar o acesso a essa memória, realizam sincronização por meio de locks (Mutex, por exemplo).
Concorrência Lock-Free
Esse modelo é composto de:
- Unidade de Concorrência: Threads do SO
- Escalonamento: Preemptivo e realizado no kernel-space.
- Coordenação: Memória compartilhada + Atomics
Nesse caso, é praticamente o mesmo modelo que o anterior. No entanto, ao invés de sincronizarem o acesso à memória por meio de locks, são utilizadas operações atômicas (também chamadas de atomics).
As próximas Partes do livro irão focar em cada modelo de concorrência relevante, aprendendo o jeito correto de raciocinar e programar em cada modelo, e vendo quando usar (ou não) cada um deles.
-
O nome “Ator” deriva do modelo de concorrência chamado “modelo de Ator”, que é muito associado com green processes, em especial por causa da implementação mais famosa desse, presente na BEAM. No entanto, nem todas implementações do modelo de Ator implementam Atores com green processes. Dois exemplos disso são Ractors (implementação da linguagem Ruby) e Akka/Pekka (uma biblioteca e runtime que implementa o modelo de Ator na JVM), pois os Atores de ambas as implementações não tem um isolamento de memória no heap, nem possuem um garbage-collector por ator. O isolamento é feito por convenção ou por restrição da API pública, mas ele não existe a nível de memória. ↩