go并发编程 - 锁与并发安全

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()
}

作用

并发场景下,通过锁机制,解决数据竞争的问题

注意事项

  1. 尽量避免使用锁
  2. 应合理使用锁机制,不能滥用

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 进行读写操作,而这可能会引发以下问题:

  1. 竞态条件(Race Condition):当多个 goroutine 同时对一个 map 进行写操作时,由于 map 内部数据结构的修改,可能会导致数据的不一致性或者丢失的问题。

  2. 不确定的迭代器行为:在迭代一个 map 的键值对时,如果其他 goroutine 正在对 map 进行写操作,可能会导致迭代器的行为非确定性,并且可能会导致遍历过程中的崩溃。

  3. 安全性问题:多个 goroutine 对 map 进行读写操作,可能会引发其他的安全问题,如数据竞争、内存访问冲突等。

map的底层实现

Go 语言的 map 内部是通过一个数组和链表(或红黑树)实现的。首先,Go 语言会根据键的哈希值找到对应的存储桶。这个哈希值在存储时通过哈希函数计算得到。每个存储桶中包含一个或多个键值对。

如果有多个键映射到同一个存储桶,那么会通过链表或红黑树来解决哈希碰撞(Hash Collision)的问题。链表用于存储较少的键值对,而红黑树用于存储较多的键值对,以提高查找的效率。

当对 map 进行插入、查找或删除操作时,Go 语言会根据键的哈希值找到对应的存储桶,然后再根据键的值进行比较,以确定具体的键值对位置。对于查找和删除操作,可以根据键的哈希值迅速定位到存储桶,然后在链表或红黑树中进行搜索或删除操作。

需要注意的是,map 的键是无序的,不同的运行时环境下可能会有不同的遍历顺序。因此,在遍历 map 时,不能对元素的顺序做任何假设。

总之,Go 语言的 map 底层实现是一个基于哈希函数、数组和链表(或红黑树)的数据结构,用于快速插入、查找和删除键值对。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值