Desde que comecei a programar, acho bastante útil desenhar pequenos diagramas para me ajudar a entender os programas. Eu sou conhecida por desenhar, por exemplo, fluxogramas simples antigos para o divertimento de outras pessoas. Nos meus dias de orientação para o objeto, eu reuni alguns diagramas de hierarquia de objeto, porque eles pareciam uma base crucial para a elaboração de programas baseados neles mesmo. Felizmente, nenhum deles está na pilha de papel rabiscado na minha mesa atual (ou pregados na minha parede!).
Programar em Go (um tipo de diagrama que eu achei bem útil), no entanto, mostra a comunicação entre goroutines. Eu insiro o nome da goroutine em uma caixa e desenho uma linha vertical em frente a ela – vagamente sugerindo um processo sequencial. Eu represento os canais com as linhas horizontais, que vão de uma linha vertical até a outra.
Então, eu anoto no diagrama tudo o que acredito ser relevante. Eu acho que lápis e papel são sempre as melhores opções para se fazer isso. Todo vez que eu tentei usar um software de desenho, ou de diagramação, eu perdi muito tempo e no final não fiquei feliz com o resultado. Para mostrar exemplos aqui, vou recorrer a arte do ASCII. Para a amostra 01, aqui está o diagrama da solução de Andrew Gerrand, do exercício 62 do A Tour of Go.
one per url to be fetched!
+---------------+
+------+ url (url string) +---------------+|
| main |--------------------------------------->| crawler.fetch |+
+------+ +---------------+
| n = workers |
| +-------------+ |
| +-------------+| |
| | crawler.run |+ |
| +-------------+ |
| | c.req string c.req |
| |-range-----------------------<-|
| | |
| c.res *result c.res |
|-range-----------------------<-|
| |
Note que, enquanto as linhas que representam os canais vão de uma linha vertical para outra, existe uma linha que sai da caixa principal para a caixa crawler.fetch. Isso não representa um canal, mas, sim, os dados passados como um parâmetro na invocação do goroutine. Sem isso, não fica óbvio onde o crawler.fetch consegue seus dados. Eu coloquei uma anotação sobre esse dado em um parêntesis remanescente da sintaxe da chamada da função.
Quando existem instâncias múltiplas de um goroutine, eu uso uma caixa sombreada e coloco uma anotação a respeito dela, dizendo quantas instâncias foram criadas ou quantas estão rodando. A principal anotação para um canal é seu tipo, que eu sempre coloco acima da linha, centralizada. A comunicação de canal geralmente segue em uma direção, que é mostrada com uma set na linha do canal.
Se você puder organizar as goroutines de forma que a comunicação no diagrama seja da direta para a esquerda, então, as setas se parecem com operações de envio e recebimento de comunicação. Eu achei útil anotar ambos os destinos das linhas dos canais com o nome do canal naquele goroutine. Neste caso, ambos os fins usam o mesmo nome da variável, então, pode parecer um pouco redundante, mas acho que é uma informação útil.
Esta é a solução do Rog:
one per url fetched!
+-----------+
+------+ {url string} +-----------+|
| main |--------------------------------------->| "fetcher" |+
+------+ +-----------+
| |
| limiter <- struct{}, 10 <-limiter |
|------------------------>>>>>------------------------------|
| |
| |
| rc result r |
|-range---------------------------------------------------<-|
O fetcher da goroutine aqui parece ser uma função literal anônima. Eu usei “fetcher” entre aspas para mostrar que fetcher não é de fato o nome da função no código fonte, mas apenas o tipo do nome descritivo usado no diagrama. Além disso, os dados fornecidos do goroutine principal não são passados como parâmetro, mas como uma variável livre para o fechamento, então, eu usei colchetes.
O canal limiter mostra um problema para o meu esquema de diagramação, porque os dados vão da esquerda para a direita. Setas na linha do canal sugerem que a sintaxe do mesmo não funciona, já que elas estariam indo para a direção errada. Na verdade, eu costumava desenhá-las na direção errada, mas ao me acostumar ao ler o código do Go, eu achei isso perturbador. Aqui estou experimentando apenas colocar os operadores do canal em sua relação correta com os nomes do próprio canal. E colocquei, também, uma seta em negrito sob o tipo do canal.
Então, como a abordagem do Rog se compara com a do Andrew a partir desta perspectiva? De imediato, ela parece melhor porque ela usa menos goroutines. A limitação de posicionamento é feita ao colocar tokens em um canal carregado e as goroutines “trabalhadoras” do Andrew não são necessárias. Se você olhar para a quantidade de comunicação de canais que está acontecendo, no entanto, é mais ou menos a mesma coisa. Em cada caso existem duas comunicações de canal por url buscada. Na verdade, não tenho certeza se podemos fazer algo melhor por um algoritmo de posicionamento limitado.
Agora, poderíamos fazer pior? Sempre! Os diagramas nem sempre são necessários para notar a diferença entre a quantidade de comunicação acontecendo no canal. Estou pedindo a todas as minhas gorountines atuais para fazerem um monte de coisas ao mesmo tempo. Coisas essas que poderiam ser evitadas com um algoritmo diferente.
Uma recomendação chocante apareceu no Go Nuts recentemente: “evitar a simultaneidade ao máximo”. Esta heresia foi imediatamente desafiada. A explicação é que você usa a simultaneidade quando você tem que usar, mas somente quando você tem que usar. Sincronização é algo caro, logo, se você não tem que compartilhar algo, não o faça! Se existe uma maneira de evitar o compartilhamento, evite.
De volta para os dois diagramas mostrados acima, uma coisa que me incomoda. Existe uma goroutine criada para cada url individual. Isso é realmente necessário? Olhando para todas as soluções apresentar no Go Nuts thread, todos eles fazem isso. As goroutines são bastante simplistas, é verdade, mas eu ainda acredito que existe um stack de 4k, e vai saber o que mais ele precisa para ser iniciado.
Existe uma solução com um diagrama assim? Ele ainda teria dois canais de comunicação por url, mas não gourotines separadas por url – apenas a principal e um pequeno número constante de gourotines “workers”.
+------+ n = workers
| main | +----------+
+------+ +----------+|
| | "worker" |
| +----------+
| |
| reqCh <- *request <-reqCh |
|------------------------->>>>>-----------------------------|
| |
| |
| resCh *result resCh |
|<--------------------------------------------------------<-|
| |
Sim, existe solução. Aqui está um exemplo:
type request struct {
url string
depth int
}
type result struct {
req *request
body string
urls []string
err os.Error
}
func Crawl(url string, depth int, f Fetcher, workers int) {
reqCh := make(chan *request)
resCh := make(chan *result)
for i := 0; i < workers; i++ {
go func() {
for r := range reqCh {
body, urls, err := f.Fetch(r.url)
resCh <- &result{r, body, urls, err}
}
}()
}
seen := map[string]bool{url: true}
backlog := []*request{&request{url, depth}}
outstanding := 1
for outstanding > 0 {
var res *result
for len(backlog) > 0 {
// feed backlog to reqCh until a result arrives.
select {
case reqCh <- backlog[len(backlog)-1]:
backlog = backlog[:len(backlog)-1]
case res = <-resCh:
goto gotResult
}
}
res = <-resCh // no backlog. block until result arrives.
gotResult:
outstanding--
if res.err != nil {
fmt.Println(res.err)
} else {
fmt.Printf("found: %s %q\n", res.req.url, res.body)
}
if res.req.depth > 1 {
for _, u := range res.urls {
if !seen[u] {
seen[u] = true
outstanding++
backlog = append(backlog,
&request{u, res.req.depth - 1})
}
}
}
}
}
Toda a mágica está em usar uma homologação selecionada. Precisamos da principal para lidar com os dados em ambas direções, dependendo de quais dados estão disponíveis. É exatamente isso que o select faz.
Esses diagramas de comunicação não descrevem algoritmos. Eles não os sugerem também. No entanto, eles te dão uma sinopse de goroutines e canais de comunicação e isso pode te levar a considerar alternativas. Tudo no diagrama é necessário? Você consegue resolver o problema usando menos?
***
Texto original disponível em: http://soniacodes.wordpress.com/2011/10/14/communication-diagrams/








