什么是Go语言中的单向通道的作用?

huangapple go评论125阅读模式
英文:

What's the point of one-way channels in Go?

问题

我正在学习Go语言,到目前为止对它印象非常好。我已经阅读了golang.org上的所有在线文档,并且已经完成了Chrisnall的《Go编程语言短语书》一半的内容。我理解了通道的概念,并且认为它们非常有用。然而,在学习过程中我可能错过了一些重要的东西,因为我看不出单向通道的意义所在。

如果我正确理解的话,只读通道只能接收数据,只写通道只能发送数据,那么为什么要有一个只能发送而不能接收的通道呢?它们可以从一个“方向”转换到另一个方向吗?如果可以,那么如果没有实际的约束,这样做有什么意义呢?它们只是向客户端代码提示通道的用途吗?

英文:

I'm learning Go and so far very impressed with it. I've read all the online docs at golang.org and am halfway through Chrisnall's "The Go Programming Language Phrasebook". I get the concept of channels and think that they will be extremely useful. However, I must have missed something important along the way, as I can't see the point to one-way channels.

If I'm interpreting them correctly, a read-only channel can only be received on and a write-only channel can only be transmitted on, so why have a channel that you can send to and never receive on? Can they be cast from one "direction" to the other? If so, again, what's the point if there's no actual constraint? Are they nothing more than a hint to client code of the channel's purpose?

答案1

得分: 110

一个通道可以被设置为只读,而发送者仍然可以拥有一个双向通道可以写入。例如:

func F() <-chan int {
    // 创建一个普通的双向通道。
    c := make(chan int)

    go func() {
        defer close(c)

        // 做一些操作
        c <- 123
    }()

    // 返回通道,隐式地将其转换为只读,
    // 根据函数返回类型。
    return c
}

调用F()的人会接收到一个只能读取的通道。
这在编译时避免潜在的通道误用时非常有用。
因为只读或只写通道是不同的类型,编译器可以使用
现有的类型检查机制来确保调用者不会尝试向其无权写入的通道写入数据。

英文:

A channel can be made read-only to whoever receives it, while the sender still has a two-way channel to which they can write. For example:

func F() &lt;-chan int {
    // Create a regular, two-way channel.
    c := make(chan int)

    go func() {
        defer close(c)

        // Do stuff
        c &lt;- 123
    }()

    // Returning it, implicitly converts it to read-only,
    // as per the function return type.
    return c
}

Whoever calls F(), receives a channel from which they can only read.
This is mostly useful to avoid potential misuse of a channel at compile time.
Because read/write-only channels are distinct types, the compiler can use
its existing type-checking mechanisms to ensure the caller does not try to write
stuff into a channel it has no business writing to.

答案2

得分: 10

我认为只读通道的主要动机是为了防止通道的损坏和恐慌。想象一下,如果你可以向time.After返回的通道写入数据,这可能会搞乱很多代码。

此外,如果你:

  • 关闭一个通道多次
  • 向已关闭的通道写入数据

这些操作对于只读通道来说是编译时错误,但在多个Go协程可以写入/关闭通道时,它们可能会导致严重的竞态条件。

解决这个问题的一种方法是永远不关闭通道,让它们被垃圾回收。然而,close不仅仅用于清理,当通道被遍历时它实际上是有用的:

func consumeAll(c &lt;-chan bool) {
    for b := range c {
        ...
    }
}

如果通道永远不关闭,这个循环将永远不会结束。如果有多个Go协程写入通道,那么就需要进行很多决策来决定哪个协程将关闭通道。

由于你不能关闭只读通道,这样可以更容易编写正确的代码。正如@jimt在他的评论中指出的那样,你不能将只读通道转换为可写通道,所以你可以确保只有能够访问可写版本的代码部分才能关闭/写入它。

编辑:

至于有多个读取者,这是完全可以的,只要你考虑到这一点。这在生产者/消费者模型中特别有用。例如,假设你有一个TCP服务器,只接受连接并将它们写入队列供工作线程使用:

func produce(l *net.TCPListener, c chan&lt;- net.Conn) {
    for {
        conn, _ := l.Accept()
        c&lt;-conn
    }
}

func consume(c &lt;-chan net.Conn) {
    for conn := range c {
        // 处理conn
    }
}

func main() {
    c := make(chan net.Conn, 10)
    for i := 0; i &lt; 10; i++ {
        go consume(c)
    }

    addr := net.TCPAddr{net.ParseIP(&quot;127.0.0.1&quot;), 3000}
    l, _ := net.ListenTCP(&quot;tcp&quot;, &amp;addr)
    produce(l, c)
}

可能你的连接处理时间比接受新连接的时间长,所以你希望有很多消费者和一个生产者。多个生产者更困难(因为你需要协调谁关闭通道),但你可以在通道发送中添加一种类似信号量的通道。

英文:

I think the main motivation for read-only channels is to prevent corruption and panics of the channel. Imagine if you could write to the channel returned by time.After. This could mess up a lot of code.

Also, panics can occur if you:

  • close a channel more than once
  • write to a closed channel

These operations are compile-time errors for read-only channels, but they can cause nasty race conditions when multiple go-routines can write/close a channel.

One way of getting around this is to never close channels and let them be garbage collected. However, close is not just for cleanup, but it actually has use when the channel is ranged over:

func consumeAll(c &lt;-chan bool) {
    for b := range c {
        ...
    }
}

If the channel is never closed, this loop will never end. If multiple go-routines are writing to a channel, then there's a lot of book-keeping that has to go on with deciding which one will close the channel.

Since you cannot close a read-only channel, this makes it easier to write correct code. As @jimt pointed out in his comment, you cannot convert a read-only channel to a writeable channel, so you're guaranteed that only parts of the code with access to the writable version of a channel can close/write to it.

Edit:

As for having multiple readers, this is completely fine, as long as you account for it. This is especially useful when used in a producer/consumer model. For example, say you have a TCP server that just accepts connections and writes them to a queue for worker threads:

func produce(l *net.TCPListener, c chan&lt;- net.Conn) {
    for {
        conn, _ := l.Accept()
        c&lt;-conn
    }
}

func consume(c &lt;-chan net.Conn) {
    for conn := range c {
        // do something with conn
    }
}

func main() {
    c := make(chan net.Conn, 10)
    for i := 0; i &lt; 10; i++ {
        go consume(c)
    }

    addr := net.TCPAddr{net.ParseIP(&quot;127.0.0.1&quot;), 3000}
    l, _ := net.ListenTCP(&quot;tcp&quot;, &amp;addr)
    produce(l, c)
}

Likely your connection handling will take longer than accepting a new connection, so you want to have lots of consumers with a single producer. Multiple producers is more difficult (because you need to coordinate who closes the channel) but you can add some kind of a semaphore-style channel to the channel send.

答案3

得分: 7

Go通道是基于霍尔的通信顺序进程(Hoare's Communicating Sequential Processes)进行建模的,这是一种面向通信参与者(小写的“a”)之间的事件流的并发过程代数。因此,通道具有方向,因为它们有发送端和接收端,即事件的生产者和消费者。Occam和Limbo也使用了类似的模型。

这一点很重要 - 如果一个通道端在不同的时间可以任意地被重复使用作为发送方和接收方,那么很难推理出关于死锁问题的结果。

英文:

Go channels are modelled on Hoare's Communicating Sequential Processes, a process algebra for concurrency that is oriented around event flows between communicating actors (small 'a'). As such, channels have a direction because they have a send end and a receive end, i.e. a producer of events and a consumer of events. A similar model is used in Occam and Limbo also.

This is important - it would be hard to reason about deadlock issues if a channel-end could arbitrarily be re-used as both sender and receiver at different times.

huangapple
  • 本文由 发表于 2012年11月28日 09:15:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/13596186.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定