-
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 *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;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->print_lock, NULL) != 0)
{
pthread_mutex_destroy(&sim->dead_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 + 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.
Nota
Existe um compile_lock no meu código, mas removi todas menções a ele nesse guia por ter perdido o sentido na minha implementação final. Fazia sentido no começo, mas depois deixou de fazer. Veja a página [GUIDE] Threads e Routines.