College Online
0%

Threads: Conceitos e Modelos

Modulo 2 · Aula 3 ~20 min de leitura Nivel: Fundamental

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:

Diagrama — Processo Single-thread vs Multi-thread
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
+------------------+           +----------------------+
i
Thread vs. Processo Criar uma thread e ~10-100x mais rapido que criar um processo (fork), porque nao e necessario copiar o espaco de enderecamento. Context switch entre threads do mesmo processo tambem e mais rapido, pois nao requer TLB flush (mesma tabela de paginas).

Por que usar Threads?

POSIX Threads (pthreads)

A API padrao para threads em C/Unix e a POSIX Threads (pthreads). Vamos ver as operacoes fundamentais:

C — Criacao basica de threads
#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
!
Race condition classica Como threads compartilham memoria, acessar dados compartilhados sem sincronizacao causa race conditions. O resultado depende da ordem de execucao das threads, que e imprevisivel. Sempre use mutexes, semaforos, ou atomics ao acessar dados compartilhados.

Sincronizacao com Mutex

C — Race condition e correcao 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:

Diagrama — User vs Kernel Threads
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:

Diagrama — Modelos M:1, 1:1, M:N
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
*
Linux e 1:1 No Linux, threads sao implementadas com 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()

Diagrama — fork() vs 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
Shell — Observando threads
# 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:

Diagrama — Agentes como Threads
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
Python — Agentes paralelos como threads
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

  1. Compile e execute o programa de race condition (com e sem mutex). Execute varias vezes o caso unsafe e observe como o resultado varia.
  2. Conte quantas threads o Firefox (ou outro browser) usa na sua maquina com ps -eLf | grep firefox | wc -l.
  3. 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.
  4. 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

Verifique seu entendimento

Qual e a principal diferenca entre criar um processo com fork() e criar uma thread com pthread_create()?

  • Threads podem executar codigo diferente, processos nao
  • Threads compartilham o espaco de enderecamento do processo pai; fork cria uma copia separada
  • Processos podem rodar em paralelo em multi-core, threads nao
  • Threads sao gerenciadas pelo hardware, processos pelo SO