Channels por baixo dos panos
Mateus Vinícius
Posted on November 25, 2023
Channels são canais de comunicação e sincronização entre goroutines. A comunicação é feita através do envio de dados entre goroutines e a sincronização é feita pausando e retomando o processamento de uma goroutine quando necessário.
Podemos criar uma nova channel, com o seguinte código:
ch := make(chan Task, 3)
O tipo chan Task
significa que estamos criando uma channel que envia e recebe um dado de tipo Task, enquanto o parâmetro 3
representa a quantidade de dados que a channel pode guardar ao mesmo tempo na sua fila, iremos explorar o que isso significa muito em breve.
Por debaixo dos panos o valor da variável ch
passa a ser um ponteiro para uma estrutura de dados chamada hchan
, que é nada mais do que uma struct com 4 propriedades principais: buffer, sendx, recvx e lock. Cada propriedade possui um papel fundamental no funcionamento das channels.
O buffer é uma fila que guarda cópias dos dados enviados para a channel. Quando uma goroutine envia um dado para uma channel, o buffer armazena e mantém um cópia desse dado - chamamos esse processo de enqueue - e, quando uma goroutine recebe esse dado, ele é removido do buffer e uma cópia fica com a goroutine que o consome - esse processo chamamos de dequeue.
Sendx representa o índice do dado sendo enviado para a fila. No nosso exemplo acima de uma fila com 3 elementos, se uma goroutine envia o primeiro dado para a channel o sendx passa a ser 1, se envia mais outro o sendx passa a ser 2, se envia mais outro o sendx passa a ser...0. Essa é a definição de uma fila circular, pois o último elemento se conecta ao primeiro. O valor do sendx seria zero porque o próximo elemento depois do terceiro é o primeiro, portanto se a fila está cheia - já possui 3 elementos - e eventualmente o primeiro elemento da fila for removido, quando uma goroutine enviar para a channel um novo dado, ele ficará guardado no índice 1 - e ai o valor do sendx volta a ser 1 - e assim sucessivamente, de forma circular.
Recvx funciona da mesma forma que a propriedade sendx, mas é referente ao índice do dado sendo recebido por uma goroutine.
Lock consiste em um mutex, que é um tipo primitivo de dado usado para evitar race condition - ou seja, evitar que diferentes threads manipulem o mesmo dado compartilhado, no caso a channel, de foma simultânea, causando comportamentos inesperados ou corrupção de dados. Se duas goroutines tentarem fazem operações simultâneas na mesma channel apenas uma conseguirá o lock, e a outra deverá aguardar o unlock da primeira para realizar sua própria operação.
Podemos visualizar o fluxo de funcionamento de uma channel enquanto fila circular com o diagram criado por @akankshadokania.
Um ponto importante que pode ser levantado é: Já que a fila circular possui um tamanho fixo de quantidade de elementos que podem ser salvos ao mesmo tempo, o que acontece se uma goroutine enviar mais um dado para a channel que já está cheia?
Usando nossa channel de 3 elementos como exemplo, imaginando que ele já possui 3 Tasks no buffer, o sendx e recvx seriam ambos 0 e o runtime do Go, sabendo que o índice 0 do buffer ainda está ocupado, bloquearia a execução da goroutine que tentou enviar o dado para a channel e apenas desbloquearia quando o recvx passasse a ser 1, ou seja, quando o primeiro elemento fosse removido da fila, assim permitindo o envio do dado para a channel.
Portanto channels são uma ferramenta poderosa por possuir essas características interessantes: São goroutine-safe, ou seja, possuem a capacidade de lidar com múltiplas goroutines sem levar a problemas de race condition; Possuem capacidade de salvar dados, como vimos no ponto sobre a propriedade buffer; Possui semântica FIFO - First In First Out -, ou seja, a fila do buffer recebe e envia os dados em ordem; Podem enviar dados entre goroutines e podem bloquear/desbloquear a execução de goroutines, dependendo da situação.
Posted on November 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.