Golang遍历map的同时并发修改map的值

目录

背景

如题,有个逻辑设计,在遍历map的同时需要并发的修改map的值

解决

先说下解决,那就是把map重新复制一份,不是同一个map自然也就不存在并发安全和死锁的问题了,但是因为不是同一个map了,自然是需要注意数据还是否有效的问题了。这个可以通过加锁之后的再次判断来解决。

需求问题

  1. 并发的读写map会出现panic:fatal error: concurrent map iteration and map write
  2. 因为既要通过range进行遍历,又要在另一个goroutine中进行修改,所以range的时候没法加锁,因为修改的时候要加锁,如果在range的整个过程加锁,那具体的修改时候就会造成死锁
  3. 也考虑过 go1.9 之后提供的 sync.Map,但是这个类型没有获取map长度的方法,所以也没法满足我的需求
并发问题举例1 - panic
package main

import (
	"fmt"
	"strconv"
	"testing"
)

func TestRangeMap(t *testing.T) {
	tmpMap := make(map[int]string, 0)
	tmpMap[1] = "1"
	tmpMap[2] = "2"
	tmpMap[3] = "3"
	tmpMap[4] = "4"
	go func() {
		for i := 0; i < 10000; i++ {
			//map并发的写
			tmpMap[i] = strconv.Itoa(i)
		}
	}()
	//遍历map,相当于读
	for k, v := range tmpMap {
		fmt.Println(k, v)
	}
}

结果:

=== RUN   TestRangeMap
1 1
2 2
fatal error: concurrent map iteration and map write

goroutine 33 [running]:
runtime.throw(0x113fe01, 0x26)
......
并发问题举例2 - 死锁
package main

import (
	"strconv"
	"sync"
	"testing"
)

type DataMap struct {
	Data map[int]string
	Mux  *sync.Mutex
}

func changeMap(data DataMap) {
	//修改时又加锁
	//因为进入该方法前,已经加锁了,所以这里肯定会死锁
	data.Mux.Lock()
	data.Data[0] = strconv.Itoa(0)
	data.Mux.Unlock()
}

func TestRangeMap(t *testing.T) {
	data := DataMap{
		Data: make(map[int]string),
		Mux:  &sync.Mutex{},
	}
	data.Data[1] = "1"
	data.Data[2] = "2"
	data.Data[3] = "3"
	data.Data[4] = "4"

	wg := &sync.WaitGroup{}
	wg.Add(1)

	go func() {
		defer wg.Done()
		//遍历前加锁
		data.Mux.Lock()
		//遍历完成后解锁
		defer data.Mux.Unlock()
		for range data.Data {
			//遍历的同时,具体内部逻辑还要修改map,修改时又加锁
			changeMap(data)
		}
	}()

	wg.Wait()
}

结果:

=== RUN   TestRangeMap
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
testing.(*T).Run(0xc0000c8100, 0x113ae67, 0xc, 0x11427a8, 0x1069e26)
......

通过复制map解决问题

/**
*  Created with GoLand
*  User: zhuxinquan
*  Date: 2018-12-06
*  Time: 下午6:11
**/
package main

import (
	"strconv"
	"sync"
	"testing"
)

type DataMap struct {
	Data map[int]string
	Mux  *sync.Mutex
}

func changeMap(data DataMap) {
	//修改时又加锁
	//因为进入该方法前,已经加锁了,所以这里肯定会死锁
	data.Mux.Lock()
	data.Data[0] = strconv.Itoa(0)
	data.Mux.Unlock()
}

func TestRangeMap(t *testing.T) {
	data := DataMap{
		Data: make(map[int]string),
		Mux:  &sync.Mutex{},
	}
	data.Data[1] = "1"
	data.Data[2] = "2"
	data.Data[3] = "3"
	data.Data[4] = "4"

	wg := &sync.WaitGroup{}
	wg.Add(1)

	tmpMap := make(map[int]string)
	//复制map, 复制之前加锁
	data.Mux.Lock()
	for k, v := range data.Data {
		tmpMap[k] = v
	}
	data.Mux.Unlock()

	go func() {
		defer wg.Done()
		//因为遍历的是临时复制的map, 所以不用加锁
		for range tmpMap {
			//遍历的同时,具体内部逻辑还要修改map,修改时加锁
			changeMap(data)
		}
	}()

	wg.Wait()
}

结果:

=== RUN   TestRangeMap
--- PASS: TestRangeMap (0.00s)
PASS

Process finished with exit code 0

没有异常了!

后记

map并发是不安全的,加锁需要谨慎注意死锁的问题,range操作是对map的读,同时并发的读写会panic,以上复制的方案只是绕过了同时读写的问题,但是就变成了两个map了,所以需要在具体操作之前校验下数据的正确性(可能已经失效),具体操作的时候,因为是加锁之后再进行的操作,所以相对来说,校验有效性就根据具体逻辑来做就行。

sync.Map 没有length方法真的比较鸡肋, 最起码我现在1.12.7的版本还没有,不知道后面go官方是否会实现这个方法。

Golang 中,遍历 `map` 的主要方式是使用 `for...range` 循环。`map` 是一种无序的键对集合,因此每次遍历的结果顺序可能不一致,这是语言设计上的有意为之,以避免开发者依赖特定顺序[^3]。 ### 基本遍历方式 以下是一个基本的遍历示例: ```go package main import "fmt" func main() { // 创建一个 map myMap := map[string]int{ "apple": 5, "banana": 10, "cherry": 15, } // 使用 for...range 遍历 map for key, value := range myMap { fmt.Printf("Key: %s, Value: %d\n", key, value) } } ``` 在上述代码中,`for key, value := range myMap` 会遍历 `myMap` 中的每个键对,并将键赋给 `key`,给 `value`,然后可以在循环体内进行操作。 ### 遍历键或 如果只需要键或,可以只接收一个变量: ```go // 只遍历键 for key := range myMap { fmt.Printf("Key: %s\n", key) } // 只遍历 for _, value := range myMap { fmt.Printf("Value: %d\n", value) } ``` ### 注意事项 1. **顺序无序性** Golang 中的 `map` 遍历时顺序是不确定的,即使以相同的顺序插入元素,每次遍历的输出顺序也可能不同。这是由于底层实现中 `map` 的结构以及扩容时的重新哈希机制所导致的[^5]。 2. **并发访问问题** 如果在遍历 `map` 的同时,有其他协程对 `map` 进行修改(例如添加或删除键对),程序可能会触发 panic,错误信息为 `fatal error: concurrent map iteration and map write`。为了避免此类问题,建议在并发环境下使用同步机制(如 `sync.Mutex` 或 `sync.RWMutex`)来保护对 `map` 的访问[^4]。 3. **避免死锁** 在使用锁保护 `map` 的同时进行遍历修改时,需要注意避免死锁。例如,遍历过程中加锁可能导致修改操作无法执行,进而引发死锁[^4]。 4. **替代方案** 对于需要并发安全的场景,可以考虑使用 `sync.Map` 类型,它是 Go 1.9 引入的并发安全的 `map` 实现。不过需要注意的是,`sync.Map` 没有直接获取 `map` 长度的方法,因此在某些特定需求下可能不适用[^4]。 ### 示例:并发安全的 map 遍历 以下是一个使用 `sync.RWMutex` 实现并发安全遍历的示例: ```go package main import ( "fmt" "sync" ) func main() { var mu sync.RWMutex myMap := map[string]int{ "apple": 5, "banana": 10, "cherry": 15, } // 并发读取 go func() { mu.RLock() defer mu.RUnlock() for key, value := range myMap { fmt.Printf("Key: %s, Value: %d\n", key, value) } }() // 并发写入 go func() { mu.Lock() defer mu.Unlock() myMap["orange"] = 20 }() } ``` 在这个示例中,使用 `sync.RWMutex` 来控制对 `map` 的并发访问,确保读写操作不会发生冲突。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值