Какая самая сложная идиома для производителя/потребителя в Go?

Что бы я хотел сделать, так это иметь набор продюсеров goroutines (из которых некоторые могут или не могут быть завершены) и пользовательскую процедуру. Проблема заключается в этом предостережении в круглых скобках - мы не знаем общее число, которое вернет ответ.

Итак, что я хочу сделать, это:

package main

import (
  "fmt"
  "math/rand"
)

func producer(c chan int) {
  // May or may not produce.
  success := rand.Float32() > 0.5
  if success {
    c <- rand.Int()
  }
}

func main() {
  c := make(chan int, 10)
  for i := 0; i < 10; i++ {
    go producer(c, signal)
  }

  // If we include a close, then that WRONG. Chan will be closed
  // but a producer will try to write to it. Runtime error.
  close(c)

  // If we don't close, then that WRONG. All goroutines will
  // deadlock, since the range keyword will look for a close.
  for num := range c {
    fmt.Printf("Producer produced: %d\n", num)
  }
  fmt.Println("All done.")
}

Итак, проблема в том, что если я закрываю ее неправильно, если я не закрываю ее - она ​​по-прежнему не так (см. комментарии в коде).

Теперь решение будет внеполосным сигнальным каналом, который ВСЕ производители пишут:

package main

import (
  "fmt"
  "math/rand"
)

func producer(c chan int, signal chan bool) {
  success := rand.Float32() > 0.5
  if success {
    c <- rand.Int()
  }
  signal <- true
}

func main() {
  c := make(chan int, 10)
  signal := make(chan bool, 10)
  for i := 0; i < 10; i++ {
    go producer(c, signal)
  }

  // This is basically a 'join'.
  num_done := 0
  for num_done < 10 {
    <- signal
    num_done++
  }
  close(c)

  for num := range c {
    fmt.Printf("Producer produced: %d\n", num)
  }
  fmt.Println("All done.")
}

И это полностью делает то, что я хочу! Но для меня это похоже на глоток. Мой вопрос: есть ли какая-либо идиома/трюк, которая позволяет мне сделать что-то подобное более простым способом?

Я посмотрел здесь: http://golang.org/doc/codewalk/sharemem/ И кажется, что complete chan (инициализированный в начале main) используется в диапазоне, но никогда не закрывается. Я не понимаю, как.

Если у кого-нибудь есть идеи, я был бы очень признателен. Ура!


Изменить: у fls0815 есть ответ, а также ответили на вопрос о том, как работает диапазон каналов с более низким уровнем.

Мой код выше, модифицированный для работы (выполняется до любезного кода fls0815):

package main

import (
  "fmt"
  "math/rand"
  "sync"
)

var wg_prod sync.WaitGroup
var wg_cons sync.WaitGroup

func producer(c chan int) {
  success := rand.Float32() > 0.5
  if success {
    c <- rand.Int()
  }
  wg_prod.Done()
}

func main() {
  c := make(chan int, 10)
  wg_prod.Add(10)
  for i := 0; i < 10; i++ {
    go producer(c)
  }

  wg_cons.Add(1)
  go func() {
    for num := range c {
      fmt.Printf("Producer produced: %d\n", num)
    }
    wg_cons.Done()
  } ()

  wg_prod.Wait()
  close(c)
  wg_cons.Wait()
  fmt.Println("All done.")
}

Ответ 1

Только производители должны закрывать каналы. Вы могли бы достичь своей цели, вызывая потребителей (пользователей), которые итерации (range) по результирующему каналу после запуска ваших производителей. В основной теме вы ждете (см. sync.WaitGroup), пока ваши потребители/продюсеры не закончат свою работу. После того, как продюсеры закончили, вы закроете результирующий канал, который заставит ваших потребителей выйти (range выйдет, когда каналы будут закрыты, и буферный элемент не останется).

Пример кода:

package main

import (
    "log"
    "sync"
    "time"
    "math/rand"
    "runtime"
)

func consumer() {
    defer consumer_wg.Done()

    for item := range resultingChannel {
        log.Println("Consumed:", item)
    }
}

func producer() {
    defer producer_wg.Done()

    success := rand.Float32() > 0.5
    if success {
        resultingChannel <- rand.Int()
    }
}

var resultingChannel = make(chan int)
var producer_wg sync.WaitGroup
var consumer_wg sync.WaitGroup

func main() {
    rand.Seed(time.Now().Unix())

    for c := 0; c < runtime.NumCPU(); c++ {
        producer_wg.Add(1)  
        go producer()
    }

    for c := 0; c < runtime.NumCPU(); c++ {
        consumer_wg.Add(1)
        go consumer()
    }

    producer_wg.Wait()

    close(resultingChannel)

    consumer_wg.Wait()
}

Причина, по которой я ставлю close -statement в основную функцию, состоит в том, что у нас есть более одного производителя. Закрытие канала в одном из производителей в приведенном выше примере приведет к проблеме, с которой вы уже столкнулись (запись на закрытых каналах, причина в том, что остался один продюсер, который все еще производит данные). Каналы следует закрывать только тогда, когда не осталось ни одного продюсера (поэтому мое предложение о закрытии канала только продюсером). Так построены каналы в Go. Здесь вы найдете дополнительную информацию о закрытии каналов.


Связанный с примером sharemem: AFAICS этот пример работает бесконечно, снова и снова перезаряжая ресурсы (от ожидающего → полного → ожидающего → завершения... и т.д.). Это то, что делает итерация в конце main-func. Он получает завершенные Ресурсы и переупорядочивает их, используя Resource.Sleep() для ожидания. Когда нет завершенного ресурса, он ждет и блокирует выполнение новых Ресурсов. Поэтому нет необходимости закрывать каналы, потому что они все время используются.

Ответ 2

Существует множество способов решения этих проблем. Здесь решение, использующее простые синхронные каналы, фундаментальные в Go. Нет буферизованных каналов, нет закрывающих каналов, нет групп WaitGroups.

Это действительно не так далеко от вашего "полного" решения, и - извините, чтобы разочаровать - не намного меньше. Он ставит потребителя в его собственную горутин, так что потребитель может потреблять числа, как производитель производит их. Он также делает различие, что "попытка" производства может закончиться либо успехом, либо неудачей. Если сбой производства, попытка выполняется немедленно. Если это удается, попытка не выполняется до тех пор, пока номер не будет потреблен.

package main

import (
    "fmt"
    "math/rand"
)

func producer(c chan int, fail chan bool) {
    if success := rand.Float32() > 0.5; success {
        c <- rand.Int()
    } else {
        fail <- true
    }
}

func consumer(c chan int, success chan bool) {
    for {
        num := <-c
        fmt.Printf("Producer produced: %d\n", num)
        success <- true
    }
}

func main() {
    const nTries = 10
    c := make(chan int)
    done := make(chan bool)
    for i := 0; i < nTries; i++ {
        go producer(c, done)
    }
    go consumer(c, done)

    for i := 0; i < nTries; i++ {
        <-done
    }
    fmt.Println("All done.")
}

Ответ 3

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

Далее, канал, сам по себе, уже является идиоматической очередью потребительского производителя в Go. Размер асинхронного буфера, поддерживающего канал, определяет, сколько производителей может производить до получения противодавления. Установите N = 0 ниже, чтобы увидеть производителю-производителю без каких-либо гонок вперед или позади. Таким образом, N = 10 позволит производителю производить до 10 продуктов перед блокировкой.

Наконец, есть несколько хороших идиом для написания сообщений последовательных процессистов в Go (например, функции, которые запускают процедуры для вас и используют шаблон for/select для связи и принятия команд управления). Я думаю о WaitGroups как неуклюжий, и хотел бы увидеть идиоматические примеры.

package main

import (
    "fmt"
    "time"
)

type control int
const  (
    sleep control = iota
    die // receiver will close the control chan in response to die, to ack.
)

func (cmd control) String() string {
    switch cmd {
    case sleep: return "sleep"
    case die: return "die"
    }
    return fmt.Sprintf("%d",cmd)
}

func ProduceTo(writechan chan<- int, ctrl chan control, done chan bool) {
    var product int
    go func() {
        for {
            select {
        case writechan <- product:
            fmt.Printf("Producer produced %v\n", product)
            product++
        case cmd:= <- ctrl:
            fmt.Printf("Producer got control cmd: %v\n", cmd)
            switch cmd {
            case sleep:
                fmt.Printf("Producer sleeping 2 sec.\n")
                time.Sleep(2000 * time.Millisecond)
            case die:
                fmt.Printf("Producer dies.\n")
                close(done)
                return
            }
            }
        }
    }()
}

func ConsumeFrom(readchan <-chan int, ctrl chan control, done chan bool) {
    go func() {
        var product int
        for {
            select {
            case product = <-readchan:
                fmt.Printf("Consumer consumed %v\n", product)
            case cmd:= <- ctrl:
                fmt.Printf("Consumer got control cmd: %v\n", cmd)
                switch cmd {
                case sleep:
                    fmt.Printf("Consumer sleeping 2 sec.\n")
                    time.Sleep(2000 * time.Millisecond)
                case die:
                    fmt.Printf("Consumer dies.\n")
                    close(done)
                    return
                }

            }
        }
    }()
}

func main() {

    N := 10
    q := make(chan int, N)

    prodCtrl := make(chan control)
    consCtrl := make(chan control)

    prodDone := make(chan bool)
    consDone := make(chan bool)


    ProduceTo(q, prodCtrl, prodDone)
    ConsumeFrom(q, consCtrl, consDone)

    // wait for a moment, to let them produce and consume
    timer := time.NewTimer(10 * time.Millisecond)
    <-timer.C

    // tell producer to pause
    fmt.Printf("telling producer to pause\n")
    prodCtrl <- sleep

    // wait for a second
    timer = time.NewTimer(1 * time.Second)
    <-timer.C

    // tell consumer to pause
    fmt.Printf("telling consumer to pause\n")
    consCtrl <- sleep


    // tell them both to finish
    prodCtrl <- die
    consCtrl <- die

    // wait for that to actually happen
    <-prodDone
    <-consDone
}

Ответ 4

Вы можете использовать простые небуферизованные каналы без групп ожидания, если вы используете шаблон генератора с функцией fanIn.

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

Проблема, конечно, в том, что функция fanIn перенаправляет нулевое значение типа канала (int), когда каждый канал закрыт.

Вы можете обойти это, используя нулевое значение вашего типа канала в качестве контрольного значения и только используя результаты из канала fanIn, если они не являются нулевым значением.

Вот пример:

package main

import (
    "fmt"
    "math/rand"
)

const offset = 1

func producer() chan int {
    cout := make(chan int)
    go func() {
        defer close(cout)
        // May or may not produce.
        success := rand.Float32() > 0.5
        if success {
            cout <- rand.Int() + offset
        }
    }()
    return cout
}

func fanIn(cin []chan int) chan int {
    cout := make(chan int)
    go func() {
        defer close(cout)
        for _, c := range cin {
            cout <- <-c
        }
    }()
    return cout
}

func main() {
    chans := make([]chan int, 0)
    for i := 0; i < 10; i++ {
        chans = append(chans, producer())
    }

    for num := range fanIn(chans) {
        if num > offset {
            fmt.Printf("Producer produced: %d\n", num)
        }
    }
    fmt.Println("All done.")
}

Ответ 5

"продюсер-потребитель" - это такая распространенная модель, что я пишу библиотеку просумер для удобства тщательного общения с Чаном. Например:

func main() {
    maxLoop := 10
    var wg sync.WaitGroup
    wg.Add(maxLoop)
    defer wg.Wait()

    consumer := func(ls []interface{}) error {
        fmt.Printf("get %+v \n", ls)
        wg.Add(-len(ls))
        return nil
    }

    conf := prosumer.DefaultConfig(prosumer.Consumer(consumer))
    c := prosumer.NewCoordinator(conf)
    c.Start()
    defer c.Close(true)

    for i := 0; i < maxLoop; i++ {
        fmt.Printf("try put %v\n", i)
        discarded, err := c.Put(i)
        if err != nil {
            fmt.Errorf("discarded elements %+v for err %v", discarded, err)
            wg.Add(-len(discarded))
        }
        time.Sleep(time.Second)
    }

}