|
| 1 | +# 18 - Concorrência e Threads |
| 2 | + |
| 3 | +!!! pdf |
| 4 | +  |
| 5 | + |
| 6 | +<br> |
| 7 | + |
| 8 | +Nossa aula de hoje envolverá aprender a API `pthreads` para criação de threads e sincronização simples. |
| 9 | + |
| 10 | +## Criando tarefas e esperando elas acabarem |
| 11 | + |
| 12 | +O exemplo abaixo cria uma thread que roda a função `primeira_thread`, espera por seu fim e mostra a mensagem *Fim do programa*. |
| 13 | + |
| 14 | +```c |
| 15 | +// Funções rodadas em thread sempre tem essa assinatura |
| 16 | +void *minha_thread(void *arg) { |
| 17 | + printf("Hello thread!\n"); |
| 18 | + return NULL; |
| 19 | +} |
| 20 | + |
| 21 | +.... |
| 22 | +pthread_t tid; |
| 23 | +int error = pthread_create(&tid, NULL, minha_thread, NULL); |
| 24 | +pthread_join(tid, NULL); // espera tid acabar. |
| 25 | +``` |
| 26 | +
|
| 27 | +!!! example |
| 28 | + Compile o arquivo *exemplo1.c* com a flag especial `-pthread` e execute-o. |
| 29 | +
|
| 30 | +<div class="termy"> |
| 31 | +
|
| 32 | + ```console |
| 33 | + $ gcc exemplo1.c -o exemplo1 -pthread |
| 34 | + $ ./exemplo1 |
| 35 | + ``` |
| 36 | +
|
| 37 | +</div> |
| 38 | +
|
| 39 | +Vamos dissecar a chamada da função `pthread_create`: |
| 40 | +
|
| 41 | +```c |
| 42 | +int error = pthread_create( |
| 43 | + &tid, // variável para guardar ID da nova thread |
| 44 | + NULL, // opções de criação. NULL = opções padrão |
| 45 | + minha_thread, // função a ser executada |
| 46 | + NULL // parâmetro passado para a função acima |
| 47 | +); |
| 48 | +``` |
| 49 | + |
| 50 | +Toda thread que rodarmos terá a seguinte assinatura (mudando, é claro, o nome da função). |
| 51 | + |
| 52 | +```c |
| 53 | +void *minha_thread(void *arg); |
| 54 | +``` |
| 55 | +
|
| 56 | +Uma variável do tipo `void *` representa um endereço de memória cujo conteúdo é desconhecido. Ou seja, ele diz somente onde encontrar os dados, mas não diz o que está guardado na memória naquele lugar. Este tipo de variável é usada quando queremos passar blocos de memória entre funções mas não queremos fixar um tipo de dados. Veremos com mais detalhes como isto funciona na parte 2. |
| 57 | +
|
| 58 | +!!! example |
| 59 | + O manual contém entradas muito bem escritas de todas as chamadas de POSIX threads que usaremos. Abra as seguintes e se familiarize com seu conteúdo. |
| 60 | +
|
| 61 | +<div class="termy"> |
| 62 | +
|
| 63 | + ```console |
| 64 | + $ man 7 pthreads |
| 65 | + $ man 3 pthread_create |
| 66 | + $ man 3 pthread_join |
| 67 | + ``` |
| 68 | +
|
| 69 | +</div> |
| 70 | +
|
| 71 | +Assim como processos, threads são escalonadas pelo kernel. Isto significa que **não controlamos a ordem em que elas rodam** no nosso programa. Ou seja, ao executar `pthread_create` não sabemos se a thread principal (aquela que roda o `main`) continuará rodando ou se o controle passará instantaneamente para a nova thread. A primitiva de **sincronização** mais simples que dispomos é `pthread_join`, que garante que uma thread só prossegue quando outra acabar. |
| 72 | +
|
| 73 | +!!! exercise text short |
| 74 | + Retire o `pthread_join` do programa exemplo e o execute. Repita a execução várias vezes. Todas as vezes o resultado é o mesmo? O quê acontece? |
| 75 | +
|
| 76 | + !!! answer "Resposta" |
| 77 | + Não. Como a `main` pode chegar no `return 0`, então o processo pode acabar sem que a thread tenha sido devidamente executada. |
| 78 | +
|
| 79 | +!!! exercise text short |
| 80 | + É possível que duas threads chamem `pthread_join` na mesma thread destino? Consulte o manual para saber esta resposta. |
| 81 | +
|
| 82 | + !!! answer "Resposta" |
| 83 | + Quando uma thread termina, ela entra num estado chamado “*joinable*” (aguardando ser recolhida). A chamada da função `pthread_join()` faz duas coisas: |
| 84 | +
|
| 85 | + * Bloqueia a thread fez a chamada da função `pthread_join()` até outra thread terminar. |
| 86 | + * Desaloca os recursos da *thread* que está finalizando (stack, estruturas internas do sistema, etc.), evitando um *memory leak*. |
| 87 | +
|
| 88 | + Assim, só uma thread pode “recolher” (*join*) outra — se mais de uma tentar, não há uma regra definida sobre qual delas consegue, e isso gera comportamento indefinido. |
| 89 | + |
| 90 | + Para saber mais acesse a seção **DESCRIPTION** em `man 3 pthread_join`: *If multiple threads simultaneously try to join with the same thread...* |
| 91 | +
|
| 92 | +A resposta acima indica que precisaremos de outras primitivas de **sincronização** mais sofisticadas no futuro. Veremos isso nas próximas aulas. |
| 93 | +
|
| 94 | +!!! example |
| 95 | + Em um novo arquivo `.c`, crie quatro threads, cada uma executando uma função diferente que faz um print distinto. Compile e execute seu programa várias vezes. A saída será sempre a mesma, com os printfs sempre na mesma ordem? O que está acontecendo?! |
| 96 | +
|
| 97 | +## Passando argumentos para threads |
| 98 | +
|
| 99 | +Nossas threads ainda são muito limitadas: elas não recebem nenhum argumento nem devolvem resultados. Vamos consertar isso nesta seção. |
| 100 | +
|
| 101 | +Vimos na parte 1 que o último argumento de `pthread_create` é um ponteiro para os dados que nossa função deverá receber. Neste sequência de exercícios iremos aprender a usar este argumento para passar dados para nossas threads. |
| 102 | +
|
| 103 | +Nosso primeiro exercício será feito passo a passo. Siga cada um dos passos a risca e depois responda as questões. Vamos trabalhar a partir de um arquivo vazio. |
| 104 | +
|
| 105 | +!!! example |
| 106 | + Crie um programa simples com uma função `main` que aloca (usando malloc) um vetor `*vi` com 4 `int`s e um vetor `*tids` com 4 `pthread_t`s. |
| 107 | +
|
| 108 | +!!! example |
| 109 | + Adicione ao seu programa um `for` que cria 4 threads, use o vetor `*vi` para armazenar, em cada posição do vetor, os valores do índice do `for`, e no vetor `*tids` na na chamada da função `pthread_create`. Passe como último argumento da função `pthread_create` o endereço do elemento correspondente de `vi`. |
| 110 | +
|
| 111 | +!!! example |
| 112 | + Espere pelo fim desta thread. |
| 113 | +
|
| 114 | +!!! example |
| 115 | + Crie uma função `void *tarefa_print_i(void *arg)` que declara uma variável `int *i` e dá print em seu conteúdo. Inicialize a variável `i` como mostrado abaixo: |
| 116 | +
|
| 117 | + > `int *i = (int *) arg;` |
| 118 | +
|
| 119 | +!!! exercise text short |
| 120 | + Explique a utilização da variável `i` na tarefa acima. |
| 121 | +
|
| 122 | + !!! answer "Resposta" |
| 123 | + Apontadores `void *` contém somente o endereço do dado, mas sem indicar seu tipo. Ao declarar `i` acima dizemos que queremos interpretar aquele endereço como o endereço de um `int`. Assim, quando fazemos `*i` conseguimos acessar o inteiro presente no endereço passado para a thread. |
| 124 | +
|
| 125 | +Se seu programa estiver correto você deverá ver no terminal 4 prints com números de 0 a 3, cada um vindo de um thread. |
| 126 | +
|
| 127 | +!!! warning |
| 128 | + Se tiver problemas, valide seu código com algum colega que já tenha sido validado pelo professor. Se não tiver ninguém por perto já validado me chame ;) |
| 129 | +
|
| 130 | +!!! exercise text short |
| 131 | + Explique como é feita a passagem do argumento para a thread. |
| 132 | +
|
| 133 | + !!! answer "Resposta" |
| 134 | + A thread recebe o endereço da respectiva posição do array alocado dinâmicamente. |
| 135 | +
|
| 136 | +
|
| 137 | +!!! exercise text short |
| 138 | + Passamos para a thread um valor alocado dinamicamente. Por que isso é necessário? |
| 139 | +
|
| 140 | + !!! answer "Resposta" |
| 141 | + Vamos discutir depois! |
| 142 | +
|
| 143 | +
|
| 144 | +Vamos explorar a resposta da pergunta acima nos próximos exercícios. Para cada exercício, encontre seu problema, descreva-o usando suas próprias palavras e mostre um exemplo de saída possível. Somente depois de escrever sua resposta rode o programa. |
| 145 | +
|
| 146 | +!!! warning |
| 147 | + Cada exercício foca em um problema diferente. A resposta não é a mesma para ambas. |
| 148 | +
|
| 149 | +!!! exercise text medium |
| 150 | + Identifique um problema de escopo de dados no código abaixo (arquivo *parte2-1.c*) |
| 151 | +
|
| 152 | + Dica: compile, execute e leia o código para tentar entender o problema! |
| 153 | +
|
| 154 | + ```c |
| 155 | + void *minha_thread(void *arg) { |
| 156 | + int *i = (int *) arg; |
| 157 | + printf("Hello thread! %d\n", *i); |
| 158 | + } |
| 159 | +
|
| 160 | + // dentro do main |
| 161 | +
|
| 162 | + for (int i = 0; i < 4; i++) { |
| 163 | + pthread_create(&tid[i], NULL, minha_thread, &i); |
| 164 | + } |
| 165 | + ``` |
| 166 | +
|
| 167 | + !!! answer "Resposta" |
| 168 | + Com threads, não tenho garantir da ordem de escalonamento (não qual thread o sistema operacional vai escolher para execução, nem em qual ordem). Assim, a thread da `main` altera o valor da variável `i` e quando cada thread executa, o valor recuperado é diferente do esperado. |
| 169 | +
|
| 170 | +!!! exercise text medium |
| 171 | + Identifique um problema de escopo de dados no código abaixo (arquivo *parte2-2.c*) |
| 172 | +
|
| 173 | + Dica: compile, execute e leia o código para tentar entender o problema! |
| 174 | +
|
| 175 | + ```c |
| 176 | + void *minha_thread(void *arg) { |
| 177 | + int *i = (int *) arg; |
| 178 | + printf("Hello thread! %d\n", *i); |
| 179 | + } |
| 180 | +
|
| 181 | + pthread_t *criar_threads(int n) { |
| 182 | + pthread_t *tids = malloc(sizeof(pthread_t) * n); |
| 183 | +
|
| 184 | + for (int i = 0; i < n; i++) { |
| 185 | + pthread_create(&tids[i], NULL, minha_thread, &i); |
| 186 | + } |
| 187 | +
|
| 188 | + return tids; |
| 189 | + } |
| 190 | +
|
| 191 | + // dentro do main |
| 192 | + pthread_t *tids = criar_threads(4); |
| 193 | +
|
| 194 | + ``` |
| 195 | +
|
| 196 | + !!! answer "Resposta" |
| 197 | + Lembra da pilha ou `stack`? Veja nos slides, cada thread tem o seu próprio espaço para guardar suas variáveis locais. Quando uma função é finalizada, o espaço alocado a ela na `stack` pode ser reutilizado. Neste exemplo, cada thread da `minha_thread` tenta ler uma variável local `i` criada no `for` da função `criar_threads`, que pode não "existir" mais quando a função `minha_thread` é executada. |
| 198 | +
|
| 199 | +
|
| 200 | +!!! warning |
| 201 | + Valide sua solução com o professor para garantir que realmente entendeu! |
| 202 | +
|
| 203 | +Agora que já entendemos como passar um argumento e que devemos sempre colocá-lo no *heap*, passar vários é muito simples: alocamos um `struct` com todos os dados que queremos enviar e passamos seu endereço no último argumento. Ao recebê-lo, a função faz um *cast* de `void *` para um ponteiro para o `struct`. |
| 204 | +
|
| 205 | +!!! example |
| 206 | + Modifique seu exercício do começo desta parte para receber dois argumentos do tipo inteiro e imprimir ambos valores. |
| 207 | +
|
| 208 | +
|
| 209 | +## Retornando valores |
| 210 | +
|
| 211 | +Na prática, ao passar `struct`s para threads como argumentos já sabemos como retornar valores: basta adicionar um campo que própria thread deve preencher com o resultado de sua execução. Isso é equivalente a criar uma função que retorna valores em variáveis passadas por referência (ou seja, escrevendo em variáveis passadas como ponteiros). |
| 212 | +
|
| 213 | +!!! example |
| 214 | + Modifique seu exercício da parte anterior para que as threads retornem a multiplicação dos dois inteiros passados. Faça o print deste valor no `main`. |
| 215 | +
|
0 commit comments