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 Escalonáveis: Processos e Threads

Tanto processos quanto threads atuam como tarefas: construtos do sistema operacional que podem ser escalonados. Isto é, na presença de vários processos e threads, apenas uma parte deles é selecionada para executar no hardware. Ao longo do tempo, uma tarefa dá lugar (voluntariamente ou não) para outra ser executada.

A diferença entre esses construtos é no nível de isolamento entre eles, o que determina o poder e os riscos que cada um possui.

Processos

Processos são tarefas independentes cujo contexto de execução é altamente isolado.

Dois processos não podem acessar uma mesma região de memória. Cada processo tem seu próprio:

  • Espaço de endereçamento virtual
  • Tabela de file descriptors (arquivos, pipes, sockets, etc)
  • Conjunto de signal dispositions
  • Conjunto de mapeamentos de memória
  • Heap e segmentos estáticos de memória (.text, .data, .bss)
  • ID (Process ID ou PID)

Todos esses componentes são isolados entre dois processos distintos. Isto significa que quando um processo A escreve algo no endereço 0x7fff, ele está escrevendo numa página de memória física que um outro processo B não pode ver (imposto pela Memory Management Unit, MMU, da CPU).

Esse isolamento tem prós e contras.

  • O principal pró é que se um processo crashar — ao dereferenciar um ponteiro inválido ou corromper seu heap — ele não irá afetar outros processos, devido ao isolamento.
  • O principal contra é que a comunicação entre dois processos (conhecida como IPC, Inter-Process Communication) é mais trabalhosa de ser feita, tanto em termos de codificação quanto de performance.

Threads

Threads (também chamadas de threads nativas, de kernel ou do SO, para distinguir de threads de usuário) são tarefas que vivem dentro de um processo e cujo contexto de execução é pouco isolado.

Diferentemente de processos, duas threads de um mesmo processo podem acessar uma mesma região de memória. De maneira geral, todas threads em um mesmo processo compartilham:

  • O mesmo espaço de endereçamento virtual
  • O mesmo heap e segmentos estáticos de memória
  • A mesma tabela de file descriptors
  • O mesmo conjunto de signal dispositions

Isso não quer dizer que as threads compartilham tudo. Cada thread tem um estado próprio, separado das outras threads no mesmo processo. Esse estado inclui:

  • Uma pilha. Permite que cada thread execute funções de maneira independente.
  • Um conjunto de registradores, incluindo os registradores PC (Program Counter) e SP (Stack Pointer). Permite que cada thread execute uma sequência de instruções individual.
  • Contexto de escalonamento do kernel. Permite que cada thread seja escalonada independentemente pelo SO.
  • Um ID (Thread ID ou TID).

Todo processo tem pelo menos uma thread, que é a thread principal.

Prós e Contras

Esse isolamento menor entre threads apresenta prós e contras.

  • O principal pró é que a comunicação entre threads é muito mais fácil de ser feita do que entre processos. Como as threads compartilham o mesmo heap e variáveis globais, a escrita de uma thread numa dessas localizações é visível para todas as outras threads do mesmo processo.
  • Além disso, outro pró é que, como a criação de uma thread re-aproveita a maior parte das estruturas de dados usadas pelo processo, esse processo é consideravelmente mais barato (em termos de ciclos de CPU e memória).

No entanto, esse “poder” é justamente o que permite alguns dos maiores riscos quando falamos de threads:

  • A memória compartilhada entre threads significa que duas escritas na mesma localização podem gerar bugs de concorrência, que levam o programa a ter um comportamento fora do esperado, como veremos mais pra frente.
  • Para evitar esses bugs de concorrência, são introduzidos mecanismos de sincronização, que restringem o acesso a regiões de memória específicas, o que pode gerar os custos de overhead mencionados no Capítulo 1.
  • A memória compartilhada também significa que se um thread corromper o heap do processo, todas as threads serão afetadas.

Simultaneous Multithreading (SMT)

Geralmente, cada thread ocupa um único núcleo do processador num dado instante. No entanto, uma tecnologia conhecida simultaneous multithreading (SMT) ou hardware multithreading permite que um único núcleo da CPU exiba para o sistema operacional duas ou mais “threads”. É por isso que em, algumas máquinas, quando o sistema operacional lista o número de threads que ele pode rodar ao mesmo tempo, esse número acaba sendo maior do que a quantidade de núcleos no processador.

Por exemplo, abaixo está o resultado do lscpu (um programa de linha de comando do Linux que retorna informações sobre as CPUs do computador) na minha máquina pessoal:

$ lscpu
Architecture:                x86_64
  CPU op-mode(s):            32-bit, 64-bit
  Address sizes:             39 bits physical, 48 bits virtual
  Byte Order:                Little Endian
CPU(s):                      16   # <-- o SO vê 16 threads/"CPUs"
  On-line CPU(s) list:       0-15
Vendor ID:                   GenuineIntel
  Model name:                13th Gen Intel(R) Core(TM) i7-13620H
    CPU family:              6
    Model:                   186
    Thread(s) per core:      2   # <-- temos duas threads por núcleo
    Core(s) per socket:      8   # <-- temos 8 núcleos físicos
    Socket(s):               1