将map变量替换为新的map对象是否是线程安全的?

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

Is replacing the map variable with a new map object thread safe?

问题

我认为这段代码不是线程安全的,因为map对象的大小大于一个机器字,并且Golang不能保证它是线程安全的。但是当我使用go run -race main.go运行示例代码时,它从未报告错误。可能的原因是ThreadSanitizer依赖于运行时检查,并且赋值操作很难满足线程不安全的条件。

以下是示例代码:

package main

import (
	"fmt"
)

var m = make(map[int]bool)

func Read() {
	for {
		for k := range m {
			fmt.Println(k)
		}
	}
}

func Replace() {
	for {
		newM := make(map[int]bool, 10)
		for i := 0; i < 10; i++ {
			newM[i] = false
		}
		m = newM
	}
}

func main() {
	c := make(chan struct{})

	go Read()
	go Replace()

	<-c
}

那么我该如何修改代码以触发并发错误呢?或者也许我错了,这段代码是线程安全的吗?

英文:

I don't think it thread-safe because map object is larger than a machine word, and Golang has no guarantee it's thead-safe. But when I run the demo code with go run -race main.go, it never reports errors. It might be the reason that ThreadSanitizer depends on runtime checking and assigning operation is hard to meet the thread unsafe condition.

Here is the sample code:

package main

import (
	&quot;fmt&quot;
)

var m = make(map[int]bool)

func Read() {
	for {
		for k := range m {
			fmt.Println(k)
		}
	}
}

func Replace() {
	for {
		newM := make(map[int]bool, 10)
		for i := 0; i &lt; 10; i++ {
			newM[i] = false
		}
		m = newM
	}
}

func main() {
	c := make(chan struct{})

	go Read()
	go Replace()

	&lt;-c
}

So how could I modify the code to trigger the concurrent error? Or maybe I am wrong and the code is thread-safe?

答案1

得分: 3

这里有几点需要注意:

        for k := range m {

Range表达式在for循环开始时只会被评估一次。所以这个操作会读取m一次(注意,这意味着如果循环中的代码重新分配了m,循环将继续迭代原始的m,但如果向m中添加了新元素或删除了元素,循环将检测到这些变化),而循环本身会调用fmt.Println,这会消耗大部分的执行时间。如果你想捕捉到数据竞争,可以将其移除。

其次,你实际上不需要初始化第二个map。

当你做了这些修改并运行数据竞争检测器时,它可能会捕捉到数据竞争。在我的情况下,它确实捕捉到了。

数据竞争检测器在检测到竞争时会报告竞争。因此,如果它报告了竞争,那就是有竞争存在。如果它没有报告竞争,并不意味着没有竞争存在。

在我的平台上,map变量本身实际上与机器字大小相同:它只是指向map结构的指针。因此,对map变量的写入实际上是原子的,也就是说,在这个平台上你不会看到部分分配的map。然而,这并不能防止竞争,因为没有保证其他goroutine何时会看到那个内存写入。

简而言之,存在竞争。这不是因为map变量的大小。要修复这个问题,可以使用互斥锁(mutex)。

英文:

There are several things to note here:

        for k := range m {

Range expression is evaluated once at the beginning of the for loop. So this operation will read m once (note that this means if the code in the loop reassigns m, the loop will continue to iterate through the original m, but if new elements are added or elements are removed from m, those will be detected by the loop), and the loop itself will call fmt.Println, which consumes most of the execution time in that goroutine. If you want to catch the race, remove that.

Second, you don't really need to initialize the second map.

And when you do these things and run the race detector, it may catch the data race. In my case, it did.

The race detector complains about a race when it detects one. Because of that, if it reports a race, then there is a race. If it doesn't report one, that doesn't mean there isn't a race.

In my platform, a map variable itself is actually the same size as a machine word: it is simply a pointer to the map structure. So, the write to a map variable is actually atomic, that is, you would not see a partially-assigned map in this platform. However, that doesn't prevent the race, because there are no guarantees on when the other goroutine will see that memory write.

In short, there is a race. It is not because of the size of the map variable. To fix, use a mutex.

答案2

得分: 0

将map变量替换为新的map对象是否线程安全?

不,它是不安全的。

如何使其安全?

  1. 在代码中添加一些锁控制,使用sync中的锁(例如sync.RWMutex),以下是根据你的代码进行的粗略实现:
package main

import (
	"fmt"
	"sync"
)

type safeMap struct {
	rwLock sync.RWMutex
	m      map[int]bool
}

var safem = safeMap{
	m: make(map[int]bool),
}

func Read() {
	for {
		safem.rwLock.RLock()
		for k := range safem.m {
			fmt.Println(k)
		}
		safem.rwLock.RUnlock()
	}
}

func Replace() {
	for {
		newM := make(map[int]bool, 10)
		for i := 0; i < 10; i++ {
			newM[i] = false
		}
		safem.rwLock.Lock()
		safem.m = newM
		safem.rwLock.Unlock()
	}
}

func main() {
	c := make(chan struct{})

	go Read()
	go Replace()

	<-c
}
  1. 使用sync.Map

Map类似于Go中的map[interface{}]interface{},但可以在多个goroutine之间安全地进行并发使用,无需额外的锁定或协调。加载、存储和删除操作的时间复杂度为摊销常数时间。

英文:

> Is replacing the map variable with a new map object thread-safe?

No, It's unsafe.

What can you do to make it safe?

  1. add some lock control in sync (like sync.RWMutex), here is a rough implementation from your code:
package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
)

type safeMap struct {
	rwLock sync.RWMutex
	m      map[int]bool
}

var safem = safeMap{
	m: make(map[int]bool),
}

func Read() {
	for {
		safem.rwLock.RLock()
		for k := range safem.m {
			fmt.Println(k)
		}
		safem.rwLock.RUnlock()
	}
}

func Replace() {
	for {
		newM := make(map[int]bool, 10)
		for i := 0; i &lt; 10; i++ {
			newM[i] = false
		}
		safem.rwLock.Lock()
		safem.m = newM
		safem.rwLock.Unlock()
	}
}

func main() {
	c := make(chan struct{})

	go Read()
	go Replace()

	&lt;-c
}
  1. use sync.Map

> Map is like a Go map[interface{}]interface{} but is safe for concurrent use by multiple goroutines without additional locking or coordination. Loads, stores, and deletes run in amortized constant time.

答案3

得分: -1

我能够通过稍微修改Replace函数来触发一个警告。

package main

import (
	"fmt"
)

var m = make(map[int]bool)

func Read() {
	for {
		for k, v := range m {
			fmt.Println(k, v)
		}
	}
}

func Replace(flag bool) {
	for {
		newM := make(map[int]bool, 10)
		for i := 0; i < 10; i++ {
			newM[i] = flag
		}
		m = newM
	}
}

func main() {
	c := make(chan struct{})

	go Read()
	go Replace(false)
	go Replace(true)

	<-c
}

正如@Burak Serdar指出的那样,无法检测到竞争的原因可能是fmt.Println()占用了大部分的执行时间。

英文:

I was able to trigger a warning, modifying the Replace function a bit.

package main

import (
	&quot;fmt&quot;
)

var m = make(map[int]bool)

func Read() {
	for {
		for k, v := range m {
			fmt.Println(k, v)
		}
	}
}

func Replace(flag bool) {
	for {
		newM := make(map[int]bool, 10)
		for i := 0; i &lt; 10; i++ {
			newM[i] = flag
		}
		m = newM
	}
}

func main() {
	c := make(chan struct{})

	go Read()
	go Replace(false)
	go Replace(true)

	&lt;-c
}

As pointed out by @Burak Serdar, the reason for not detecting race might be due to fmt.Println() taking most of the execution time.

huangapple
  • 本文由 发表于 2023年5月23日 11:25:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/76311111.html
匿名

发表评论

匿名网友

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

确定