为什么Go通道限制缓冲区大小

时间:2017-01-28 03:52:55

标签: go channel goroutine

我是Go的新手,我可能会忽略这一点,但为什么Go通道限制在缓冲通道的最大缓冲区大小可以有?例如,如果我创建一个这样的频道

channel := make(chan int, 100)

我无法在没有阻止的情况下向频道添加超过100个元素,这有什么原因吗?此外,它们无法动态调整大小,因为通道API不支持它。

这似乎限制了语言支持单一机制的通用同步,因为与无界信号相比,它缺乏方便性。例如,可以无限制地增加广义信号量的值。

3 个答案:

答案 0 :(得分:7)

如果程序的某个组件无法跟上其输入,则需要对系统的其余部分施加压力,而不是让它继续运行并生成将永远不会被处理的千兆字节数据因为系统内存耗尽而崩溃。

实际上没有无限缓冲区这样的东西,因为机器对它们可以处理的内容有限制。 Go要求您指定缓冲通道的大小,以便您考虑程序实际需要和可以处理的缓冲区大小。如果它确实需要十亿个项目,并且可以处理它们,那么你可以创建一个很大的渠道。但在大多数情况下,缓冲区大小为0或1实际上是需要的。

答案 1 :(得分:0)

缓冲区大小是可以在没有发送阻止的情况下发送到通道的元素数。默认情况下,通道的缓冲区大小为0(使用make(chan int)获得此值)。这意味着每个发送都会阻塞,直到另一个goroutine从该频道收到。缓冲区大小为1的通道可以保存1个元素直到发送块,因此您将获得

c := make(chan int, 1)
c <- 1 // doesn't block
c <- 2 // blocks until another goroutine receives from the channel

我建议您查看此内容以获得更多说明:  https://rogpeppe.wordpress.com/2010/02/10/unlimited-buffering-with-low-overhead/  http://openmymind.net/Introduction-To-Go-Buffered-Channels/

答案 2 :(得分:0)

这是因为 Channels 是为并发 goroutines 之间的高效通信而设计的,但你的需求是不同的:你阻塞的事实表明接收者没有参加工作队列,而“动态”很少是空闲的。< /p>

您可以使用多种不同的模式和算法来解决您遇到的问题:您可以更改频道以接受整数数组,您可以添加额外的 goroutine 以更好地平衡或过滤工作。或者您可以实现自己的动态频道。这样做当然是了解为什么动态通道不是构建并发的好方法的有用练习。

package main

import "fmt"

func dynamicChannel(initial int) (chan <- interface{}, <- chan interface{}) {
    in := make(chan interface{}, initial)
    out := make(chan interface{}, initial)
    go func () {
        defer close(out)
        buffer := make([]interface{}, 0, initial)
    loop:
        for {
            packet, ok := <- in
            if !ok {
                break loop
            }
            select {
            case out <- packet:
                continue
            default:
            }
            buffer = append(buffer, packet)
            for len(buffer) > 0 {
                select {
                case packet, ok := <-in:
                    if !ok {
                        break loop
                    }
                    buffer = append(buffer, packet)

                case out <- buffer[0]:
                    buffer = buffer[1:]
                }
            }
        }
        for len(buffer) > 0 {
            out <- buffer[0]
            buffer = buffer[1:]
        }
    } ()

    return in, out
}


func main() {
    in, out := dynamicChannel(4)

    in <- 10
    fmt.Println(<-out)

    in <- 20
    in <- 30

    fmt.Println(<-out)
    fmt.Println(<-out)

    for i := 100; i < 120; i++ {
        in <- i
    }
    fmt.Println("queued 100-120")
    fmt.Println(<-out)
    close(in)
    fmt.Println("in closed")
    for i := range out {
        fmt.Println(i)
    }
}

通常,如果您正在阻塞,则表明您的并发性没有很好地平衡。考虑不同的策略。例如,一个简单的工具来查找具有匹配 .checksum 文件的文件,然后检查哈希:

func crawl(toplevel string, workQ chan <- string) {
  defer close(workQ)
  for _, path := filepath.Walk(toplevel, func (path string, info os.FileInfo, err error) error {
    if err == nil && info.Mode().IsRegular() {
      workQ <- path
  }
}

// if a file has a .checksum file, compare it with the file's checksum.
func validateWorker(workQ <- chan string) {
  for path := range workQ {
    // If there's a .checksum file, read it, limit to 256 bytes.
    expectedSum, err := os.ReadFile(path + ".checksum")[:256]
    if err != nil {  // ignore
      continue
    }
    file, err := os.Open(path)
    if err != nil {
      continue
    }
    defer close(file)
    hash := sha256.New()
    if _, err := io.Copy(hash, file); err != nil {
      log.Printf("couldn't hash %s: %w", path, err)
      continue
    }
    actualSum := fmt.Sprintf("%x", hash.Sum(nil))
    if actualSum != expectedSum {
      log.Printf("%s: mismatch: expected %s, got %s", path, expectedSum, actualSum)
  }
}

即使没有任何 .checksum 文件,爬网功能也往往会超过工作队列。当遇到 .checksum 文件时,尤其是文件很大时,worker 可能需要很长时间才能执行单个校验和。

这里更好的目标是通过减少“validateWorker”所做的事情的数量来实现更一致的吞吐量。现在它有时很快,因为它会检查校验和文件。有时它很慢,因为它也加载必须读取和校验文件。

type ChecksumQuery struct {
  Filepath  string
  ExpectSum string
}

func crawl(toplevel string, workQ chan <- ChecksumQuery) {
  // Have a worker filter out files which don't have .checksums, and allow it
  // to get a little ahead of the crawl function.
  checkupQ := make(chan string, 4)

  go func () {
    defer close(workQ)
    for path := range checkupQ {
      expected, err := os.ReadFile(path + ".checksum")[:256]
      if err == nil && len(expected) > 0 {
        workQ <- ChecksumQuery{ path, string(expected) }
      }
    }
  }()

  go func () {
    defer close(checkupQ)
    for _, path := filepath.Walk(toplevel, func (path string, info os.FileInfo, err error) error {
      if err == nil && info.Mode().IsRegular() {
        checkupQ <- path
      }
    }
  }()
}

运行合适数量的validate worker,为workQ分配合适的大小,但如果爬虫或validate函数阻塞,那是因为validate正在做有用的工作。

如果您的验证工作人员都很忙,那么他们都在从磁盘消耗大文件并对它们进行哈希处理。让其他工作人员通过抓取更多文件名、分配和传递字符串来打断这一点,这并不有利。

其他场景可能是将大列表传递给工作人员,其中通过通道传递切片(它很便宜);或动态大小的事物组,在这种情况下,请考虑传递通道或捕获通道。