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

时间:2015-02-21 20:04:24

标签: multithreading go channels

请考虑以下代码:

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的内存模型文档,并了解保证通过通道发送变量可以保证在接收之前发生,但是如果有多个通道与a通信,我是否仍然有些困惑单个实例。

很抱歉,如果我措辞严厉,谢谢你的帮助。

2 个答案:

答案 0 :(得分:3)

原则上,使用通道是确保同步访问结构数据的有效方法。我用你的方法看到的问题是你的Run函数只做一次读取然后返回。只要您每次都从同一个goroutine拨打Run,它就可以运行,但这样会更容易。

只有所有结构访问仅限于一个,而且只有一个goroutine,才能保证内存安全。我通常这样做的方法是创建一个循环在通道上的轮询例程。无限期,或直到明确停止。

Here is an example。我为每个支持的操作创建了单独的通道,主要是为了让它更清晰。您可以轻松使用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
}

答案 1 :(得分:3)

是的,select只会等待或执行一个case块。 因此,如果您在任何时候只有一个Run函数,并且您知道没有其他goroutine会改变缓存,那么它将无竞争。

我假设你想要一个围绕选择的无限循环。

这是一个例子,你可以看到当一个人正在执行时,选择不会进入另一个区块...... https://play.golang.org/p/zFeRPK1h8c

btw,'self' is frowned upon作为接收者名称。