горутина внутри бесконечного цикла for. Это хорошая практика?

Итак, я работаю над частью кода:

// Main
for {
    c := make(chan string)
    data := make(map[string]string)
    go doStuff(data,c)
    fmt.Println(<-c)

    time.Sleep(2*time.Second)
}

// doStuff
func doStuff(d map[string]string,ch chan string){
    defer close(ch)
    //Code to make changes to passed data
    ch <-"changes made"
}

Что это делает, так это то, что он передает карту и канал горутине, внутри которой вносятся некоторые изменения в карту, и она отправляет сообщение, а в основном она печатает и ждет другого сообщения об изменении, и это продолжается с интервалом 2 секунды до прерывания клавиатуры или некоторой логики после обработки переданных данных горутине.

Я где-то чувствую, что это не эффективный способ сделать это. Итак, мой вопрос заключается в том, можно ли помещать горутину в бесконечный цикл for или есть ли более эффективный способ сделать это?


person DarthCucumber    schedule 30.10.2020    source источник
comment
Если вам нужно дождаться вывода горутины сразу после ее запуска, зачем вообще запускать горутину?   -  person Burak Serdar    schedule 31.10.2020
comment
Нет ничего плохого в бесконечном цикле как таковом. Я часто использую конструкцию for { ... }, когда условие выхода из цикла требует слишком много команд, чтобы их можно было легко поместить в условное выражение for. Менее уместна идея создания канала в цикле, который содержит только одно значение. Если ваша горутина всегда возвращает только одно значение, наличие этого возвращаемого значения является достаточным признаком того, что горутина выполнена. По возможности повторно используйте каналы и не закрывайте их, если вы хотите отправить больше данных позже. Очевидно, что в вашем случае горутина все равно бесполезна, и только в образовательных целях   -  person Daniel Farrell    schedule 31.10.2020
comment
@DanielFarrell спасибо, что ответили на мой вопрос. Можете ли вы написать ответ, чтобы я мог отметить его правильно?   -  person DarthCucumber    schedule 31.10.2020
comment
Я бы хотел. Я также добавлю дополнительную вспомогательную информацию, так как у меня будет больше места.   -  person Daniel Farrell    schedule 31.10.2020


Ответы (1)


Нет ничего плохого в бесконечном цикле как таковом. Я часто использую конструкцию for { ... }, когда условие выхода из цикла требует слишком много команд, чтобы их можно было легко поместить в условие for.

Основываясь на моем каталоге $GOPATH/src/github.com/, который, очевидно, является довольно неполным набором образцов, я вижу сотни таких применений помимо моего собственного. Только github.com/docker/docker использует 454 таких бесконечных цикла.

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

Очевидно, что в вашем случае горутина в любом случае бессмысленна и предназначена только для образовательных целей. Но подумайте об этом, если вы так склонны:

package main

import (
  "log"
)

func doStuff(datachan <-chan map[string]string, reschan chan<- int) {
  for {
    data, ok := <-datachan
    if !ok {
      log.Print("Channel closed.")
      break
    }
    log.Printf("Data had %d length: %+v", len(data), data)
    reschan<-len(data)
  }
  return
}

const workers = 3

func main() {
  var datachan = make(chan map[string]string)
  var reschan = make(chan int)
  var inflight = 0
  var inputs = []map[string]string {
    map[string]string{ "hi": "world" },
    map[string]string{ "bye": "space", "including": "moon" },
    map[string]string{ "bye": "space", "including": "moon" },
    map[string]string{ },
    map[string]string{ },
  }
  // an inline funciton definition can change inflight within main()'s scope
  processResults := func (res int) {
    log.Printf("Main function got result %d", res)
    inflight--
  }
  // start some workers
  for i := 0; i < workers; i++{
    go doStuff(datachan, reschan)
  }
  for _, data := range inputs {
      //Select allows reading from reschan if datachan is not available for
      // writing, thus freeing up a worker to read from datachan next loop
      written := false
      for written  != true {
        select {
          case res := <-reschan:
            processResults(res)
          case datachan <- data:
            inflight++
            written = true
        }
      }
  }
  close(datachan)
  for inflight > 0 {
    processResults(<-reschan)
  }
}

Вывод:

2020/10/31 13:15:08 Data had 1 length: map[hi:world]
2020/10/31 13:15:08 Main function got result 1
2020/10/31 13:15:08 Data had 0 length: map[]
2020/10/31 13:15:08 Main function got result 0
2020/10/31 13:15:08 Data had 0 length: map[]
2020/10/31 13:15:08 Channel closed.
2020/10/31 13:15:08 Main function got result 0
2020/10/31 13:15:08 Data had 2 length: map[bye:space including:moon]
2020/10/31 13:15:08 Channel closed.
2020/10/31 13:15:08 Main function got result 2
2020/10/31 13:15:08 Data had 2 length: map[bye:space including:moon]
2020/10/31 13:15:08 Channel closed.
2020/10/31 13:15:08 Main function got result 2

Здесь я добавляю немного больше структуры, чтобы проиллюстрировать некоторые более распространенные варианты использования for { и close(chan).

Я использую потенциально бесконечный цикл в горутинах worker, которых 3 (преднамеренно создано больше, чем используется). Я считаю, сколько раз я пишу на канал, чтобы убедиться, что я прочитал каждый ответ. Когда основная горутина заканчивается, все остальные горутины бесцеремонно убиваются, поэтому я должен убедиться, что я позволил им завершиться. Подсчет результатов — один из простых способов сделать это.

Я также демонстрирую правильное использование close(chan). Хотя закрытие канала после использования, как это сделали вы, не является неправильным, обычно в этом нет необходимости, так как открытые каналы будут удалены сборщиком мусора после того, как все ссылки на них исчезнут. (https://stackoverflow.com/questions/8593645/is-it-ok-to-leave-a-channel-open#:%7E:text=It%27s%20OK%20to%20leave%20a,закоторымследует%20нет%20более%20данные%20.)

close(chan) обычно используется, чтобы сообщить читателям канала, что на канале больше нет данных.

    data, ok := <-datachan

Второе значение, логическое, говорит нам, прочитали ли мы data или канал был фактически закрыт и опорожнен. Так что это часть приемника, чтобы убедиться, что мы обработали весь канал.

Поскольку я использую select, этот код может обрабатывать inputs произвольной длины со статическим набором рабочих процессов. Ни один из этих каналов не буферизован — читатель должен читать, чтобы писатель мог писать. Поэтому мне нужно убедиться, что я получил какие-либо результаты от работника, прежде чем я попытаюсь отправить другой ввод данных этому считывателю. Использование select делает это тривиальным: операция завершается успешно на том канале, который готов первым (если оба канала готовы, выбор выбирается случайным образом — в этом случае он работает идеально).

В заключение, for {, close(chan) и select очень хорошо работают вместе при отправке неизвестного количества входных данных в логические рабочие процессы горутины.

Несколько заключительных замечаний. В реальном мире обычно используется https://gobyexample.com/waitgroups вместо того, чтобы реализовывать все это вручную. . Концепция, как правило, та же, но в ней намного меньше отслеживания вещей, и в результате получается более чистый код. Я реализовал это сам, поэтому концепции были ясны.

И, наконец, вы заметите, что ничто не гарантирует, что рабочие горутины увидят закрытый канал до завершения программы. На практике технически возможно, что сообщение о закрытом канале может быть зарегистрировано не всеми горутинами. Но использование счетчика inflight гарантирует, что я получу их результаты, даже если у них не было возможности наблюдать за закрытием канала. Закрытие каналов и выход из воркеров имеет больше смысла, когда приложение будет продолжать запускать несколько пакетов воркеров с течением времени — если мы не уведомим их о закрытии, а затем создадим больше воркеров позже, это приведет к утечке памяти, как эти воркеры продолжать ждать ввода, который никогда не придет. С другой стороны, нет ничего необычного в использовании одного и того же набора рабочих процессов для нескольких пакетов запросов.

person Daniel Farrell    schedule 31.10.2020
comment
Вау! этот пример кода ответил на мой вопрос. спасибо за подробный ответ. - person DarthCucumber; 31.10.2020