Mutex 与 RWMutex
Mutex(互斥锁)和RWMutex(读写互斥锁)都是常用于并发编程的同步原语,用于控制多个线程对共享资源的访问。
Mutex是一种排他锁,它提供了独占访问共享资源的能力。当一个线程获取到Mutex后,其他线程就无法获得这个Mutex,只能等待当前线程释放Mutex。这样可以确保同一时间只有一个线程访问共享资源,从而避免竞争条件。
RWMutex是一种读写锁,它允许多个线程同时读取共享资源,但在有线程进行写操作时,其他线程无论是读还是写都需要等待。这样可以提高读多写少场景下的并发性能,因为多个线程可以并行地读取资源。
RWMutex锁分为读锁(RLock)和写锁(Lock)。当一个线程获取到写锁后,其他线程无法获得读锁或写锁,直到写锁被释放。当一个线程获取到读锁后,其他线程还可以获取读锁,但不能获取写锁,只有当所有读锁都被释放后,写锁才能被获取。
协程操作问题
单协程操作
// 单协程操作
func singleRoutine() {
mp := make(map[string]int, 0)
list := []string{"A", "B", "C", "D"}
for i := 0; i < 20; i++ {
for _, item := range list {
_, ok := mp[item]
if !ok {
mp[item] = 0
}
mp[item] += 1
}
}
fmt.Println(mp)
}
多协程操作, 非协程安全 , 对map读写出现并发问题
// 多协程操作, 非协程安全 , 对map读写出现并发问题
func multipleRoutine() {
mp := make(map[string]int, 0)
list := []string{"A", "B", "C", "D"}
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for _, item := range list {
_, ok := mp[item]
if !ok {
mp[item] = 0
}
mp[item] += 1
}
}()
}
wg.Wait()
fmt.Println(mp)
}
互斥锁协程安全
// 互斥锁协程安全
func multipleSafeRoutine() {
type safeMap struct {
data map[string]int
sync.Mutex //加锁进行操作,其他协程将进行等待,直到锁被释放掉
}
mp := safeMap{
data: make(map[string]int, 0),
Mutex: sync.Mutex{},
}
list := []string{"A", "B", "C", "D"}
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mp.Lock()
defer mp.Unlock() //对map进行解锁,允许其他进程操作
for _, item := range list {
_, ok := mp.data[item]
if !ok {
mp.data[item] = 0
}
mp.data[item] += 1
}
}()
}
wg.Wait()
fmt.Println(mp)
}
读写锁
type cache struct {
data map[string]string
sync.RWMutex
}
func newCache() *cache {
return &cache{
data: make(map[string]string, 0),
RWMutex: sync.RWMutex{},
}
}
Get 获取方法
// Get 获取方法
func (c *cache) Get(key string) string {
c.RLock()
defer c.RUnlock() //读锁和读锁不会产生互斥
value, ok := c.data[key]
if ok {
return value
}
return ""
}
Set 设置值
// Set 设置值
func (c *cache) Set(key, value string) {
c.Lock()
defer c.Unlock() //写锁与写锁之间会产生互斥
c.data[key] = value
}
读写锁
// 读写锁
func multipleSafeRoutineByRWMutex() {
c := newCache()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
c.Set("name", "nick")
}()
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(c.Get("name"))
}()
}
wg.Wait()
}
作用
并发场景下,通过锁机制,解决数据竞争的问题
注意事项
- 尽量避免使用锁
- 应合理使用锁机制,不能滥用
sync.Map
sync.Map 是Go语言中提供的并发安全的映射(map)类型。与普通的 map 不同,sync.Map 可以在多个 goroutine 并发读写而无需额外的锁。它的设计目的是提供一种高效的并发安全的 map 实现,而不需要手动管理锁。
使用 sync.Map 时,可以通过调用 Load、Store、Delete 和 LoadOrStore 来读写操作其中的键值对。这些操作是并发安全的,意味着可以在多个 goroutine 中同时操作同一个 sync.Map 实例而不会导致数据不一致或竞争条件的问题。
最重要的一点是,sync.Map 并没有提供遍历所有键值对的功能。这是为了避免在并发环境下需要加锁操作,因为在遍历时 map 的内容可能会发生变化。如果需要遍历所有的键值对,可以使用 Range 方法,该方法接受一个函数作为参数,在遍历过程中回调该函数处理每一个键值对。
需要注意的是,sync.Map 并不适用于所有的场景,因为由于其内部的实现机制,它可能会比普通的 map 操作费时更长。因此在一些高度竞争的场景中,或者需要对键值对进行频繁修改和遍历的场景中,可能需要考虑使用其他的同步机制,比如使用互斥锁(Mutex)或读写互斥锁(RWMutex)来保证并发安全。
package _case
import (
"fmt"
"sync"
)
func MapCase() {
mp := sync.Map{}
//设置键值对
mp.Store("name", "nick")
mp.Store("email", "qq.com")
//通过key获取value
fmt.Println(mp.Load("name")) //nick true
fmt.Println(mp.Load("email")) //qq.com true
// 通过key获取value, 如果不存在则设置指定的value并返回
// ok 为true表示key存在并返回值,为 false表示key 不存在并设置后返回
fmt.Println(mp.LoadOrStore("hobby", "篮球")) //篮球 false
fmt.Println(mp.LoadOrStore("hobby", "羽毛球")) //篮球 true
// 根据key获取value,删除该key
// ok 为 true表示key存在, 为false表示key, 不存在
fmt.Println(mp.LoadAndDelete("hobby")) //篮球 true
fmt.Println(mp.LoadAndDelete("hobby")) //<nil> false
mp.Range(func(key, value any) bool {
fmt.Println(key, value)
return true
})
//email qq.com
//name nick
}
使用sync.map实现并发安全
func MapCase1() {
mp := sync.Map{}
list := []string{"A", "B", "C", "D"}
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for _, item := range list {
value, ok := mp.Load(item)
if !ok {
value, _ = mp.LoadOrStore(item, 0)
}
val := 0
val = value.(int)
val += 1
mp.Store(item, val)
}
}()
}
wg.Wait()
fmt.Println(mp)
}
为什么map不是并发安全
Map 不是并发安全的主要原因是它的底层实现是非并发安全的。在并发环境下,多个 goroutine 可能同时对同一个 map 进行读写操作,而这可能会引发以下问题:
-
竞态条件(Race Condition):当多个 goroutine 同时对一个 map 进行写操作时,由于 map 内部数据结构的修改,可能会导致数据的不一致性或者丢失的问题。
-
不确定的迭代器行为:在迭代一个 map 的键值对时,如果其他 goroutine 正在对 map 进行写操作,可能会导致迭代器的行为非确定性,并且可能会导致遍历过程中的崩溃。
-
安全性问题:多个 goroutine 对 map 进行读写操作,可能会引发其他的安全问题,如数据竞争、内存访问冲突等。
map的底层实现
Go 语言的 map 内部是通过一个数组和链表(或红黑树)实现的。首先,Go 语言会根据键的哈希值找到对应的存储桶。这个哈希值在存储时通过哈希函数计算得到。每个存储桶中包含一个或多个键值对。
如果有多个键映射到同一个存储桶,那么会通过链表或红黑树来解决哈希碰撞(Hash Collision)的问题。链表用于存储较少的键值对,而红黑树用于存储较多的键值对,以提高查找的效率。
当对 map 进行插入、查找或删除操作时,Go 语言会根据键的哈希值找到对应的存储桶,然后再根据键的值进行比较,以确定具体的键值对位置。对于查找和删除操作,可以根据键的哈希值迅速定位到存储桶,然后在链表或红黑树中进行搜索或删除操作。
需要注意的是,map 的键是无序的,不同的运行时环境下可能会有不同的遍历顺序。因此,在遍历 map 时,不能对元素的顺序做任何假设。
总之,Go 语言的 map 底层实现是一个基于哈希函数、数组和链表(或红黑树)的数据结构,用于快速插入、查找和删除键值对。