【问题标题】:Golang buffer with concurrent readers具有并发阅读器的 Golang 缓冲区
【发布时间】:2017-11-02 19:20:16
【问题描述】:

我想在 Go 中构建一个支持多个并发读取器和一个写入器的缓冲区。所有写入缓冲区的内容都应该被所有阅读器读取。新读者可以随时加入,这意味着已经写入的数据必须能够回放给迟到的读者。

缓冲区应满足以下接口:

type MyBuffer interface {
    Write(p []byte) (n int, err error)
    NextReader() io.Reader
}

对于这种最好使用内置类型的实现,您有什么建议吗?

【问题讨论】:

  • 标准库中没有任何东西可以做到这一点。不过,您可以使用围绕频道构建的自定义结构,每个读者都会将他们读到的内容回显到频道,以便其他读者可以阅读。问题是定义限制在哪里。您希望能够为迟到的读者播放旧数据,但这意味着永远保留所有这些数据(嗯,在程序的生命周期内),因为您永远不知道新读者何时会加入。这是一个很大的内存泄漏风险。
  • 如果保留和重放旧数据不那么重要,这提供了单广播多接收系统的可靠实现:rogpeppe.wordpress.com/2009/12/01/…

标签: go concurrency buffer writer reader


【解决方案1】:

根据作者的性质和您使用它的方式,将所有内容保存在内存中(以便能够为以后加入的读者重新播放所有内容)是非常危险的,并且可能需要大量内存,或者导致您的应用由于内存不足而崩溃。

将它用于“低流量”记录器将所有内容保存在内存中可能没问题,但例如流式传输一些音频或视频很可能不行。

如果下面的读取器实现读取了写入缓冲区的所有数据,它们的Read() 方法将正确地报告io.EOF。必须小心,因为一旦遇到io.EOF,某些构造(例如bufio.Scanner)可能无法读取更多数据(但这不是我们实现的缺陷)。

如果您希望我们缓冲区的阅读器在缓冲区中没有更多可用数据时等待,等待新数据写入而不是返回io.EOF,您可以将返回的阅读器包装在提供的“尾部阅读器”中这里:Go: "tail -f"-like generator

“内存安全”文件实现

这是一个非常简单和优雅的解决方案。它使用文件来写入,也使用文件来读取。同步基本上由操作系统提供。这不会冒内存错误的风险,因为数据仅存储在磁盘上。根据作者的性质,这可能足够,也可能不够。

我宁愿使用下面的接口,因为Close()在文件的情况下很重要。

type MyBuf interface {
    io.WriteCloser
    NewReader() (io.ReadCloser, error)
}

而且实现极其简单:

type mybuf struct {
    *os.File
}

func (mb *mybuf) NewReader() (io.ReadCloser, error) {
    f, err := os.Open(mb.Name())
    if err != nil {
        return nil, err
    }
    return f, nil
}

func NewMyBuf(name string) (MyBuf, error) {
    f, err := os.Create(name)
    if err != nil {
        return nil, err
    }
    return &mybuf{File: f}, nil
}

我们的mybuf 类型嵌入了*os.File,因此我们可以“免费”获得Write()Close() 方法。

NewReader() 只是打开现有的支持文件进行读取(在只读模式下)并返回它,再次利用它实现了io.ReadCloser

创建一个新的MyBuf 值是在NewMyBuf() 函数中实现的,如果创建文件失败,该函数也可能返回一个error

注意事项:

请注意,由于mybuf 嵌入了*os.File,因此type assertion 可以“访问”os.File 的其他导出方法,即使它们不是MyBuf 接口的一部分。我不认为这是一个缺陷,但如果你想禁止这个,你必须将mybuf 的实现更改为不嵌入os.File,而是将它作为一个命名字段(但你必须添加@987654349 @ 和 Close() 方法自己,正确转发到 os.File 字段)。

内存实现

如果文件实现不充分,这里有一个内存实现。

由于我们现在只在内存中,我们将使用以下接口:

type MyBuf interface {
    io.Writer
    NewReader() io.Reader
}

这个想法是存储曾经传递给我们缓冲区的所有字节切片。当调用Read() 时,读取器将提供存储的切片,每个读取器将跟踪其Read() 方法提供了多少存储的切片。同步一定要处理,我们就用一个简单的sync.RWMutex

废话不多说,实现如下:

type mybuf struct {
    data [][]byte
    sync.RWMutex
}

func (mb *mybuf) Write(p []byte) (n int, err error) {
    if len(p) == 0 {
        return 0, nil
    }
    // Cannot retain p, so we must copy it:
    p2 := make([]byte, len(p))
    copy(p2, p)
    mb.Lock()
    mb.data = append(mb.data, p2)
    mb.Unlock()
    return len(p), nil
}

type mybufReader struct {
    mb   *mybuf // buffer we read from
    i    int    // next slice index
    data []byte // current data slice to serve
}

func (mbr *mybufReader) Read(p []byte) (n int, err error) {
    if len(p) == 0 {
        return 0, nil
    }
    // Do we have data to send?
    if len(mbr.data) == 0 {
        mb := mbr.mb
        mb.RLock()
        if mbr.i < len(mb.data) {
            mbr.data = mb.data[mbr.i]
            mbr.i++
        }
        mb.RUnlock()
    }
    if len(mbr.data) == 0 {
        return 0, io.EOF
    }

    n = copy(p, mbr.data)
    mbr.data = mbr.data[n:]
    return n, nil
}

func (mb *mybuf) NewReader() io.Reader {
    return &mybufReader{mb: mb}
}

func NewMyBuf() MyBuf {
    return &mybuf{}
}

请注意,Writer.Write() 的一般约定包括实现不得保留传递的切片,因此我们必须在“存储”它之前对其进行复制。

另请注意,Read() 的读者会尝试在最短的时间内锁定。也就是说,它只在我们需要来自缓冲区的新数据切片时才锁定,并且只进行读锁定,这意味着如果读取器有部分数据切片,它将在Read() 中发送,而不锁定和接触缓冲区。

【讨论】:

  • 非常感谢。优雅的解决方案。您对io.WriteCloser 的建议是个好主意。理想情况下,读者的Read() 方法会等待更多数据,直到作者关闭,此时他们会收到io.EOF。关于内存消耗,一旦超过一定大小,可以启动内存中的缓冲区,并将数据转储到磁盘。我将使用您的建议来实现这一目标。再次感谢您。
【解决方案2】:

我链接到仅附加提交日志,因为它似乎与您的要求非常相似。我对分布式系统和提交日志还很陌生,所以我可能会扼杀几个概念,但是 kafka 的介绍用漂亮的图表清楚地解释了一切。

Go 对我来说也很新,所以我确信有更好的方法:

但也许您可以将缓冲区建模为切片,我认为有几种情况:

  • 缓冲区没有读取器,新数据写入缓冲区,缓冲区长度增加
  • 缓冲区有一个/多个阅读器:

    • 读者订阅缓冲区
    • 缓冲区创建一个通道并将其返回给该客户端
    • buffer 维护着一个客户频道列表
    • 发生写入 -> 循环通过所有客户端通道并发布到它 (pub sub)

这解决了 pubsub 实时消费者流,其中消息被扇出,但不解决回填问题。

Kafka 启用了回填以及他们的intro illustrates 如何做到这一点:)

这个偏移量由消费者控制:通常消费者会 在读取记录时线性推进其偏移量,但事实上,因为 位置由消费者控制,它可以消费记录 它喜欢的任何顺序。例如,消费者可以重置为较旧的 偏移以重新处理过去的数据或跳到最 最近的记录并从“现在”开始消费。

这种特性组合意味着 Kafka 消费者非常 便宜——它们来来去去不会对集群或 其他消费者。例如,您可以使用我们的命令行工具 “尾随”任何主题的内容,而不改变所消费的内容 任何现有的消费者。

【讨论】:

    【解决方案3】:

    作为实验的一部分,我不得不做类似的事情,所以分享一下:

    type MultiReaderBuffer struct {
        mu  sync.RWMutex
        buf []byte
    }
    
    func (b *MultiReaderBuffer) Write(p []byte) (n int, err error) {
        if len(p) == 0 {
            return 0, nil
        }
        b.mu.Lock()
        b.buf = append(b.buf, p...)
        b.mu.Unlock()
        return len(p), nil
    }
    
    func (b *MultiReaderBuffer) NewReader() io.Reader {
        return &mrbReader{mrb: b}
    }
    
    type mrbReader struct {
        mrb *MultiReaderBuffer
        off int
    }
    
    func (r *mrbReader) Read(p []byte) (n int, err error) {
        if len(p) == 0 {
            return 0, nil
        }
        r.mrb.mu.RLock()
        n = copy(p, r.mrb.buf[r.off:])
        r.mrb.mu.RUnlock()
        if n == 0 {
            return 0, io.EOF
        }
        r.off += n
        return n, nil
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-28
      • 2017-07-22
      • 2016-06-17
      • 2017-09-23
      • 1970-01-01
      • 2012-03-11
      • 2023-03-23
      相关资源
      最近更新 更多