C Threads (pthreads)

O computador da Ana tem vários núcleos: vários processadores dentro de um só. Um programa comum usa um núcleo por vez; os outros ficam parados. Com threads (em inglês, "fios", como os fios de uma corda), um mesmo programa faz várias coisas ao mesmo tempo, cada uma num núcleo, todas dividindo as mesmas variáveis. Isso deixa programas muito mais rápidos e é também a fonte de alguns dos erros mais traiçoeiros da programação. Nesta aula você vai criar threads com a biblioteca pthreads, ver duas threads estragarem uma conta simples, e consertar com um mutex e com variáveis atômicas.

Você vai aprender
  • O que é uma thread, e a diferença para um processo.
  • Como criar uma thread com pthread_create, esperar com pthread_join e compilar com -pthread.
  • Como mandar dados para a thread (e receber de volta) por um ponteiro void *.
  • O que é uma condição de corrida, e por que vendas++ em duas threads perde vendas.
  • Como proteger os dados com pthread_mutex_lock e com _Atomic.
  • Como achar corridas com -fsanitize=thread, e o que é um impasse (deadlock).

Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/threads, com gcc -std=c23 -Wall -Wextra -pthread arquivo.c -o programa. A ordem das linhas e os números das corridas mudam a cada execução: no seu computador, eles vão ser outros.

As pthreads são do POSIX (Linux, macOS e WSL; aula Instalar no Windows e no Mac). O editor do guia roda o programa numa única linha de execução: o <pthread.h> existe e compila, mas o pthread_create sempre falha. As caixas "Experimente" desta aula praticam as partes que rodam em qualquer lugar.

A ideia: várias cozinheiras na mesma cozinha

Na aula Processos, o fork clonava a cozinheira e a cozinha inteira: cada uma com a sua bancada, os seus ingredientes. Uma não atrapalha a outra, mas também não vê o que a outra fez.

Threads são várias cozinheiras na mesma cozinha:

  • dividem a bancada, a geladeira e o caderno de anotações (as variáveis globais, a memória do malloc);
  • cada uma tem a sua própria tábua (as variáveis locais: cada thread tem a sua pilha);
  • trabalham ao mesmo tempo, então o jantar sai mais rápido;
  • e, se duas forem escrever no mesmo caderno ao mesmo tempo sem combinar, a anotação sai errada. Esse é o assunto da seção 3.

1. A primeira thread

#include <stdio.h>
#include <pthread.h>

void *cozinhar(void *arg) {
    (void) arg;
    printf("Thread: estou cozinhando o arroz.\n");
    return NULL;
}

int main(void) {
    pthread_t ajudante;
    if (pthread_create(&ajudante, NULL, cozinhar, NULL) != 0) {
        fprintf(stderr, "Não consegui criar a thread.\n");
        return 1;
    }
    printf("main: enquanto isso, eu corto a salada.\n");
    pthread_join(ajudante, NULL);
    printf("main: o arroz ficou pronto. Hora de comer!\n");
    return 0;
}
#include <pthread.h>
"POSIX threads", a biblioteca de threads do Linux e do macOS. Todos os nomes dela começam com pthread_.
void *cozinhar(void *arg)
A função que a thread vai rodar. O formato é obrigatório: recebe um void * e devolve um void * (ponteiros "para qualquer coisa": aula Ponteiro void). Com eles, dá para mandar e receber qualquer tipo de dado (seção 2).
pthread_t ajudante;
A "identidade" da thread, como o pid_t de um processo.
pthread_create(&ajudante, NULL, cozinhar, NULL)
Cria a thread e ela começa a rodar cozinhar na hora, ao mesmo tempo que o main. Os quatro argumentos: onde guardar a identidade, as opções (NULL = as normais), a função e o argumento que a função vai receber. Devolve 0 se deu certo, ou um código de erro.
pthread_join(ajudante, NULL)
"Juntar": o main espera a thread terminar, como o waitpid espera um processo. O NULL é onde guardar o que a função devolveu (não precisamos).
$ gcc -std=c23 -Wall -Wextra -pthread ola.c -o ola
$ ./ola
main: enquanto isso, eu corto a salada.
Thread: estou cozinhando o arroz.
main: o arroz ficou pronto. Hora de comer!

Enquanto a thread cozinhava, o main seguiu sozinho: cortou a salada e parou no pthread_join até o arroz ficar pronto. Desta vez o main escreveu primeiro, mas isso não é garantido: as duas rodam ao mesmo tempo, e quem chega primeiro ao printf varia.

O -pthread na linha do gcc prepara a compilação e a ligação para threads. Desde 2021 (glibc 2.34), as funções das pthreads moram dentro da biblioteca C principal, e o programa liga mesmo sem a opção; em sistemas mais antigos, sem ela (ou sem o -lpthread), aparece undefined reference to `pthread_create'. Use sempre o -pthread: funciona em todo lugar.

Experimente: o mesmo programa no editor do guia, agora mostrando o código de erro do pthread_create com strerror. Aqui ele falha: o editor roda o programa numa linha de execução só. Repare como o teste do retorno (aula errno e perror) evitou que o programa seguisse como se a thread existisse.

#include <stdio.h>
#include <string.h>
#include <pthread.h>

void *cozinhar(void *arg) {
    (void) arg;
    return NULL;
}

int main(void) {
    pthread_t ajudante;
    int erro = pthread_create(&ajudante, NULL, cozinhar, NULL);
    if (erro != 0) {
        fprintf(stderr, "pthread_create: %s (código %d)\n", strerror(erro), erro);
        return 1;
    }
    pthread_join(ajudante, NULL);
    printf("Deu certo.\n");
    return 0;
}

Atenção a um detalhe do pthread_create: ele devolve o código do erro, em vez de pôr em errno como as funções das aulas anteriores. Por isso o programa usa strerror(erro), e não perror. No Linux, o mesmo programa mostra Deu certo.


2. Mandar dados para a thread

O último argumento do pthread_create chega na função como o void *arg. Para mandar várias informações, junte tudo numa struct e mande o endereço. A thread também pode escrever o resultado na mesma struct:

#include <stdio.h>
#include <pthread.h>

struct pedido {
    const char *prato;
    int quantidade;
    int minutos;          // a thread preenche
};

void *preparar(void *arg) {
    struct pedido *p = arg;
    p->minutos = p->quantidade * 5;
    printf("Preparando %d x %s\n", p->quantidade, p->prato);
    return NULL;
}

int main(void) {
    struct pedido pedidos[3] = {
        {"pastel", 4, 0},
        {"suco", 2, 0},
        {"bolo", 1, 0},
    };
    pthread_t cozinheiras[3];

    for (int i = 0; i < 3; i++) {
        pthread_create(&cozinheiras[i], NULL, preparar, &pedidos[i]);
    }
    for (int i = 0; i < 3; i++) {
        pthread_join(cozinheiras[i], NULL);
    }
    for (int i = 0; i < 3; i++) {
        printf("%s: %d minutos\n", pedidos[i].prato, pedidos[i].minutos);
    }
    return 0;
}
LinhaO que faz
struct pedido *p = arg;Converte o void * de volta para o tipo verdadeiro. Em C, essa conversão é automática (não precisa de (struct pedido *)).
p->minutos = …Escreve o resultado direto na struct do main (aula Structs e Ponteiros).
pthread_t cozinheiras[3];Um array de identidades, uma por thread.
&pedidos[i]Cada thread recebe o endereço do seu pedido. Nenhuma mexe no pedido da outra.
O segundo forEspera as três terminarem. Só depois o main pode ler os minutos com segurança.
$ gcc -std=c23 -Wall -Wextra -pthread argumento.c -o argumento
$ ./argumento
Preparando 4 x pastel
Preparando 1 x bolo
Preparando 2 x suco
pastel: 20 minutos
suco: 10 minutos
bolo: 5 minutos
$ ./argumento
Preparando 4 x pastel
Preparando 2 x suco
Preparando 1 x bolo
pastel: 20 minutos
suco: 10 minutos
bolo: 5 minutos

As três threads rodaram ao mesmo tempo, e a ordem do "Preparando" mudou de uma execução para a outra (o bolo passou na frente do suco na primeira). Já o resultado final é sempre o mesmo: cada thread escreveu só no seu pedido, e o main só leu depois dos pthread_join.

Missão: a parte mais delicada de uma thread é a conversão do void *. Ela não depende de threads, então dá para praticar aqui: o main chama preparar direto, do jeito que o pthread_create chamaria. Complete preparar: converta arg para struct pedido * numa variável (struct pedido *p = arg;) e calcule minutos = quantidade * 5. A saída deve ser pastel: 20 minutos.

#include <stdio.h>

struct pedido {
    const char *prato;
    int quantidade;
    int minutos;
};

void *preparar(void *arg) {
    // converta arg para struct pedido * e calcule minutos = quantidade * 5
    (void) arg;
    return NULL;
}

int main(void) {
    struct pedido p = {"pastel", 4, 0};
    preparar(&p);       // no Linux: pthread_create(&t, NULL, preparar, &p);
    printf("%s: %d minutos\n", p.prato, p.minutos);
    return 0;
}
#include <stdio.h>

struct pedido {
    const char *prato;
    int quantidade;
    int minutos;
};

void *preparar(void *arg) {
    // converta arg para struct pedido * e calcule minutos = quantidade * 5
    struct pedido *p = arg;
    p->minutos = p->quantidade * 5;
    return NULL;
}

int main(void) {
    struct pedido p = {"pastel", 4, 0};
    preparar(&p);       // no Linux: pthread_create(&t, NULL, preparar, &p);
    printf("%s: %d minutos\n", p.prato, p.minutos);
    return 0;
}

3. Condição de corrida: as vendas que somem

Agora as duas threads mexem na mesma variável. A lojinha tem dois caixas, e cada um registra um milhão de vendas no mesmo contador:

#include <stdio.h>
#include <pthread.h>

long vendas = 0;

void *registrar(void *arg) {
    (void) arg;
    for (int i = 0; i < 1000000; i++) {
        vendas++;
    }
    return NULL;
}

int main(void) {
    pthread_t caixa1, caixa2;
    pthread_create(&caixa1, NULL, registrar, NULL);
    pthread_create(&caixa2, NULL, registrar, NULL);
    pthread_join(caixa1, NULL);
    pthread_join(caixa2, NULL);
    printf("Vendas: %ld (esperado: 2000000)\n", vendas);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -pthread corrida.c -o corrida
$ ./corrida
Vendas: 1367943 (esperado: 2000000)
$ ./corrida
Vendas: 913709 (esperado: 2000000)
$ ./corrida
Vendas: 1173979 (esperado: 2000000)

Nenhum aviso do compilador, e uma resposta errada e diferente a cada vez. Mais de um milhão de vendas sumiram! O motivo é que vendas++ parece uma coisa só, mas para o processador são três passos:

  1. ler o valor de vendas da memória para um registrador;
  2. somar 1 no registrador;
  3. gravar o registrador de volta na memória.

Se os dois caixas leem o mesmo valor (digamos, 500) antes de qualquer um gravar, os dois somam 1 e os dois gravam 501. Duas vendas, mas o contador só andou uma. Quando o resultado depende de quem chega primeiro, o nome é condição de corrida (race condition). Para o padrão C, duas threads escrevendo na mesma variável sem proteção é comportamento indefinido. A missão final desta aula simula os três passos, um por linha, no editor.


4. Mutex: um de cada vez

A solução clássica é um mutex ("mutual exclusion", exclusão mútua): uma trava que só uma thread segura por vez. É como a chave do banheiro do posto de gasolina: quem quer entrar pega a chave; se ela não está no gancho, espera; quem sai devolve.

#include <stdio.h>
#include <pthread.h>

long vendas = 0;
pthread_mutex_t trava = PTHREAD_MUTEX_INITIALIZER;

void *registrar(void *arg) {
    (void) arg;
    for (int i = 0; i < 1000000; i++) {
        pthread_mutex_lock(&trava);
        vendas++;
        pthread_mutex_unlock(&trava);
    }
    return NULL;
}

int main(void) {
    pthread_t caixa1, caixa2;
    pthread_create(&caixa1, NULL, registrar, NULL);
    pthread_create(&caixa2, NULL, registrar, NULL);
    pthread_join(caixa1, NULL);
    pthread_join(caixa2, NULL);
    printf("Vendas: %ld (esperado: 2000000)\n", vendas);
    return 0;
}
pthread_mutex_t trava = PTHREAD_MUTEX_INITIALIZER;
Cria a trava, já pronta para usar (aberta). Fica global, para todas as threads usarem a mesma.
pthread_mutex_lock(&trava)
"Trancar": pega a trava. Se outra thread estiver com ela, esta espera, parada, até ela ser devolvida.
vendas++;
A região crítica: o pedaço que só uma thread pode fazer por vez. Os três passos agora acontecem sem ninguém no meio.
pthread_mutex_unlock(&trava)
"Destrancar": devolve a trava. Esquecer o unlock deixa todas as outras threads esperando para sempre.
$ gcc -std=c23 -Wall -Wextra -pthread mutex.c -o mutex
$ time ./mutex
Vendas: 2000000 (esperado: 2000000)

real	0m0,077s
user	0m0,094s
sys	0m0,049s
$ time ./corrida
Vendas: 1253101 (esperado: 2000000)

real	0m0,005s
user	0m0,009s
sys	0m0,000s

Certo, sempre. O preço: pegar e devolver a trava dois milhões de vezes deixou o programa umas 15 vezes mais lento que a versão errada (o comando time do Bash mostra o tempo; real é o do relógio). Na prática, a região crítica é maior que um ++ e o custo da trava pesa menos. A regra é deixar dentro do lock/unlock só o que mexe nos dados divididos.


5. _Atomic: o contador indivisível C11

Para um contador simples, o C11 tem uma solução mais leve: as variáveis atômicas (de <stdatomic.h>). Numa variável atômica, o ++ vira uma única instrução especial do processador, que ninguém consegue interromper no meio:

#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>

atomic_long vendas = 0;

void *registrar(void *arg) {
    (void) arg;
    for (int i = 0; i < 1000000; i++) {
        vendas++;
    }
    return NULL;
}

int main(void) {
    pthread_t caixa1, caixa2;
    pthread_create(&caixa1, NULL, registrar, NULL);
    pthread_create(&caixa2, NULL, registrar, NULL);
    pthread_join(caixa1, NULL);
    pthread_join(caixa2, NULL);
    printf("Vendas: %ld (esperado: 2000000)\n", (long) vendas);
    return 0;
}
atomic_long vendas = 0;
Um long atômico. É o mesmo que _Atomic long (a palavra-chave do C11); o stdatomic.h dá nomes prontos: atomic_int, atomic_bool…
vendas++
O mesmo código de antes, mas agora os três passos acontecem de uma vez só. Também existem funções explícitas, como atomic_fetch_add(&vendas, 1).
(long) vendas
Lê o valor atômico como um long comum para o printf.
$ gcc -std=c23 -Wall -Wextra -pthread atomico.c -o atomico
$ time ./atomico
Vendas: 2000000 (esperado: 2000000)

real	0m0,024s
user	0m0,046s
sys	0m0,000s

Certo e três vezes mais rápido que o mutex. Mas só serve para operações simples numa variável só. Para "tirar do estoque e somar no caixa" como uma coisa só, com duas variáveis, use um mutex.

E o volatile? Ele não resolve corridas. O volatile só obriga o compilador a ler e escrever na memória toda vez; os três passos do ++ continuam separados, e duas threads continuam podendo se intrometer no meio. Para threads, use _Atomic ou um mutex (aula volatile e restrict).

6. O fiscal das threads: -fsanitize=thread

Corridas são difíceis de achar: o programa pode dar a resposta certa 99 vezes e errar na centésima. O gcc e o Clang têm um fiscal que vigia cada acesso à memória e avisa quando duas threads mexem na mesma variável sem proteção: o ThreadSanitizer (aula Sanitizers).

$ gcc -std=c23 -Wall -Wextra -g -fsanitize=thread corrida.c -o corrida-tsan
$ ./corrida-tsan 2>&1 | head -21
==================
WARNING: ThreadSanitizer: data race (pid=3)
  Read of size 8 at 0x559adca94018 by thread T2:
    #0 registrar /home/ana/c/threads/corrida.c:9 (corrida-tsan+0x127d) (BuildId: 8d4b677db1f7512da80073296a1c335c21a0c819)

  Previous write of size 8 at 0x559adca94018 by thread T1:
    #0 registrar /home/ana/c/threads/corrida.c:9 (corrida-tsan+0x1297) (BuildId: 8d4b677db1f7512da80073296a1c335c21a0c819)

  Location is global 'vendas' of size 8 at 0x559adca94018 (corrida-tsan+0x4018)

  Thread T2 (tid=7, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1078 (libtsan.so.2+0x5cb02) (BuildId: 66d37dc704b813df6c813b77217284deb43a0042)
    #1 main /home/ana/c/threads/corrida.c:17 (corrida-tsan+0x1320) (BuildId: 8d4b677db1f7512da80073296a1c335c21a0c819)

  Thread T1 (tid=6, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1078 (libtsan.so.2+0x5cb02) (BuildId: 66d37dc704b813df6c813b77217284deb43a0042)
    #1 main /home/ana/c/threads/corrida.c:16 (corrida-tsan+0x1303) (BuildId: 8d4b677db1f7512da80073296a1c335c21a0c819)

SUMMARY: ThreadSanitizer: data race /home/ana/c/threads/corrida.c:9 in registrar
==================
==================
Linha do relatórioO que diz
WARNING: ThreadSanitizer: data race"Corrida de dados": duas threads, a mesma memória, sem proteção.
Read of size 8 … by thread T2 / Previous write … by thread T1A thread 2 leu 8 bytes (o long) que a thread 1 tinha escrito, os dois na linha 9 (o vendas++).
Location is global 'vendas'O nome da variável disputada.
Thread T2 … created by main thread at: … corrida.c:17Onde cada thread nasceu (os pthread_create das linhas 16 e 17).

O head -21 mostrou só o primeiro aviso; no fim, o programa diz ThreadSanitizer: reported 2 warnings (dois avisos). Compilado com o mesmo fiscal, o mutex.c roda sem nenhum aviso. Use o -fsanitize=thread sempre que testar um programa com threads (ele não combina com o -fsanitize=address: um de cada vez).


7. Impasse: cada uma esperando a outra

Com mais de uma trava aparece outro perigo. A Ana e a Bia vão preparar uma salada, e há uma faca e uma tábua (dois mutex). A Ana pega a faca e depois a tábua; a Bia pega a tábua e depois a faca:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t faca = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t tabua = PTHREAD_MUTEX_INITIALIZER;

void *ana(void *arg) {
    (void) arg;
    pthread_mutex_lock(&faca);
    printf("Ana pegou a faca e espera a tábua...\n");
    sleep(1);
    pthread_mutex_lock(&tabua);
    printf("Ana cortou o tomate.\n");
    pthread_mutex_unlock(&tabua);
    pthread_mutex_unlock(&faca);
    return NULL;
}

void *bia(void *arg) {
    (void) arg;
    pthread_mutex_lock(&tabua);
    printf("Bia pegou a tábua e espera a faca...\n");
    sleep(1);
    pthread_mutex_lock(&faca);
    printf("Bia cortou a cebola.\n");
    pthread_mutex_unlock(&faca);
    pthread_mutex_unlock(&tabua);
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, ana, NULL);
    pthread_create(&t2, NULL, bia, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Salada pronta!\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -pthread impasse.c -o impasse
$ timeout 3 ./impasse; echo "código: $?"
Ana pegou a faca e espera a tábua...
Bia pegou a tábua e espera a faca...
código: 124

A Ana segura a faca esperando a tábua; a Bia segura a tábua esperando a faca. Nenhuma solta o que tem, e as duas esperam para sempre. Isso é um impasse (deadlock). O programa não dá erro nem gasta processador: só para. O timeout 3 o matou depois de 3 segundos (código 124). O sleep(1) está ali só para o impasse acontecer sempre; sem ele, aconteceria de vez em quando, o que é ainda pior.

O conserto: todas as threads pegam as travas na mesma ordem. Se a Bia também pegar primeiro a faca, quem chegar segundo espera de mãos vazias, e ninguém fica preso.


Qual usar?

SituaçãoUse
Cada thread trabalha só nos seus dados (como os pedidos da seção 2)Nada: só pthread_join antes de ler os resultados
Um contador ou uma bandeira dividida_Atomic / atomic_int
Várias variáveis que mudam juntas, listas, arquivospthread_mutex_t
Tarefas que não precisam dividir memória, e em que uma não pode derrubar a outraProcessos (fork, aula Processos)
Código em C puro, sem POSIX<threads.h> do C11 (thrd_create, mtx_lock): mesma ideia, outros nomes; é opcional no padrão, e o gcc com glibc tem

Para comparar, a primeira thread com o <threads.h> do C11 (no editor do guia, ele não existe: dá erro do ligador, undefined symbol: thrd_create):

#include <stdio.h>
#include <threads.h>

int cozinhar(void *arg) {
    (void) arg;
    printf("Thread C11: estou cozinhando o arroz.\n");
    return 0;
}

int main(void) {
    thrd_t ajudante;
    if (thrd_create(&ajudante, cozinhar, NULL) != thrd_success) {
        fprintf(stderr, "Não consegui criar a thread.\n");
        return 1;
    }
    thrd_join(ajudante, NULL);
    printf("main: pronto.\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -pthread c11.c -o c11
$ ./c11
Thread C11: estou cozinhando o arroz.
main: pronto.

As pthreads são muito mais usadas (livros, bibliotecas, exemplos na internet) e têm mais recursos; por isso a aula usa elas.


Quando dá errado

error: passing argument 3 of ‘pthread_create’ from incompatible pointer type

A função da thread foi escrita como void cozinhar(void):

$ gcc -std=c23 -Wall -Wextra -pthread assinatura.c -o assinatura
assinatura.c: In function ‘main’:
assinatura.c:10:37: error: passing argument 3 of ‘pthread_create’ from incompatible pointer type [-Wincompatible-pointer-types]
   10 |     pthread_create(&ajudante, NULL, cozinhar, NULL);
      |                                     ^~~~~~~~
      |                                     |
      |                                     void (*)(void)
In file included from assinatura.c:2:
/usr/include/pthread.h:204:36: note: expected ‘void * (*)(void *)’ but argument is of type ‘void (*)(void)’
  204 |                            void *(*__start_routine) (void *),
      |                            ~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~
assinatura.c:4:6: note: ‘cozinhar’ declared here
    4 | void cozinhar(void) {
      |      ^~~~~~~~

O que significa: o pthread_create espera void * (*)(void *), "uma função que recebe void * e devolve void *" (aula Ponteiros para Funções).

Como consertar: void *cozinhar(void *arg), com (void) arg; se não usar o argumento e return NULL; no fim.

A thread não escreveu nada

O main criou uma thread que dorme 1 segundo e depois escreve "o arroz está pronto", mas esqueceu o pthread_join:

$ gcc -std=c23 -Wall -Wextra -pthread semjoin.c -o semjoin
$ ./semjoin
main: terminei.

O que significa: quando o main termina, o processo inteiro termina, com todas as threads dentro, prontas ou não.

Como consertar: um pthread_join para cada pthread_create, antes do return do main.

O resultado muda a cada execução

O que significa: uma condição de corrida (seção 3). Duas threads mexem na mesma variável, e pelo menos uma escreve, sem mutex nem _Atomic.

Como consertar: compile com -g -fsanitize=thread para achar a variável e a linha, e proteja-a com um mutex ou torne-a atômica.

O programa para e não termina nunca, sem usar o processador

O que significa: um impasse (seção 7), ou um pthread_mutex_lock sem o unlock correspondente (por exemplo, um return no meio da região crítica).

Como consertar: pegue as travas sempre na mesma ordem, e confira que todo caminho que sai da região crítica passa pelo unlock.

Funções que "misturam" resultados entre threads

O que significa: algumas funções antigas guardam dados num lugar só, dentro da biblioteca: localtime, gmtime e strtok. Duas threads chamando ao mesmo tempo escrevem uma por cima da outra (aula Data e Hora, seção 7).

Como consertar: use as versões _r ("reentrant"): localtime_r, gmtime_r, strtok_r. O errno, por sua vez, é seguro: cada thread tem o seu (aula errno e perror).


Referência rápida

EscritaO que faz
gcc … -pthreadCompila e liga com suporte a threads
void *f(void *arg) { … return NULL; }O formato da função de uma thread
pthread_create(&t, NULL, f, &dados)Cria a thread t rodando f(&dados); devolve 0 ou o código do erro
pthread_join(t, NULL)Espera t terminar (o segundo argumento recebe o void * devolvido)
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;Cria uma trava
pthread_mutex_lock(&m) / pthread_mutex_unlock(&m)Pega / devolve a trava
atomic_int x = 0; (<stdatomic.h>)Inteiro com ++, --, += indivisíveis (C11)
-g -fsanitize=threadLiga o ThreadSanitizer, que acha corridas
thrd_create, thrd_join, mtx_lock (<threads.h>)As threads do C11 padrão

Exercícios

1. Esperar a thread

Complete a função que faz o main esperar a thread t:

(t, NULL);

2. Pegar a trava

Complete a linha que vem antes de vendas++; num programa com o mutex trava:

3. A opção do gcc

Complete a opção de compilação para threads:

gcc -std=c23 -Wall -Wextra programa.c -o programa

4. Qual é a principal diferença entre duas threads e dois processos criados com fork?

5. Duas threads fazem contador++ um milhão de vezes cada, sem proteção. O que acontece?

6. Como evitar o impasse entre duas threads que usam as travas faca e tabua?

Missão final: a venda perdida, passo a passo

Missão: este programa não tem threads: cada linha é um dos três passos do vendas++ (ler, somar, gravar) de um dos dois caixas, na ordem em que o processador os misturou. O resultado é a corrida da seção 3 em câmera lenta: duas vendas, contador 1. Reordene as linhas do jeito que o mutex obriga: o caixa 1 faz os seus três passos, e só depois o caixa 2 começa. A saída deve ser Vendas: 2.

#include <stdio.h>

int main(void) {
    int vendas = 0;
    int a, b;               // a "memória de rascunho" de cada caixa

    a = vendas;             // caixa 1 lê
    b = vendas;             // caixa 2 lê
    a = a + 1;              // caixa 1 soma
    b = b + 1;              // caixa 2 soma
    vendas = a;             // caixa 1 grava
    vendas = b;             // caixa 2 grava

    printf("Vendas: %d\n", vendas);
    return 0;
}
#include <stdio.h>

int main(void) {
    int vendas = 0;
    int a, b;               // a "memória de rascunho" de cada caixa

    a = vendas;             // caixa 1 lê
    a = a + 1;              // caixa 1 soma
    vendas = a;             // caixa 1 grava
    b = vendas;             // caixa 2 lê
    b = b + 1;              // caixa 2 soma
    vendas = b;             // caixa 2 grava

    printf("Vendas: %d\n", vendas);
    return 0;
}

Os a e b fazem o papel dos registradores de cada núcleo. No programa de verdade, quem garante esta ordem é o pthread_mutex_lock: enquanto o caixa 1 segura a trava, o caixa 2 espera antes do "ler".


Resumo

  • Uma thread é uma linha de execução dentro do processo. As threads de um programa rodam ao mesmo tempo e dividem as variáveis globais e a memória do malloc; cada uma tem as suas variáveis locais.
  • pthread_create(&t, NULL, f, arg) cria; pthread_join(t, NULL) espera. A função tem o formato void *f(void *arg). Compile com -pthread.
  • Dados vão para a thread pelo void *: o endereço de uma struct, convertido de volta dentro da função.
  • Duas threads escrevendo na mesma variável sem proteção é uma condição de corrida: resultados errados, diferentes a cada vez, e comportamento indefinido.
  • Proteja com um mutex (pthread_mutex_lock/unlock) ou, para contadores, com _Atomic. volatile não basta.
  • Ache corridas com -g -fsanitize=thread. Evite impasses pegando as travas sempre na mesma ordem.
  • O editor do guia não tem threads: lá, pthread_create devolve "Not supported".