Threads: Conceitos e Modelos
Video da aula estara disponivel em breve
O que e uma Thread?
Uma thread (ou "linha de execucao") e a menor unidade de execucao que pode ser escalonada pelo SO. Se um processo e um "programa em execucao", uma thread e um "fluxo de execucao dentro de um processo".
Um processo pode ter multiplas threads, todas compartilhando o mesmo espaco de enderecamento (codigo, dados, heap, arquivos abertos), mas cada uma com sua propria:
- Pilha (stack): variaveis locais e enderecos de retorno
- Registradores: incluindo o program counter
- Estado: running, ready, waiting
Single-threaded: Multi-threaded:
+------------------+ +------------------+
| Processo | | Processo |
| +------+ | | +------+------+------+
| |Thread| | | |Thr 1 |Thr 2 |Thr 3 |
| | | | | | | | |
| |stack | | | |stack |stack |stack |
| |regs | | | |regs |regs |regs |
| |PC | | | |PC |PC |PC |
| +------+ | | +------+------+------+
| | | |
| code | data | heap | | code | data | heap | COMPARTILHADO
| files | signals | | files | signals | entre threads
+------------------+ +----------------------+
Por que usar Threads?
- Responsividade: em uma GUI, uma thread processa input do usuario enquanto outra faz I/O
- Compartilhamento de recursos: threads compartilham memoria — comunicacao sem overhead de IPC
- Economia: criacao e context switch mais baratos que processos
- Paralelismo real: em CPUs multi-core, threads podem rodar simultaneamente em cores diferentes
POSIX Threads (pthreads)
A API padrao para threads em C/Unix e a POSIX Threads (pthreads). Vamos ver as operacoes fundamentais:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// Funcao que cada thread executara
void* worker(void* arg) {
int id = *(int*)arg;
printf("Thread %d: iniciando (tid=%lu)\n", id, pthread_self());
// Simula trabalho
sleep(1);
printf("Thread %d: terminando\n", id);
return NULL;
}
int main() {
pthread_t threads[4];
int ids[4];
// Criar 4 threads
for (int i = 0; i < 4; i++) {
ids[i] = i;
int rc = pthread_create(&threads[i], NULL, worker, &ids[i]);
if (rc != 0) {
fprintf(stderr, "Erro ao criar thread %d\n", i);
return 1;
}
}
// Esperar todas terminarem (join)
for (int i = 0; i < 4; i++) {
pthread_join(threads[i], NULL);
}
printf("Main: todas as threads terminaram\n");
return 0;
}
// Compilar: gcc -pthread -o threads threads.c
Sincronizacao com Mutex
#include <stdio.h>
#include <pthread.h>
int counter = 0; // variavel compartilhada
pthread_mutex_t lock; // mutex para proteger counter
void* increment_unsafe(void* arg) {
for (int i = 0; i < 1000000; i++)
counter++; // BUG: race condition!
return NULL;
}
void* increment_safe(void* arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock);
counter++; // protegido pelo mutex
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&lock, NULL);
// Teste UNSAFE: resultado imprevisivel
counter = 0;
pthread_create(&t1, NULL, increment_unsafe, NULL);
pthread_create(&t2, NULL, increment_unsafe, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Unsafe: %d (esperado: 2000000)\n", counter);
// Teste SAFE: sempre 2000000
counter = 0;
pthread_create(&t1, NULL, increment_safe, NULL);
pthread_create(&t2, NULL, increment_safe, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Safe: %d (esperado: 2000000)\n", counter);
pthread_mutex_destroy(&lock);
return 0;
}
User Threads vs. Kernel Threads
Threads podem ser implementadas em dois niveis:
User-Level Threads (ULT): Kernel-Level Threads (KLT):
+------------------------+ +------------------------+
| Processo | | Processo |
| +----+ +----+ +----+ | | +----+ +----+ +----+ |
| | UT1| | UT2| | UT3| | | | KT1| | KT2| | KT3| |
| +----+ +----+ +----+ | | +----+ +----+ +----+ |
| +------------------+ | +----|----|----|---------+
| | Thread Library | | | | |
| | (user space) | | +----|----|----|--------+
| +------------------+ | | Kernel: escalona |
+------------------------+ | cada thread |
| Kernel: ve 1 thread | | independentemente |
+-----------+-------------+ +-----------------------+
|
Se UT1 faz I/O, KT1 faz I/O: apenas KT1
TODAS bloqueiam! bloqueia. KT2 e KT3
continuam rodando.
User Threads: Kernel Threads:
+ Criacao rapida (~100ns) + Paralelismo real em multi-core
+ Sem system call pra switch + I/O nao bloqueia todas
- Sem paralelismo real - Criacao mais cara (~1us)
- I/O bloqueia o processo - Context switch com system call
Modelos de Threading
A relacao entre user threads e kernel threads define o modelo de threading:
Many-to-One (M:1): One-to-One (1:1): Many-to-Many (M:N):
UT1 UT2 UT3 UT4 UT1 UT2 UT3 UT4 UT1 UT2 UT3 UT4 UT5
\ | | / | | | | \ | | / |
\ | | / | | | | \ | | / |
\ | |/ | | | | \| |/ |
\| | | | | | | | |
v v v v v v v v v
+----+ +--+ +--+ +--+ +--+ +--+ +--+ +--+
| KT | |KT| |KT| |KT| |KT| |KT| |KT| |KT|
+----+ +--+ +--+ +--+ +--+ +--+ +--+ +--+
Sem paralelismo. Paralelismo real. Melhor dos dois mundos.
Green threads. Linux usa este modelo. Complexo de implementar.
Ex: Java 1.0 Ex: Linux (NPTL) Ex: Go goroutines,
Ex: Java 1.2+ Java 21+ virtual threads
clone() — que cria um novo "task" que compartilha o espaco de enderecamento do pai. Para o kernel, threads e processos sao a mesma coisa: task_struct. A diferenca e quais recursos sao compartilhados.
Threads no Linux: clone()
fork(): clone(CLONE_VM | CLONE_FS | ...):
Cria novo processo Cria nova thread
Processo Pai Processo Filho Processo Thread Nova
+-----------+ +-----------+ +----------+-----------+
| code | | code COPY | | code | (shared) |
| data | | data COPY | | data | (shared) |
| heap | | heap COPY | | heap | (shared) |
| files | | files COPY| | files | (shared) |
| stack | | stack COPY| | stack | stack OWN |
| page table| | NEW p.t. | | page tbl | (shared) |
+-----------+ +-----------+ +----------+-----------+
Espaco de Espaco de MESMO espaco de
memoria memoria memoria
SEPARADO SEPARADO COMPARTILHADO
# Ver threads de um processo
$ ps -eLf | grep firefox
UID PID PPID LWP NLWP CMD
marco 1234 1 1234 85 firefox
marco 1234 1 1235 85 firefox
marco 1234 1 1236 85 firefox
# LWP = Light Weight Process (thread ID)
# NLWP = Number of LWPs (total de threads)
# Ver com top (pressione H para ver threads)
$ top -H -p 1234
# Ver via /proc
$ ls /proc/1234/task/
1234 1235 1236 1237 ...
# Contar threads de um processo
$ cat /proc/1234/status | grep Threads
Threads: 85
No harness.os
O padrao orchestrator do harness.os e um modelo de multi-threading aplicado a agentes de IA:
Processo = Sessao Orquestradora
+----------------------------------------------------------+
| Orchestrator Session (main thread) |
| |
| Shared Memory: |
| - Neon Postgres (harness DB) |
| - schema_reference (cached context) |
| - session_handoffs table |
| |
| +--------+ +--------+ +--------+ |
| | Agent | | Agent | | Agent | "Worker |
| | way2fly| | way2move| | way2save| Threads" |
| | | | | | | |
| | stack: | | stack: | | stack: | Cada um tem |
| | own | | own | | own | seu contexto |
| | context| | context| | context| proprio |
| +--------+ +--------+ +--------+ |
| | | | |
| v v v |
| Resultados coletados pelo orchestrator |
+----------------------------------------------------------+
Modelo: M:N (multiplos agentes : pool de CPU slots)
- Agentes sao "green threads" — gerenciados pelo orchestrator
- Compartilham o banco (shared memory)
- Cada um tem seu contexto independente (stack)
- Race condition: dois agentes escrevendo na mesma tabela
simultaneamente — resolvido por project_id isolation
import threading
class AgentThread(threading.Thread):
"""Cada agente delegado e como uma thread."""
def __init__(self, project_slug, task, shared_db):
super().__init__()
self.project_slug = project_slug # identidade
self.task = task # trabalho a fazer
self.shared_db = shared_db # "memoria compartilhada"
self.result = None # stack local
def run(self):
# Cada "thread" executa seu trabalho isolado
self.result = execute_task(
self.task,
db=self.shared_db,
project=self.project_slug
)
# Orchestrator: spawna "threads" em paralelo
agents = [
AgentThread("way2fly", "run QA", harness_db),
AgentThread("way2move", "run QA", harness_db),
AgentThread("way2save", "run QA", harness_db),
]
for agent in agents:
agent.start() # pthread_create equivalente
for agent in agents:
agent.join() # pthread_join equivalente
# Coletar resultados (como shared memory reads)
results = [a.result for a in agents]
A analogia com threads e precisa: os agentes compartilham o mesmo banco de dados (shared memory), mas cada um tem seu proprio contexto de conversacao (stack). O risco de race condition existe — dois agentes escrevendo no mesmo registro simultaneamente — e e mitigado pelo isolamento por project_id (como um mutex por recurso).
Homework
- Compile e execute o programa de race condition (com e sem mutex). Execute varias vezes o caso unsafe e observe como o resultado varia.
- Conte quantas threads o Firefox (ou outro browser) usa na sua maquina com
ps -eLf | grep firefox | wc -l. - No harness.os, quando o orchestrator spawna 3 agentes em paralelo, qual e o modelo de threading? Justifique se e M:1, 1:1, ou M:N.
- Identifique uma potencial race condition no harness.os: dois agentes chamando
log_learning()ao mesmo tempo com dados sobre o mesmo topico. Como o banco Postgres resolve isso? (Dica: MVCC.)
Resumo
- Thread = fluxo de execucao dentro de um processo; compartilha memoria, tem stack/regs proprios
- Vantagens: criacao rapida, compartilhamento facil, paralelismo real em multi-core
- pthreads:
pthread_create(),pthread_join(),pthread_mutex_lock() - Modelos: M:1 (green threads), 1:1 (Linux NPTL), M:N (Go goroutines)
- No Linux, threads sao tarefas (task_struct) que compartilham espaco de enderecamento via clone()
Verifique seu entendimento
Qual e a principal diferenca entre criar um processo com fork() e criar uma thread com pthread_create()?