没有接收器时,保持数据打开的缓冲通道是否可以?

时间:2018-05-31 03:20:37

标签: go concurrency channel goroutine

假设一个频道有10个发送者和一个接收者。发送方功能需要一些时间才能返回值。接收器只需要来自通道的一个值(接收的第一个值),而不使用其他9个值。接收器不需要等待其余9个值。这就是为什么我没有使用sync.WaitGroup

我使用了缓冲通道,因此当接收器只接收第一个时,缓冲通道中将有9个数据。我的问题是:

  1. 没有接收器时,是否可以保留数据打开的缓冲通道?下面的示例代码是简单的,但如果程序是一个守护进程,它最终会被垃圾收集吗?

  2. 有没有更好的方法来处理这种情况?我试图使用取消频道但失败了。而且我不确定context是否适合这种情况。

  3. 示例代码:

    package main
    
    import (
        "errors"
        "fmt"
        "math/rand"
        "time"
    )
    
    func main() {
        rand.Seed(time.Now().UnixNano())
    
        i, err := getRandomInt()
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(i)
        }
    
        fmt.Println("Waiting goroutines to be finished...")
        time.Sleep(2 * time.Second)
    }
    
    func getRandomInt() (int, error) {
        ch := make(chan int, 10)
    
        // 10 senders
        for i := 0; i < 10; i++ {
            go func(i int) {
                defer fmt.Printf("Goroutine #%d finished\n", i)
                fmt.Printf("Goroutine #%d started\n", i)
    
                data := heavyJob()
                ch <- data
                fmt.Printf("Goroutine #%d sent data %d to ch\n", i, data)
                return
            }(i)
        }
    
        // 1 receiver
        timeout := time.After(2000 * time.Millisecond)
        for {
            select {
            case value := <-ch:
                // uses only the value received first, the rest are discarded
                return value, nil
            case <-timeout:
                return -1, errors.New("Timeout")
            }
        }
    }
    
    // takes 1900~2900ms to finish
    func heavyJob() int {
        r := rand.Intn(1000)
        time.Sleep(time.Duration(r+1900) * time.Millisecond)
        return r
    }
    

    Run on playground

1 个答案:

答案 0 :(得分:2)

回答主要问题:

  1. 离开频道是好的,它会被垃圾收集。
  2. 这似乎基于意见,但对我来说,如果您创建一个包含10个空格的缓冲通道的唯一原因是发送方goroutines可以退出;这感觉它会从重新设计中受益。还有其他(可能更好)的方法可以确保发送方goroutines可以关闭。
  3. 基本上,您在工作者数量和缓冲通道大小之间创建隐式耦合。改变这两个数字中的一个,一些东西会死锁/破坏! (作为旁注,缓冲通道通常用于消费者和生产者以相同的速率工作的情况,但是一个没有稳定的输出。它很尖锐,缓冲平滑了峰值和谷值。)

    考虑到这一点,我建议显式更好地管理你不想要所有值的事实。

    这是getRandomInt()函数的更新版本。请注意在顶部使用defer设置上下文取消,以及在send上使用select语句。

    func getRandomInt() (int, error) {
        ctx := context.Background() // creates a fresh, empty context
        ctx, cancel := context.WithCancel(ctx)
        defer cancel() // cancels the context when getRandomInt() returns
    
        ch := make(chan int)
    
        // 10 senders
        for i := 0; i < 10; i++ {
            go func(i int) {
                defer fmt.Printf("Goroutine #%d finished\n", i)
                fmt.Printf("Goroutine #%d started\n", i)
    
                data := heavyJob()
    
                // this select statement wil block until either this goroutine 
                // is the first to send, or the context is cancelled. In which case
                // another routine has already sent and it can discard it's values.
                select { 
                case ch <- data:
                    fmt.Printf("Goroutine #%d sent data %d to ch\n", i, data)
                case <-ctx.Done():
                    fmt.Printf("Goroutine #%d did not send, context is cancelled, would have sent data %d to ch\n", i, data)
                }
            }(i)
        }
    
        // 1 receiver
        timeout := time.After(2000 * time.Millisecond)
        select {
        case value := <-ch:
            // uses only the value received first, the rest are discarded
            return value, nil
        case <-timeout:
            return -1, errors.New("Timeout")
        }
    }
    

    使用取消设置上下文意味着一旦调用cancel()函数,上下文就变为“完成”。这是告诉所有发件人goroutines不要等待发送的方法。

    在发送时,select语句将阻塞,直到cancel()函数取消上下文为止;或接收方法读取第一个值。

    我还删除了频道的缓冲,因为不再需要它了。