将发送到通道隐藏在函数调用后面是否安全?

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

Is it safe to hide sending to channel behind function call

问题

我有一个名为Hub的结构体,其中包含一个Run()方法,该方法在自己的goroutine中执行。该方法按顺序处理传入的消息。消息并发地从多个生产者(单独的goroutine)中到达。当然,我使用一个channel来完成这个任务。但是现在我想将Hub隐藏在一个interface后面,以便能够从其实现中进行选择。因此,将channel作为简单的Hub字段不合适。

package main
import "fmt"
import "time"

type Hub struct {
    msgs chan string
}
func (h *Hub) Run() {
    for {
        msg, hasMore := <- h.msgs
        if !hasMore {
            return
        }
        fmt.Println("hub: msg received", msg)
    }
}
func (h *Hub) SendMsg(msg string) {
    h.msgs <- msg
}

func send(h *Hub, prefix string) {
    for i := 0; i < 5; i++ {
        fmt.Println("main: sending msg")
        h.SendMsg(fmt.Sprintf("%s %d", prefix, i))
    }
}

func main() {
    h := &Hub{make(chan string)}
    go h.Run()
    for i := 0; i < 10; i++ {
        go send(h, fmt.Sprintf("msg sender #%d", i))
    }
    time.Sleep(time.Second)
}

所以我引入了Hub.SendMsg(msg string)函数,它只是调用h.msgs <- msg,我可以将其添加到HubInterface中。作为一个Go新手,我想知道,从并发的角度来看,这样做是否安全?如果安全的话,这是Go中常见的方法吗?

Playground 在这里

英文:

I have a struct called Hub with a Run() method which is executed in its own goroutine. This method sequentially handles incoming messages. Messages arrive concurrently from multiple producers (separate goroutines). Of course I use a channel to accomplish this task. But now I want to hide the Hub behind an interface to be able to choose from its implementations. So, using a channel as a simple Hub's field isn't appropriate.

package main
import &quot;fmt&quot;
import &quot;time&quot;

type Hub struct {
	msgs chan string
}
func (h *Hub) Run() {
	for {
		msg, hasMore := &lt;- h.msgs
		if !hasMore {
			return
		}
		fmt.Println(&quot;hub: msg received&quot;, msg)
	}
}
func (h *Hub) SendMsg(msg string) {
	h.msgs &lt;- msg
}

func send(h *Hub, prefix string) {
	for i := 0; i &lt; 5; i++ {
		fmt.Println(&quot;main: sending msg&quot;)
		h.SendMsg(fmt.Sprintf(&quot;%s %d&quot;, prefix, i))
	}
}

func main() {
	h := &amp;Hub{make(chan string)}
	go h.Run()
	for i := 0; i &lt; 10; i++ {
		go send(h, fmt.Sprintf(&quot;msg sender #%d&quot;, i))
	}
	time.Sleep(time.Second)
}

So I've introduced Hub.SendMsg(msg string) function that just calls h.msgs &lt;- msg and which I can add to the HubInterface. And as a Go-newbie I wonder, is it safe from the concurrency perspective? And if so - is it a common approach in Go?

Playground here.

答案1

得分: 2

通道发送语义在将发送移入方法时不会改变。安德鲁的回答指出,通道需要使用make函数创建才能成功发送,但无论发送是否在方法内部,这一点始终如此。

如果你担心确保调用者不会意外地得到一个带有nil通道的无效Hub实例,一种方法是将结构体类型设为私有(hub),并使用一个NewHub()函数返回一个完全初始化的hub,并包装在你的接口类型中。由于结构体是私有的,其他包中的代码无法使用不完整的结构体字面值(或任何结构体字面值)来初始化它。

话虽如此,在Go语言中通常可以创建无效或荒谬的值,并且这是被接受的:net.IP("HELLO THERE BOB")是有效的语法,或者net.IP{}。因此,如果你认为公开你的Hub类型更好,那就去做吧。

英文:

Channel send semantics do not change when you move the send into a method. Andrew's answer points out that the channel needs to be created with make to send successfully, but that was always true, whether or not the send is inside a method.

If you are concerned about making sure callers can't accidentally wind up with invalid Hub instances with a nil channel, one approach is to make the struct type private (hub) and have a NewHub() function that returns a fully initialized hub wrapped in your interface type. Since the struct is private, code in other packages can't try to initialize it with an incomplete struct literal (or any struct literal).

That said, it's often possible to create invalid or nonsense values in Go and that's accepted: net.IP(&quot;HELLO THERE BOB&quot;) is valid syntax, or net.IP{}. So if you think it's better to expose your Hub type go ahead.

答案2

得分: -1

简单回答

是的

更好的回答

不是

通道非常适合从未知的Go协程中发出数据。它们可以安全地这样做,但我建议在某些部分要小心。在所列示的示例中,通道是由消费者创建结构体时创建的(而不是由消费者创建)。

假设消费者像下面这样创建了Hub:&amp;Hub{}。完全有效...除了SendMsg()的所有调用都会永远阻塞。幸运的是,你将它们放在了自己的Go协程中。所以你还好,对吗?错了。你现在正在泄漏Go协程。似乎没问题...直到你运行一段时间。Go鼓励你拥有有效的零值。在这种情况下,&amp;Hub{}是无效的。

通过使用select{}可以确保SendMsg()不会阻塞,但是你必须决定在遇到默认情况时要做什么(例如,丢弃数据)。通道可能会因为不正确的设置而阻塞。假设以后你不仅仅是在从通道中读取数据后打印数据。如果读取变得非常缓慢,或者在IO上阻塞,会发生什么情况?那么你将开始对生产者施加压力。

最终,通道允许你不用太多考虑并发性...但是如果这是一个高吞吐量的东西,那么你需要考虑很多。如果这是生产代码,那么你需要了解你的API中涉及到SendMsg()阻塞的情况。

英文:

Easy answer

Yes

Better answer

No

Channels are great for emitting data from unknown go-routines. They do so safely, however I would recommend being careful with a few parts. In the listed example the channel is created with the construction of the struct by the consumer (and not not by a consumer).

Say the consumer creates the Hub like the following: &amp;Hub{}. Perfectly valid... Apart from the fact that all the invokes of SendMsg() will block for forever. Luckily you placed those in their own go-routines. So you're still fine right? Wrong. You are now leaking go-routines. Seems fine... until you run this for a period of time. Go encourages you to have valid zero values. In this case &amp;Hub{} is not valid.

Ensuring SendMsg() won't block could be achieved via a select{} however you then have to decide what to do when you encounter the default case (e.g. throw data away). The channel could block for more reasons than bad setup too. Say later you do more than simply print the data after reading from the channel. What if the read gets very slow, or blocks on IO. You then will start pushing back on the producers.

Ultimately, channels allow you to not think much about concurrency... However if this is something of high-throughput, then you have quite a bit to consider. If it is production code, then you need to understand that your API here involves SendMsg() blocking.

huangapple
  • 本文由 发表于 2016年11月13日 02:38:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/40566314.html
匿名

发表评论

匿名网友

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

确定