
Provavelmente, você já ouviu falar sobre a linguagem de programação Go, sua popularidade está em constante crescimento, o que é bastante razoável. Esse idioma é simples, rápido e conta com uma ótima comunidade. Um dos aspectos mais curiosos da linguagem é o modelo de programação multithread. As primitivas subjacentes permitem criar programas multithread com facilidade e simplicidade. Este artigo é destinado a quem deseja aprender essas primitivas: goroutines e canais. E, através das ilustrações, mostrarei como trabalhar com elas. Espero que seja uma boa ajuda para você em seus estudos futuros.
Programas únicos e multithread
Você provavelmente já escreveu programas de thread único. Normalmente, é assim: existe um conjunto de funções para executar várias tarefas, cada função é chamada apenas quando a anterior preparou os dados para ela. Assim, o programa é executado seqüencialmente.
Esse será o nosso primeiro exemplo - o programa de mineração de minério. Nossas funções buscarão, extrair e processar minério. O minério na mina em nosso exemplo é representado por listas de strings, funções as tomam como parâmetros e retornam uma lista de strings "processados". Para um programa de thread único, nosso aplicativo será projetado da seguinte maneira:

Neste exemplo, todo o trabalho é realizado por um thread (Gopher de Gary). Três funções principais: pesquisa, produção e processamento são executadas seqüencialmente uma após a outra.
func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} foundOre := finder(theMine) minedOre := miner(foundOre) smelter(minedOre) }
Se imprimirmos o resultado de cada função, obtemos o seguinte:
From Finder: [ore ore ore] From Miner: [minedOre minedOre minedOre] From Smelter: [smeltedOre smeltedOre smeltedOre]
O design e a implementação simples são uma vantagem de uma abordagem de thread único. Mas e se você quiser executar e executar funções independentemente uma da outra? Aqui, a programação multithread ajuda você.

Essa abordagem para a mineração de minério é muito mais eficiente. Agora, vários threads (esquilos) trabalham de forma independente, e Gary faz apenas parte do trabalho. Um esquilo procura minério, o outro produz e o terceiro derrete, e tudo isso é potencialmente simultâneo. Para implementar essa abordagem, precisamos de duas coisas no código: criar processadores Gopher independentemente um do outro e transferir minério entre eles. Go tem goroutines e canais para isso.
Gorutins
As goroutines podem ser consideradas como "threads leves". Para criar goroutines, basta colocar a palavra-chave
go antes do código de chamada da função. Para demonstrar como é simples, vamos criar duas funções de pesquisa, chamá-las com a palavra-chave
go e imprimir uma mensagem toda vez que encontrarem o “minério” na mina.

func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} go finder1(theMine) go finder2(theMine) <-time.After(time.Second * 5)
A saída do nosso programa será a seguinte:
Finder 1 found ore! Finder 2 found ore! Finder 1 found ore! Finder 1 found ore! Finder 2 found ore! Finder 2 found ore!
Como você pode ver, não há ordem em que a função “encontre minério” primeiro; As funções de pesquisa funcionam simultaneamente. Se você executar o exemplo várias vezes, a ordem será diferente. Agora podemos executar programas multithread (multi-sphere), e este é um progresso sério. Mas o que fazer quando precisamos estabelecer uma conexão entre goroutines independentes? Está chegando a hora da magia dos canais.
Canais

Canais permitem que goroutines troquem dados. Este é um tipo de tubo através do qual as goroutins podem enviar e receber informações de outras goroutines.

A leitura e gravação no canal são realizadas usando o operador de seta (<-), que indica a direção do movimento dos dados.

myFirstChannel := make(chan string) myFirstChannel <- "hello"
Agora nosso escoteiro não precisa acumular minério, ele pode transferi-lo imediatamente usando os canais.

Atualizei o exemplo, agora o código do localizador e do minerador é de funções anônimas. Não se preocupe muito se você não os encontrou antes, mas lembre-se de que cada um deles é chamado com a palavra-chave
go , portanto, ela será executada em sua própria goroutine. O mais importante aqui é que as goroutines transmitem dados entre si usando o canal
oreChan . E vamos lidar com funções anônimas mais perto do fim.
func main() { theMine := [5]string{“ore1”, “ore2”, “ore3”} oreChan := make(chan string)
A conclusão abaixo demonstra claramente que nosso mineiro recebe três vezes do minério de canal uma porção de cada vez.
Miner: Received ore1 from finder Miner: Received ore2 from finder Miner: Received ore3 from finder
Portanto, agora podemos transferir dados entre diferentes goroutines (Gopher), mas antes de começarmos a escrever um programa complexo, vamos examinar algumas propriedades importantes dos canais.
Fechaduras
Em algumas situações, ao trabalhar com canais, a goroutin pode estar bloqueada. Isso é necessário para que as goroutines possam sincronizar-se umas com as outras antes de começar ou continuar trabalhando.
Bloqueio de gravação

Quando a goroutine (gopher) envia dados para um canal, ela é bloqueada até que outra goroutine leia os dados do canal.
Bloqueio de leitura

Semelhante ao bloqueio ao gravar em um canal, a goroutin pode ser bloqueada durante a leitura de um canal até que nada seja gravado nele.
Se as fechaduras, à primeira vista, parecerem complicadas para você, você pode imaginá-las como uma "transferência de dinheiro" entre duas goroutinas (esquilos). Quando um esquilo quer transferir ou receber dinheiro, ele precisa aguardar o segundo participante da transação.
Depois de lidar com os bloqueios de goroutina nos canais, vamos discutir dois tipos diferentes de canais: com buffer e sem buffer. Escolhendo esse ou aquele tipo, determinamos amplamente o comportamento do programa.
Canais sem buffer

Em todos os exemplos anteriores, usamos apenas esses canais. Nesses canais, apenas um dado pode ser transmitido por vez (com bloqueio, como descrito acima).
Canais tamponados

Os fluxos em um programa nem sempre podem ser perfeitamente sincronizados. Suponha, no nosso exemplo, que um gopher-scout encontrou três partes de minério, enquanto um gopher-mineiro conseguiu extrair apenas uma parte das reservas encontradas ao mesmo tempo. Aqui, para que o reconhecimento do esquilo não passe a maior parte do tempo esperando o mineiro terminar seu trabalho, usaremos canais em buffer. Vamos começar criando um canal com capacidade para 3.
bufferedChan := make(chan string, 3)
Podemos enviar vários dados para o canal em buffer, sem a necessidade de lê-los com outra goroutine. Essa é a principal diferença em relação aos canais sem buffer.

bufferedChan := make(chan string, 3) go func() { bufferedChan <- "first" fmt.Println("Sent 1st") bufferedChan <- "second" fmt.Println("Sent 2nd") bufferedChan <- "third" fmt.Println("Sent 3rd") }() <-time.After(time.Second * 1) go func() { firstRead := <- bufferedChan fmt.Println("Receiving..") fmt.Println(firstRead) secondRead := <- bufferedChan fmt.Println(secondRead) thirdRead := <- bufferedChan fmt.Println(thirdRead) }()
A ordem de saída nesse programa será a seguinte:
Sent 1st Sent 2nd Sent 3rd Receiving.. first second third
Para evitar complicações desnecessárias, não usaremos canais em buffer em nosso programa. Mas é importante lembrar que esses tipos de canais também estão disponíveis para uso.
Também é importante observar que os canais em buffer nem sempre o impedem de bloquear. Por exemplo, se um gopher scout é dez vezes mais rápido que um gopher miner, e eles são conectados através de um canal em buffer com capacidade de 2, então o gopher scout será bloqueado toda vez que for enviado, se já houver duas partes de dados no canal.
Juntando tudo
Assim, armados com goroutines e canais, podemos escrever um programa usando todas as vantagens da programação multithread no Go.

theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string)
Esse programa produzirá o seguinte:
From Finder: ore From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted
Comparado ao nosso primeiro exemplo, essa é uma grande melhoria, agora todas as funções são executadas independentemente, cada uma em sua própria goroutine. E também recebemos um transportador de canais, através do qual o minério é transferido imediatamente após o processamento. Para manter o foco em um entendimento básico da operação de canais e goroutines, omiti alguns pontos, o que pode levar a dificuldades no lançamento do programa. Concluindo, quero me debruçar sobre esses recursos da linguagem, pois eles ajudam no trabalho com goroutines e canais.
Gorutins anônimos

Assim como executamos uma função regular na goroutine, podemos declarar uma função anônima imediatamente após a palavra-chave
go e chamá-la usando a seguinte sintaxe:
Assim, se precisarmos chamar uma função em apenas um lugar, podemos executá-la em uma goroutine separada sem nos preocuparmos com sua declaração prévia.
A principal função é goroutine.

Sim, a função
principal funciona em sua própria goroutine. E, mais importante, após sua conclusão, todas as outras goroutines também terminam. É por esse motivo que efetuamos uma chamada temporizada no final de nossa função
principal . Essa chamada cria um canal e envia dados para ele após 5 segundos.
<-time.After(time.Second * 5)
Lembre-se de que a goroutine será bloqueada durante a leitura do canal até que algo seja enviado a ele? É exatamente o que acontece quando o código especificado é adicionado. A goroutine principal será bloqueada, dando às outras goroutias 5 segundos de tempo para trabalhar. Este método funciona bem, mas geralmente é usada uma abordagem diferente para verificar se todas as goroutines concluíram seu trabalho. Para transmitir um sinal sobre a conclusão do trabalho, um canal especial é criado, a goroutine principal é impedida de ler a partir dele e, assim que a goroutine filha conclui seu trabalho, ela grava nesse canal; A goroutine principal é desbloqueada e o programa termina.

func main() { doneChan := make(chan string) go func() {
Leia de um tubo em um loop de alcance
Em nosso exemplo, na função do goffer-getter, usamos o loop
for para selecionar três elementos do canal. Mas o que fazer se não se sabe com antecedência quantos dados podem existir no canal? Nesses casos, você pode usar o canal como argumento para o loop
for-range , assim como nas coleções. A função atualizada pode ser assim:
Assim, o minerador lerá tudo o que o escoteiro lhe enviar, o uso do canal no ciclo garantirá isso. Observe que após todos os dados do canal terem sido processados, o ciclo será bloqueado na leitura; para evitar o bloqueio, você precisa fechar o canal chamando
close (channel) .
Leitura de canal sem bloqueio
Usando a construção de
caso de seleção , o bloqueio de leituras do tubo pode ser evitado. A seguir, é apresentado um exemplo do uso dessa construção: goroutine lerá os dados do canal, se houver, caso contrário, o bloco
padrão será executado:
myChan := make(chan string) go func(){ myChan <- “Message!” }() select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) } <-time.After(time.Second * 1) select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) }
Uma vez iniciado, esse código produzirá o seguinte:
No Msg Message!
Gravação de canal sem bloqueio
Bloqueios ao gravar em um canal podem ser evitados usando a mesma construção de
caso de seleção . Vamos fazer uma pequena edição no exemplo anterior:
select { case myChan <- “message”: fmt.Println(“sent the message”) default: fmt.Println(“no message sent”) }
O que estudar mais

Há um grande número de artigos e relatórios que cobrem o trabalho com canais e goroutines com muito mais detalhes. E agora, com o código, você tem uma idéia clara do porquê e como essas ferramentas são usadas, você pode tirar o máximo proveito dos seguintes materiais:
Obrigado por reservar um tempo para ler. Espero ter ajudado você a entender os canais, goroutines e os benefícios que os programas multithread oferecem.