英文:
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
发送。
这种方式是否是线程安全的,可以保护缓存免受竞态条件的影响,或者是否可能出现同一个响应同时发送到AddChannel
和RemoveChannel
,导致缓存损坏?
我已经阅读了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 := <-self.AddChannel:
//..code
case resp := <- self.RemoveChannel:
//..code
case find := <- 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 "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
}
答案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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论