Почему этот скрипт golang ставит меня в тупик? + несколько вопросов

avatar
hackerdudeeeeee
8 августа 2021 в 17:44
132
3
-4

Я получил этот код от кого-то на github, и я пытаюсь поиграть с ним, чтобы понять параллелизм.

package main

import (
    "bufio"
    "fmt"
    "os"
    "sync"
    "time"
)

var wg sync.WaitGroup

func sad(url string) string {
    fmt.Printf("gonna sleep a bit\n")
    time.Sleep(2 * time.Second)
    return url + " added stuff"
}

func main() {
    sc := bufio.NewScanner(os.Stdin)
    urls := make(chan string)
    results := make(chan string)

    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for url := range urls {
                n := sad(url)
                results <- n
            }
        }()
    }

    for sc.Scan() {
        url := sc.Text()
        urls <- url
    }

    for result := range results {
        fmt.Printf("%s arrived\n", result)
    }

    wg.Wait()
    close(urls)
    close(results)
}

У меня есть несколько вопросов:

  1. Почему этот код вызывает зависание?
  2. Как этот цикл for существует перед операцией приема ввода от пользователя. Подпрограммы go ждут, пока что-нибудь не будет передано в канал URL, а затем начинают выполнять работу? Я не понимаю этого, потому что это не последовательно, например, почему получение ввода от пользователя, а затем размещение каждого ввода в канале URL-адресов, а затем запуск подпрограмм go считается неправильным?
  3. Внутри цикла for у меня есть еще один цикл, который выполняет итерацию по каналу URL-адресов. Каждая процедура go обрабатывает ровно одну строку ввода? или одна рутина обрабатывает несколько строк одновременно? как все это работает?
  4. Правильно ли я собираю выходные данные?
Источник

Ответы (3)

avatar
Benny Jobigan
8 августа 2021 в 18:10
1

В основном вы все делаете правильно, но иногда что-то не так. Цикл for sc.Scan() будет продолжаться до тех пор, пока Сканер не завершит работу, а цикл for result := range results никогда не запустится, поэтому никакая подпрограмма go (в данном случае main) не сможет получить от results. При выполнении вашего примера я запустил цикл for result := range results перед for sc.Scan(), а также в его собственной процедуре go - иначе for sc.Scan() никогда не будет достигнут.

go func() {
    for result := range results {
        fmt.Printf("%s arrived\n", result)
    }
}()

for sc.Scan() {
    url := sc.Text()
    urls <- url
}

Кроме того, поскольку вы запускаете wg.Wait() до close(urls), основная горутина остается заблокированной в ожидании завершения 20 sad() go подпрограмм. Но они не могут закончить, пока не будет вызван close(urls). Поэтому просто закройте этот канал перед ожиданием группы ожидания.

close(urls)
wg.Wait()
close(results)
hackerdudeeeeee
8 августа 2021 в 18:27
0

я понял первую часть, но я не понимаю вторую, почему именно закрытие канала URL-адресов после неправильного ожидания?

Benny Jobigan
8 августа 2021 в 18:37
0

Каждая из ваших 20 горутин будет зацикливаться на for url := range urls, пока urls открыто. Пока urls не будет закрыто и цикл не завершится, эти горутины никогда не смогут достичь отложенного wg.Done(). Без всех вызовов wg.Done() основная горутина заблокируется на wg.Wait(). Все горутины ждут чего-то, что не может случиться — тупиковой ситуации. Закрыв urls перед wg.Wait(), вы гарантируете завершение 20 горутин. Мои предложения, конечно, лишь один из способов выполнить задачу.

avatar
mh-cbon
8 августа 2021 в 19:17
1
hackerdudeeeeee
8 августа 2021 в 22:26
1

Ваш ответ мне нравится еще больше! ты всегда приходишь и помогаешь мне здесь спасибо человек! я

avatar
Burak Serdar
8 августа 2021 в 17:52
0

Цикл for создает 20 горутин, все из которых ожидают ввода из канала urls. Когда кто-то записывает в этот канал, одна из горутин подхватывает его и продолжает работу. Это типичная реализация рабочего пула.

Затем сканер считывает ввод построчно и отправляет его в канал urls, где одна из горутин подхватывает его и записывает ответ в канал results. На данный момент нет других горутин, читающих из канала results, поэтому это заблокируется.

Поскольку сканер считывает URL-адреса, все остальные горутины подхватывают их и блокируют. Поэтому, если сканер прочитает более 20 URL-адресов, он заблокируется, потому что все горутины будут ожидать читателя.

.

Если URL-адресов меньше 20, цикл сканирования завершится, и будут прочитаны результаты. Однако это также в конечном итоге приведет к тупику, потому что цикл for завершится, когда канал будет закрыт, а закрыть канал будет некому.

Чтобы это исправить, сначала закройте канал urls сразу после окончания чтения. Это освободит все циклы for в горутинах. Затем вы должны поместить чтение цикла for из канала results в горутину, чтобы вы могли вызывать wg.Wait во время обработки результатов. После wg.Wait вы можете закрыть канал results.

Это не гарантирует, что все элементы в канале results будут прочитаны. Программа может завершиться до того, как все сообщения будут обработаны, поэтому используйте третий канал, который вы закрываете в конце горутины, которая читает из канала results. То есть:

done:=make(chan struct{})
go func() {
  defer close(done)
  for result := range results {
        fmt.Printf("%s arrived\n", result)
    }
}()
wg.Wait()
close(results)
<-done
hackerdudeeeeee
8 августа 2021 в 18:11
0

чтобы иметь дело с тем, что никто не читает из канала результатов, должен ли я переместить цикл for по каналу результатов выше цикла for подпрограмм go? я пробовал это, и он все еще заблокирован, как мне исправить этот код в основном?

hackerdudeeeeee
8 августа 2021 в 18:12
0

и как мне закрыть канал URL-адресов, если я получаю менее 20 URL-адресов?

hackerdudeeeeee
8 августа 2021 в 18:38
0

у меня просто есть 2 небольших вопроса: 1-что вы подразумеваете под «это освободит все циклы for в горутинах». ? 2-что означает это "<-done" в отдельной строке? я знаю, что когда стрелка слева, это означает, что вы получаете данные и помещаете их в какую-то переменную, что она здесь делает?

Burak Serdar
8 августа 2021 в 18:41
0

Чтение цикла for из канала прекращается только тогда, когда канал закрыт. Вот что я имею в виду под «выпуском горутин». <-done будет блокироваться, пока done не будет закрыто.

Benny Jobigan
8 августа 2021 в 18:44
0

Хорошее замечание об использовании готового канала для обеспечения чтения всех результатов - я не учел это в своем ответе.