What is the neatest idiom for producer/consumer in Go?

后端 未结 5 2064
不知归路
不知归路 2021-02-01 23:42

What I would like to do is have a set of producer goroutines (of which some may or may not complete) and a consumer routine. The issue is with that caveat in parentheses - we do

相关标签:
5条回答
  • 2021-02-02 00:23

    I'm adding this because the extant answers don't make a couple things clear. First, the range loop in the codewalk example is just an infinite event loop, there to keep re-checking and updating the same url list forever.

    Next, a channel, all by itself, already is the idiomatic consumer-producer queue in Go. The size of the async buffer backing the channel determines how much producers can produce before getting backpressure. Set N = 0 below to see lock-step producer consumer without anyone racing ahead or getting behind. As it is, N = 10 will let the producer produce up to 10 products before blocking.

    Last, there are some nice idioms for writing communicating sequential processees in Go (e.g. functions that start go routines for you, and using the for/select pattern to communicate and accept control commands). I think of WaitGroups as clumsy, and would like to see idiomatic examples instead.

    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
    }
    
    0 讨论(0)
  • 2021-02-02 00:32

    producer-consumer is such a common pattern that I write a library prosumer for convenience with dealing with chan communication carefully. Eg:

    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)
        }
    
    }
    

    close has a param called graceful, which means whether drain the underlying chan.

    0 讨论(0)
  • 2021-02-02 00:33

    There are always lots of ways to solve these problems. Here's a solution using the simple synchronous channels that are fundamental in Go. No buffered channels, no closing channels, no WaitGroups.

    It's really not that far from your "mouthful" solution, and--sorry to disappoint--not that much smaller. It does put the consumer in it's own goroutine, so that the consumer can consume numbers as the producer produces them. It also makes the distinction that a production "try" can end in either success or failure. If production fails, the try is done immediately. If it succeeds, the try is not done until the number is consumed.

    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.")
    }
    
    0 讨论(0)
  • 2021-02-02 00:34

    Only producers should close channels. You could achieve your goal by invoking consumer(s) which iterates (range) over the resulting channel once your producers were started. In your main thread you wait (see sync.WaitGroup) until your consumers/producers finished their work. After producers finished you close the resulting channel which will force your consumers to exit (range will exit when channels are closed and no buffered item is left).

    Example code:

    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()
    }
    

    The reason I put the close-statement into the main function is because we have more than one producer. Closing the channel in one producer in the example above would lead to the problem you already ran into (writing on closed channels; the reason is that there could one producer left who still produces data). Channels should only be closed when there is no producer left (therefore my suggestion on closing the channel only by the producer). This is how channels are constructed in Go. Here you'll find some more information on closing channels.


    Related to the sharemem example: AFAICS this example runs endless by re-queuing the Resources again and again (from pending -> complete -> pending -> complete... and so on). This is what the iteration at the end of the main-func does. It receives the completed Resources and re-queues them using Resource.Sleep() to pending. When there is no completed Resource it waits and blocks for new Resources being completed. Therefore there is no need to close the channels because they are in use all the time.

    0 讨论(0)
  • 2021-02-02 00:35

    You can use simple unbuffered channels without wait groups if you use the generator pattern with a fanIn function.

    In the generator pattern, each producer returns a channel and is responsible for closing it. A fanIn function then iterates over these channels and forwards the values returned on them down a single channel that it returns.

    The problem of course, is that the fanIn function forwards the zero value of the channel type (int) when each channel is closed.

    You can work around it by using the zero value of your channel type as a sentinel value and only using the results from the fanIn channel if they are not the zero value.

    Here's an example:

    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.")
    }
    
    0 讨论(0)
提交回复
热议问题