Skip to content

[GUIDE] Structs

Lunna Boo edited this page Aug 2, 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 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. Caso o programa esteja rodando em FIFO, a função que o ponteiro aponta é a função que compara em FIFO, se não, será a função que compara em EDF. Basicamente, é a função que decide quem vai primeiro e quem vai por último.

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	*compile_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	compile_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 compile_lock que protege as variáveis compiling_score e last_compile de cada coder, que são alteradas múltiplas vezes durante a simulação. Isso é importante para que a Monitor Thread seja capaz de monitorar essas variáveis sem correr riscos de ler lixo nem valores datados.
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 + mutexes) 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