Golang中map的赋值操作是否是原子性的

Golang中map的赋值操作是否是原子性的 以下是示例代码:

var data map[string]string

func main() {

go func() {
	v := data["d"]
	fmt.Println(v)

}()
go func() {

	temp := make(map[string]string)
	temp["a"] = "a"
	data = temp
}()
 
}

一个协程读取映射,一个协程修改映射,步骤:data = temp 是原子操作吗? 当数据类型是数组时会发生什么?

4 回复

你好,@skillian 我将一个 map 用作本地缓存,一个协程会读取它,而另一个协程会定期修改这个 map。如果我获取 map 的地址,然后将这个地址赋值给一个全局的 map 地址指针,这个赋值操作是原子性的吗?换句话说,在 Go 语言中,指针赋值是原子性的吗? 这是一个示例代码:

var data *map[string]string
temp :=make(map[string]string)
data=&temp

更多关于Golang中map的赋值操作是否是原子性的的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


一个协程负责读取,另一个协程会定期修改这个映射。

如果没有互斥锁或其他同步机制,这是不安全的,可能会导致程序崩溃。

在 Go 语言中,指针赋值是原子操作吗?

不是。你必须使用 sync/atomic 包,就像我之前的帖子中所做的那样。需要注意的是,x86 架构确实保证了指针的加载和存储是原子操作,但为了让你的代码能在 ARM 等其他架构上正常工作,你必须使用 sync/atomic 包。

你好,@SmallSmartMouse

不,map 的赋值操作不是原子的。此外,你用创建的 temp map 重新赋值给全局的 data map 也不是原子的。

如果你的想法是初始化一个 map,然后将其存储到一个将被视为只读的全局变量中,你可以使用 sync/atomic 包和一些指针技巧来实现:https://play.golang.org/p/3UmzbTT0Pcl

如果全局 map 需要保持可变,那么你需要使用互斥锁来保护对其的访问:https://play.golang.org/p/khh7W9NnHNi

不过,我认为这两种解决方案可能都不会被其他 Go 开发者所推崇。你应该使用更高级的抽象来控制对 map 的访问,但在不了解你计划如何使用这个 map 的情况下,我无法提供一个好的示例。

在Golang中,data = temp 这个赋值操作是原子性的,但整个map的并发访问仍然存在竞态条件问题。

1. Map赋值操作的原子性

package main

import (
	"fmt"
	"sync/atomic"
	"unsafe"
)

var data map[string]string

func main() {
	// 指针赋值是原子性的
	temp := make(map[string]string)
	temp["key"] = "value"
	
	// 这个赋值操作本身是原子的
	// 相当于对指针进行原子写入
	data = temp
	
	// 从内存模型角度看,这类似于:
	atomic.StorePointer(
		(*unsafe.Pointer)(unsafe.Pointer(&data)), 
		unsafe.Pointer(&temp),
	)
}

2. 但并发访问仍然不安全

package main

import (
	"fmt"
	"time"
)

var data map[string]string

func main() {
	// 协程1:读取map
	go func() {
		for i := 0; i < 1000; i++ {
			if data != nil {
				_ = data["key"] // 这里可能panic
			}
		}
	}()

	// 协程2:写入map
	go func() {
		temp := make(map[string]string)
		temp["key"] = "value"
		data = temp // 赋值是原子的,但...
		
		// 修改已赋值的map
		data["new"] = "value" // 这里与读取并发,不安全
	}()

	time.Sleep(time.Second)
}

3. 数组的情况

package main

import (
	"fmt"
	"time"
)

var arr [1000]int

func main() {
	// 协程1:读取数组
	go func() {
		for i := 0; i < 1000; i++ {
			fmt.Println(arr[0]) // 可能读到中间状态
		}
	}()

	// 协程2:修改数组
	go func() {
		newArr := [1000]int{}
		for i := 0; i < 1000; i++ {
			newArr[i] = i
		}
		arr = newArr // 数组赋值是值复制,非原子
	}()

	time.Sleep(time.Second)
}

4. 正确的并发访问方式

package main

import (
	"sync"
	"sync/atomic"
)

var (
	data atomic.Value
	mu   sync.RWMutex
	dataWithMutex map[string]string
)

// 方法1:使用atomic.Value
func safeWithAtomic() {
	// 写入
	temp := make(map[string]string)
	temp["key"] = "value"
	data.Store(temp)
	
	// 读取
	m := data.Load().(map[string]string)
	_ = m["key"]
}

// 方法2:使用sync.RWMutex
func safeWithMutex() {
	// 写入
	mu.Lock()
	dataWithMutex = make(map[string]string)
	dataWithMutex["key"] = "value"
	mu.Unlock()
	
	// 读取
	mu.RLock()
	_ = dataWithMutex["key"]
	mu.RUnlock()
}

关键点总结:

  1. 指针赋值是原子的data = temp 这个操作本身是原子的
  2. 但map操作不是原子的:对map的读写操作(如 data["key"])不是原子的
  3. 数组赋值不是原子的:数组是值类型,赋值会复制整个数组
  4. 需要同步机制:即使赋值是原子的,也需要mutex或atomic.Value来保证并发安全

竞态检测器会报告问题:

go run -race main.go
回到顶部