Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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: threading em Python; java.lang.Thread na JVM; System.Threading.Thread em .NET; std::thread em C++; <pthread.h>/pthread_t e <threads.h>/thrd_t em 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; ThreadId em Haskell; greenlet em 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: multiprocessing em Python, std::process e ipc-channel em Rust; <unistd.h> e <spawn.h> em C/C++; Boost.Process e Boost.Interprocess em C++; java.lang.Process na JVM; os e os/exec em Go; System.Diagnostics.Process e System.IO.Pipes em .NET; child_process e cluster em Node.js; pcntl_*, posix_*, proc_* e msg_* em PHP; Foundation.Process e Foundation.Pipe em 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::process em WebAssembly, dart:isolate em 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/await no modelo de programação assíncrono.

      Implementações: asyncio/coroutine em Python, corotinas em C++20, async/await em várias linguagens (C#, Rust, JavaScript, Dart, Swift, Python, Zig), generators em várias linguagens (Python, JavaScript, PHP), Amp e ReactPHP em PHP.

  • Uma fiber é uma unidade de concorrência, geralmente equivalente a uma corotina stackful.

    Implementações: Swoole\Coroutine e \Fiber em PHP, Fiber em Ruby, Boost.Fiber em C++, funções de fibers (ConvertThreadToFiber, CreateFiber, SwitchToFiber, etc) expostas na API Win32 em C/C++ (<windows.h>), node-fibers em Node.js, etc.

Modelos de Concorrência

Um modelo de concorrência é uma combinação de três coisas:

  1. Uma unidade de concorrência (representando tarefas)
  2. A forma como essas tarefas serão escalonadas
  3. 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:

  1. Unidade de Concorrência: Threads do SO
  2. Escalonamento: Preemptivo e realizado no kernel-space.
  3. 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:

  1. Unidade de Concorrência: Threads do SO
  2. Escalonamento: Preemptivo e realizado no kernel-space.
  3. 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.


  1. 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.