Skip to content

[GUIDE] Structs

Lunna Boo edited this page Aug 7, 2026 · 16 revisions

Após alguns meses desenvolvendo em Python, meu cérebro passou a pensar de maneira mais orientada a objetos. O que pode ser bom num projeto desse tamanho, mas pode atrapalhar caso você leve isso longe demais.
Nós queremos um projeto estruturado com boa encapsulação de dados, e não exatamente Orientado a Objeto.

Criei 5 structs:

  1. t_coder
  2. t_simulation
  3. t_heap_node
  4. t_heap
  5. t_dongle

t_coder contém todas as variáveis relevantes para os coders, sua thread (pthread_t), dongle direito e esquerdo, ponteiros para mutexes necessários e um ponteiro *sim para um t_simulation.

t_simulation contém todas as variáveis relevantes para a simulação no geral. Também tem uma thread (pthread_t), mas tem número de coders, os mutexes, que os coders apontam para, etc.

t_heap é a priority queue, contém *nodes (t_heap_nodes), que é um array de todos os nós, tamanho, capacidade, um counter para desempates e um ponteiro para uma função que recebe dois heap nodes e retorna um int. Essa função é importante porque é usada como regra principal de ordenação na priority queue.

t_heap_node é um nó na t_heap, um coder na fila. Contém um ponteiro para o coder que está na fila, prioridade e seq, que é um contador que é dado a cada node, coder que entra na fila, e incrementa a cada novo node. Seq serve de fallback em casos de empate de prioridade.

t_dongle representa o estado de um dongle. Contém id, condicional para saber se está ocupado ou livre, quanto tempo até que esteja livre novamente, um mutex (necessário para a cond) e um pthread_cond_t, que permite que uma thread espere até que uma condição seja verdadeira. Nesse caso, o cond é o que vai permitir a thread entrar em sleep e acordar quando o dongle desejado estiver livre. É uma variável perfeita para gerenciar filas de espera.

Caso esteja um pouco nebuloso, aqui estão os structs para referência:

typedef struct s_heap_node
{
	t_coder	*coder;
	long	priority;
	long	seq;
}	t_heap_node;

typedef int				(*t_heap_cmp)(t_heap_node *, t_heap_node *);

typedef struct s_heap
{
	t_heap_node	*nodes;
	int			size;
	int			capacity;
	long		seq;
	t_heap_cmp	cmp;
}	t_heap;

typedef struct s_dongle
{
	int				id;
	int				taken;
	long			available_after;
	pthread_mutex_t	mutex;
	pthread_cond_t	cond;
	t_heap			*wait_queue;
}	t_dongle;

typedef struct s_coder
{
	pthread_t		thread;
	int				id;
	int				is_compiling;
	int				compiling_score;
	long			last_compile;
	t_dongle		*r_dongle;
	t_dongle		*l_dongle;
	pthread_mutex_t	*dead_lock;
	pthread_mutex_t	*print_lock;
	t_simulation	*sim;
}	t_coder;

typedef struct s_simulation
{
	pthread_t		thread;
	int				nb_of_coders;
	long			start_time;
	int				dead_flag;
	pthread_mutex_t	dead_lock;
	pthread_mutex_t	print_lock;
	long			time_to_burnout;
	long			time_to_compile;
	long			time_to_debug;
	long			time_to_refactor;
	int				compiling_quota;
	long			dongle_cooldown;
	char			*scheduler;
	t_coder			*coders;
	t_dongle		*dongles;
}	t_simulation;

Inicializando os structs

A ideia principal que devemos manter ao inicializar os structs é estruturar o processo em pequenas etapas. Se uma etapa falhar, desfaça tudo que foi feito até ali. Isso é importante para evitarmos leaks de memória mais pra frente.
Então, faça uma coisa de cada vez e mantenha o código organizado. A última coisa que queremos é alocar memória num canto, esquecer e depois ter um leak por não ter desalocado. Leve como exemplo a inicialização dos mutexes que serão compartilhados entre todos os coders:

if (pthread_mutex_init(&sim->dead_lock, NULL) != 0)
	return (0);
if (pthread_mutex_init(&sim->compile_lock, NULL) != 0)
{
	pthread_mutex_destroy(&sim->dead_lock);
	return (0);
}
if (pthread_mutex_init(&sim->print_lock, NULL) != 0)
{
	pthread_mutex_destroy(&sim->dead_lock);
	pthread_mutex_destroy(&sim->compile_lock);
	return (0);
}

Se o segundo mutex falhar, destruimos o primeiro e retornamos. Se o terceiro falhar, destruimos todos os anteriores e retornamos. Siga essa lógica pra tudo daqui em diante.

Já que usei os mutexes de exemplo, vou aproveitar para explicar o porquê de cada um deles existir. O dead_lock protege a variável dead_flag, que serve pra sinalizar o fim da simulação.
O̶ ̶̶c̶o̶m̶p̶i̶l̶e̶_̶l̶o̶c̶k̶̶ ̶q̶u̶e̶ ̶p̶r̶o̶t̶e̶g̶e̶ ̶a̶s̶ ̶v̶a̶r̶i̶á̶v̶e̶i̶s̶ ̶̶c̶o̶m̶p̶i̶l̶i̶n̶g̶_̶s̶c̶o̶r̶e̶̶ ̶e̶ ̶̶l̶a̶s̶t̶_̶c̶o̶m̶p̶i̶l̶e̶̶ ̶d̶e̶ ̶c̶a̶d̶a̶ ̶c̶o̶d̶e̶r̶,̶ ̶q̶u̶e̶ ̶s̶ã̶o̶ ̶a̶l̶t̶e̶r̶a̶d̶a̶s̶ ̶m̶ú̶l̶t̶i̶p̶l̶a̶s̶ ̶v̶e̶z̶e̶s̶ ̶d̶u̶r̶a̶n̶t̶e̶ ̶a̶ ̶s̶i̶m̶u̶l̶a̶ç̶ã̶o̶.̶ ̶I̶s̶s̶o̶ ̶é̶ ̶i̶m̶p̶o̶r̶t̶a̶n̶t̶e̶ ̶p̶a̶r̶a̶ ̶q̶u̶e̶ ̶a̶ ̶̶M̶o̶n̶i̶t̶o̶r̶ ̶T̶h̶r̶e̶a̶d̶̶ ̶s̶e̶j̶a̶ ̶c̶a̶p̶a̶z̶ ̶d̶e̶ ̶m̶o̶n̶i̶t̶o̶r̶a̶r̶ ̶e̶s̶s̶a̶s̶ ̶v̶a̶r̶i̶á̶v̶e̶i̶s̶ ̶s̶e̶m̶ ̶c̶o̶r̶r̶e̶r̶ ̶r̶i̶s̶c̶o̶s̶ ̶d̶e̶ ̶l̶e̶r̶ ̶l̶i̶x̶o̶ ̶n̶e̶m̶ ̶v̶a̶l̶o̶r̶e̶s̶ ̶d̶a̶t̶a̶d̶o̶s̶.̶ (compile lock deixou de ser útil na minha implementação atual [[[GUIDE] Threads e Routines]
Já o print_lock é necessário para evitar que as mensagens de log (coder X fez tal coisa etc) não se sobreponham.

Sobre os arrays, como o array de dongles, é importante que o array inteiro seja alocado com um único malloc. Isso garante que um único free() consiga limpar todos os dongles, mantém as coisas organizadas.
Depois de mallocar o array, inicializamos cada dongle dentro dele (propriedades + locks) seguindo a mesma regra: se um mutex falhar, destruímos os anteriores antes de retornar.

O mesmo vale para o array de coders, exceto que temos que ser cuidadosos ao declarar o dongle direito e esquerdo de cada um para que a "mesa circular" deles faça sentido e os dongles sejam corretamente compartilhados entre eles.
É bom lembrar também que os coders tem ponteiros para os mutexes da t_simulation, sem isso perdemos o estado compartilhado dos mutexes e a consistência da simulação.

Caso precise de ajuda com a lógica de "mesa circular" dos coders, essa é a receita de bolo:

r_dongle = &dongles[i] // O dongle direito do coder i é o dongle i.
l_dongle = &dongles[(i + 1) % nb] // O dongle esquerdo do coder i é o dongle (i + 1) % nb. 

Aqui nb é o número total de coders, e consequentemente de dongles, e o módulo é importante para que quando o i se torne equivalente a nb ele retorne ao primeiro dongle.
Ou seja, se i=3 e nb=4, e queremos o dongle esquerdo do coder 3, então faremos 3 + 1 = 4. Acontece que o dongle 4 não existe, levando em consideração que a lista de dongles é indexada a zero. Então a essa altura já temos dongle[0] até dongle[3], totalizando 4.
É aqui que entra o % nb, 4 % 4 = 0, ou seja, o dongle a esquerda do coder 3 é o dongle[0]. Essa é uma estratégia básica de fazer com que o array volte ao começo após chegar no limite, impedindo que acesse memória não alocada.

Clone this wiki locally