多个通道与单个共享结构进行通信是否是线程安全的?

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

Is it thread-safe to have multiple channels for communicating with a single shared struct?

问题

考虑以下代码:

type Cache struct{
	cache map[string]*http.Response
	AddChannel chan *http.Response
	RemoveChannel chan *http.Response
	FindChannel chan string
}

func (self *Cache) Run(){
	select{
		case resp := <-self.AddChannel:
		//..code
		case resp := <- self.RemoveChannel:
		//..code
		case find := <- self.FindChannel:
		//..code
	}
}

在这段代码中,创建了一个缓存,并在单独的goroutine上调用了Run函数。

如果要缓存一个响应,它会通过缓存的AddChannel发送;

如果要删除一个响应,它会通过RemoveChannel发送;

如果需要查找一个响应,适当的键会通过FindChannel发送。

这种方式是否是线程安全的,可以保护缓存免受竞态条件的影响,或者是否可能出现同一个响应同时发送到AddChannelRemoveChannel,导致缓存损坏?

我已经阅读了Go的内存模型文档,并理解通过通道发送变量是有保证的,会在接收之前发生,但如果有多个通道与单个实例进行通信,我对此还有些困惑。

如果我表达问题不清楚,对不起,并感谢你的帮助。

英文:

Consider the following code:

type Cache struct{
	cache map[string]*http.Response
	AddChannel chan *http.Response
	RemoveChannel chan *http.Response
	FindChannel chan string
}

func (self *Cache) Run(){
	select{
		case resp := &lt;-self.AddChannel:
		//..code
		case resp := &lt;- self.RemoveChannel:
		//..code
		case find := &lt;- self.FindChannel:
		//..code
	}
}

In this code, a cache is created and the Run function is called on a separate goroutine.

If a response is to be cached, it is sent through the cache's AddChannel;

if a response is to be removed, it is sent through the RemoveChannel

and if a response needs to be found, the appropriate key is sent through the FindChannel.

Is this a thread-safe way of protecting the cache against race conditions or is it possible that, for example, the same response could be sent to both the AddChannel and RemoveChannel leading to cache corruption.

I have read Go's memory model documentation and understand that it is guaranteed that sending a variable through a channel is guaranteed to happen before receiving but i'm somewhat confused as to whether this still holds if there are multiple channels to communicate to a single instance.

Sorry if I worded the question badly and thanks for your help.

答案1

得分: 3

原则上,使用通道是确保对结构体数据进行同步访问的有效方法。我对你的方法有一个问题,就是你的Run函数只进行一次读取然后返回。只要每次都从相同的goroutine调用Run,它可能会工作,但有一种更简单的方法。

只有当所有结构体访问都限制在一个且仅一个goroutine中时,才能保证内存安全。我通常的做法是创建一个轮询例程,它在通道上循环。可以无限循环,也可以直到明确停止为止。

这里有一个示例。我为每个支持的操作创建了单独的通道,主要是为了更清楚地了解正在发生的情况。你可以轻松地使用一个像chan interface{}这样的单个通道,并根据接收到的消息类型进行切换,以确定应执行的操作类型。这种设置方式在很大程度上基于Erlang的消息传递概念。它需要一定量的样板代码来设置,但消除了互斥锁的需求。它的效率和可扩展性只能通过测试来发现。还要注意,它会产生相当多的分配开销。

package main

import "fmt"

func main() {
    t := NewT()
    defer t.Close()

    t.Set("foo", 123)
    fmt.Println(t.Get("foo"))

    t.Set("foo", 321)
    fmt.Println(t.Get("foo"))

    t.Set("bar", 456)
    fmt.Println(t.Get("bar"))
}

type T struct {
    get  chan getRequest
    set  chan setRequest
    quit chan struct{}

    data map[string]int
}

func NewT() *T {
    t := &T{
        data: make(map[string]int),
        get:  make(chan getRequest),
        set:  make(chan setRequest),
        quit: make(chan struct{}, 1),
    }

    // Fire up the poll routine.
    go t.poll()
    return t
}

func (t *T) Get(key string) int {
    ret := make(chan int, 1)
    t.get <- getRequest{
        Key:   key,
        Value: ret,
    }
    return <-ret
}

func (t *T) Set(key string, value int) {
    t.set <- setRequest{
        Key:   key,
        Value: value,
    }
}

func (t *T) Close() { t.quit <- struct{}{} }

// poll loops indefinitely and reads from T's channels to do
// whatever is necessary. Keeping it all in this single routine,
// ensures all struct modifications are preformed atomically.
func (t *T) poll() {
    for {
        select {
        case <-t.quit:
            return

        case req := <-t.get:
            req.Value <- t.data[req.Key]

        case req := <-t.set:
            t.data[req.Key] = req.Value
        }
    }
}

type getRequest struct {
    Key   string
    Value chan int
}

type setRequest struct {
    Key   string
    Value int
}

希望这可以帮助到你!

英文:

In principle the usage of channels is a valid way to ensure synchronous access to the struct data. The problem I see with your approach is that your Run function does only a single read and then returns. As long as you call Run from the same goroutine every time, it might work but there's an easier way.

The memory safety can be guaranteed only of all struct access is confined to one, and only one, goroutine. The way I usually do that is to create a polling routine which loops on the channels. Either indefinitely, or until it is explicitly stopped.

Here is an example. I create separate channels for each supported operation, mostly to make it clearer what is going on. You can easily use a single channel like chan interface{}, and switch on the type of the message received to see what kind of operation you should be performing. This kind of setup is very loosely based on Erlang's message passing concepts. It requires a fair amount of boilerplate to setup, but eliminates the need for mutex locks. Whether it is efficient and scaleable is something you can only discover through testing. Note also that it packs a fair amount of allocation overhead.

package main
import &quot;fmt&quot;
func main() {
t := NewT()
defer t.Close()
t.Set(&quot;foo&quot;, 123)
fmt.Println(t.Get(&quot;foo&quot;))
t.Set(&quot;foo&quot;, 321)
fmt.Println(t.Get(&quot;foo&quot;))
t.Set(&quot;bar&quot;, 456)
fmt.Println(t.Get(&quot;bar&quot;))
}
type T struct {
get  chan getRequest
set  chan setRequest
quit chan struct{}
data map[string]int
}
func NewT() *T {
t := &amp;T{
data: make(map[string]int),
get:  make(chan getRequest),
set:  make(chan setRequest),
quit: make(chan struct{}, 1),
}
// Fire up the poll routine.
go t.poll()
return t
}
func (t *T) Get(key string) int {
ret := make(chan int, 1)
t.get &lt;- getRequest{
Key:   key,
Value: ret,
}
return &lt;-ret
}
func (t *T) Set(key string, value int) {
t.set &lt;- setRequest{
Key:   key,
Value: value,
}
}
func (t *T) Close() { t.quit &lt;- struct{}{} }
// poll loops indefinitely and reads from T&#39;s channels to do
// whatever is necessary. Keeping it all in this single routine,
// ensures all struct modifications are preformed atomically.
func (t *T) poll() {
for {
select {
case &lt;-t.quit:
return
case req := &lt;-t.get:
req.Value &lt;- t.data[req.Key]
case req := &lt;-t.set:
t.data[req.Key] = req.Value
}
}
}
type getRequest struct {
Key   string
Value chan int
}
type setRequest struct {
Key   string
Value int
}

答案2

得分: 3

是的,选择语句只会等待或执行一个case块。所以,如果你在任何时候只有一个Run函数,并且你知道没有其他goroutine会修改缓存,那么它就是无竞争的。

我假设你想要一个无限循环的select。

这里有一个例子,你可以看到在一个case块执行时,select不会进入另一个case块... https://play.golang.org/p/zFeRPK1h8c

顺便说一下,使用"self"作为接收器名称是不被推荐的。

英文:

Yes the select will only ever either be waiting or executing one case block.
So if you only have one Run function at any time and you know no other goroutines will mutate the cache, then it will be race free.

I assume you wanted a infinite loop round the select.

heres an example where you can see the select does not enter another block whilst one is executing... https://play.golang.org/p/zFeRPK1h8c

btw, 'self' is frowned upon as a receiver name.

huangapple
  • 本文由 发表于 2015年2月22日 04:04:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/28650702.html
匿名

发表评论

匿名网友

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

确定