concorrencia101
  • Introdução
  • First things first
  • Agradecimentos
  • Parte I - Concorrência no sistema operacional
    • O que é o programa no sistema operacional
    • Escalonador preemptivo de tarefas
    • Uma nota sobre escalonamento cooperativo
    • Propriedades de um processo
    • Clone de processo (forking)
    • Clone leve de processo (thread)
    • Todo processo tem uma thread principal
    • Uma nota sobre paralelismo
    • Principais desafios em cenário de concorrência
      • Race condition
      • Sincronização com locks
      • Modelo de atores
    • E o I/O?
      • Latência de CPU vs Latência de I/O
      • Chamadas bloqueantes
      • Chamadas não-bloqueantes
      • Assincronismo e escalonamento cooperativo
    • Vamos colocar em prática...
  • PARTE II - Concorrência em diferentes linguagens
    • Definindo ambientes de execução
    • Concorrência em C
      • Forking de processos
      • Threads
      • Race condition e sincronização de threads com mutex
      • Desafios com o uso de threads
      • Thread Pool em C
      • Green threads
      • Modelo de Atores
      • Trabalhando com I/O
    • Concorrência em Ruby
      • Forking de processos
      • Threads
      • Race condition, YARV, GVL e paralelismo em Ruby
      • Modelo de Atores
      • Trabalhando com I/O
Powered by GitBook
On this page
  • Thread Join
  • Um exemplo mais robusto
  • Diferenças entre forking e threads
  1. PARTE II - Concorrência em diferentes linguagens
  2. Concorrência em C

Threads

PreviousForking de processosNextRace condition e sincronização de threads com mutex

Last updated 4 months ago

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 (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.

POSIX Threads