Threads

Agora que entendemos como funciona o forking de processos, vamos explorar as threads. Threads são uma forma mais leve de concorrência, permitindo que múltiplas tarefas sejam executadas dentro do mesmo processo, compartilhando o mesmo espaço de memória.

Conforme explicado na primeira parte do guia, no sistema operacional podemos criar threads através da chamada de sistema clone com os argumentos corretos. Na linguagem C, conseguimos manipular threads do sistema operacional, também chamadas de kernel threads.

Entretanto em C, embora podemos, não precisamos chamar a syscall clone diretamente devido à sua complexidade inerente. Mas para nossa sorte, na biblioteca padrão temos acesso a um cabeçalho chamado pthread.h, que abstrai de forma muito mais simples a criação de kernel threads utilizando POSIX Threads (ou pthreads), que são o padrão em sistemas UNIX-like:

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

void* handle() {
	printf("Hello from thread!\n");
	return NULL;
}

int main() {
	pthread_t thread;

	// Cria uma thread
	pthread_create(&thread, NULL, handle, NULL);

	printf("Hello from main thread!\n");
	return 0;
}

A função pthread_create recebe:

  • um ponteiro para a variável que irá referenciar a thread na memória (pthread_t)

  • um ponteiro para a função que será executada no contexto da thread

  • outros argumentos opcionais, que iremos deixar como NULL

Ao executar por algumas vezes o programa, podemos perceber a inconsistência nas mensagens, muitas vezes imprimindo apenas:

Hello from main thread

Bom, se você leu direitinho a primeira parte do guia, vai se lembrar que:

Todo programa tem uma thread principal

Ou seja, quando o processo é iniciado, ele é encapsulado dentro de uma thread chamada "principal", pelo que quando falamos do processo em si, estamos também falando desta thread principal.

Entretanto, podemos ver que a mensagem da thread criada com pthread_create não apareceu na saída, e isto se deve à natureza de concorrência do escalonamento de tarefas do sistema operacional, onde verificamos em ação no tópico anterior com forking de processos.

Se eu rodar o programa, pode ser que a thread foi escalonada rapidamente e a mensagem aparece com sucesso. Mas pode ser também que a thread ainda não foi escalonada e o programa principal já foi finalizado. Enfim:

Não temos controle algum sobre a ordem e execução das tarefas no sistema operacional!

Como fazer com que o programa principal "espere" uma ou mais threads em execução serem finalizadas?

Thread Join

Com a função pthread_join , o contexto da thread será trazido para o mesmo contexto da thread principal, então na prática o programa irá esperar pela execução da thread até que ela seja finalizada:

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

void* handle() {
	printf("Hello from thread!\n");
	return NULL;
}

int main() {
	pthread_t thread;

	// Cria uma thread
	pthread_create(&thread, NULL, handle, NULL);

	// Aguarda a thread terminar
	pthread_join(thread, NULL);

	printf("Hello from main thread!\n");
	return 0;
}

Saída:

Hello from thread!
Hello from main thread!

Wow! How cool is that?

Um exemplo mais robusto

Já vimos os building blocks para criação de threads em C. Agora, vamos a um exemplo um pouco mais robusto, similar ao que aprendemos no tópico de forking de processos:

pthreads-complex.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* handle(void* arg) {
	int thread_id = *((int*)arg); // Recebe o ID da thread
	printf("Thread %d is running...\n", thread_id);
	sleep(2); // Simula uma tarefa que leva 2 segundos
	printf("Thread %d is finished\n", thread_id);
	return NULL;
}

int main() {
	int num_threads = 3;
	pthread_t threads[num_threads];
	int thread_ids[num_threads];

	for (int i = 0; i < num_threads; i++) {
		thread_ids[i] = i + 1; // Identificação das threads
		pthread_create(&threads[i], NULL, handle, &thread_ids[i]);
	}

	for (int i = 0; i < num_threads; i++) {
		pthread_join(threads[i], NULL); // Aguarda cada thread terminar
		printf("Thread %d has been finished.\n", i + 1);
	}

	printf("All threads are finished.\n");
	return 0;
}
Thread 1 is running...
Thread 2 is running...
Thread 3 is running...
Thread 3 is finished
Thread 2 is finished
Thread 1 is finished
Thread 1 has been finished.
Thread 2 has been finished.
Thread 3 has been finished.
All threads are finished.

Repare que a ordem de execução pode mudar, devido à (e lá vamos novamente repetir) natureza da concorrência.

Diferenças entre forking e threads

Até o momento, exploramos 2 formas de concorrência em C que são forking de processos e threads.

No forking, a comunicação entre processos (IPC) precisa ser feita através de pipes ou forçar algum mecanismo de compartilhamento de memória, pois por padrão os processos não compartilham memória uns com os outros.

Ja com threads, cada thread compartilha o mesmo contexto que é a memória do processo principal, ou seja, duas ou mais threads podem provocar um cenário de condição de corrida se precisarem ler/escrever no mesmo recurso.

Para mitigar problemas de acesso a recurso compartilhado entre threads, precisamos recorrer ao uso de locks.

Last updated