-
Notifications
You must be signed in to change notification settings - Fork 0
[GUIDE] Structs
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:
- t_coder
- t_simulation
- t_heap_node
- t_heap
- 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;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.