有多个通道与单个共享结构进行通信是否线程安全?
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 上调用了 运行 函数。
如果要缓存响应,则通过缓存的 AddChannel
发送;
如果要删除响应,则通过 RemoveChannel
发送
如果需要找到响应,则通过 FindChannel
发送适当的密钥。
这是一种保护缓存免受竞争条件影响的线程安全方法吗,或者是否有可能,例如,可以将相同的响应发送到 AddChannel
和 RemoveChannel
,从而导致缓存损坏。
我已经阅读了 Go 的内存模型文档并了解它保证通过通道发送变量一定会在接收之前发生,但我有点困惑如果有多个通道进行通信,这是否仍然成立到单个实例。
抱歉,如果我的问题措辞不当,感谢您的帮助。
原则上,通道的使用是确保同步访问结构数据的有效方式。我在您的方法中看到的问题是您的 Run
函数只读取一次,然后 returns。只要您每次都从同一个 goroutine 调用 Run
,它可能会起作用,但还有更简单的方法。
内存安全只有在所有struct访问都限制在一个,而且只有一个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
}
是的,select 只会等待或执行一个 case 块。
因此,如果您在任何时候只有一个 运行 函数,并且您知道没有其他 goroutine 会改变缓存,那么它将是无竞争的。
我假设你想要一个围绕 select 的无限循环。
这是一个示例,您可以看到 select 在执行时没有进入另一个块... https://play.golang.org/p/zFeRPK1h8c
顺便说一句,'self' is frowned upon 作为收件人姓名。
考虑以下代码:
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 上调用了 运行 函数。
如果要缓存响应,则通过缓存的 AddChannel
发送;
如果要删除响应,则通过 RemoveChannel
如果需要找到响应,则通过 FindChannel
发送适当的密钥。
这是一种保护缓存免受竞争条件影响的线程安全方法吗,或者是否有可能,例如,可以将相同的响应发送到 AddChannel
和 RemoveChannel
,从而导致缓存损坏。
我已经阅读了 Go 的内存模型文档并了解它保证通过通道发送变量一定会在接收之前发生,但我有点困惑如果有多个通道进行通信,这是否仍然成立到单个实例。
抱歉,如果我的问题措辞不当,感谢您的帮助。
原则上,通道的使用是确保同步访问结构数据的有效方式。我在您的方法中看到的问题是您的 Run
函数只读取一次,然后 returns。只要您每次都从同一个 goroutine 调用 Run
,它可能会起作用,但还有更简单的方法。
内存安全只有在所有struct访问都限制在一个,而且只有一个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
}
是的,select 只会等待或执行一个 case 块。 因此,如果您在任何时候只有一个 运行 函数,并且您知道没有其他 goroutine 会改变缓存,那么它将是无竞争的。
我假设你想要一个围绕 select 的无限循环。
这是一个示例,您可以看到 select 在执行时没有进入另一个块... https://play.golang.org/p/zFeRPK1h8c
顺便说一句,'self' is frowned upon 作为收件人姓名。