GO 多线程工具使用和分析

GO 多线程工具使用和分析

Go 语言中的 sync 包提供了一些用于同步和互斥访问共享资源的原语,使用这些可以避免多个goroutine同时访问共享资源时出现的问题,他们有:

  1. 互斥锁
  2. 读写锁
  3. cond
  4. WaitGroup
  5. map
  6. once
  7. pool
  8. atomic

本文介绍它们的使用方式

互斥锁(Mutex)

互斥锁是最基本的同步原语,用于保护共享资源的访问。

在 Go 语言中,可以使用 sync.Mutex 类型来创建一个互斥锁,并使用 LockUnlock 方法分别来进行加锁和解锁操作。并且它不是可重入锁


func main() {
	var (
		count int
		mutex sync.Mutex
	)

	for i := 0; i < 1000; i++ {
		go func() {
			mutex.Lock()
			defer mutex.Unlock()
			count++
		}()
	}
	time.Sleep(time.Second*3)
	fmt.Println(count)
}

不可重入

func main() {
	var lock sync.Mutex
	lock.Lock()
	lock.Lock()
	lock.Unlock()
	lock.Unlock()
}
// output:
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_SemacquireMutex(0xc00008a000?, 0x0?, 0xc000086000?)
	D:/goSdk/go1.19/src/runtime/sema.go:77 +0x25
sync.(*Mutex).lockSlow(0xc00008a000)
	D:/goSdk/go1.19/src/sync/mutex.go:193 +0x165
sync.(*Mutex).Lock(...)
	D:/goSdk/go1.19/src/sync/mutex.go:109
main.main()
	D:/GolandProjects/base_go/synchander/sync.go:8 +0x6c

Process finished with the exit code 2

Mutex两种模式

normal(正常)

一个尝试加锁的goroutine会先自旋几次,尝试通过原子操作获得锁,若几次自旋之后仍不能获得锁,则通过信号量排队等待。(等待队列)
所有等待者会按照先入先出FIFO的顺序排队(信号量的底层是通过park来操作的)。

当锁被释放,第一个等待者被唤醒后并不会直接拥有锁,而是需要和后来者竞争,也就是那些处于自旋阶段,尚未排队等待的goroutine。

这种情况下后来者更有优势,有两个原因:

  1. 它们(处在自旋阶段,尚未排队的goroutine)正在CPU上运行,自然比刚被唤醒的goroutine更有优势。
  2. 处于自旋状态的goroutine可以有很多,而被唤醒的goroutine每次只有一个。

所以被唤醒的goroutine有很大概率拿不到锁。这种情况下它会被重新插入到队列的头部,而不是尾部。

starvation(饥饿)

当锁被释放之后,队列头部的goroutine可获取锁,没有goroutine和它竞争。尝试获取锁的每一个goroutine都会被添加到队列的 尾部。当他们获取锁的时候,mutex处于未加锁的状态。

两种模式的的切换

normal -> starvation

当一个goroutine本次加锁等待时间超过了1ms后,它会把当前Mutex从正常模式切换至“饥饿模式”。

starvation -> normal

当一个等待队列中等待者获取到锁之后,在下面两种情况下,会切换会正常模式

  1. 它等待锁的时间小于1ms(意思是当前goroutine等待的时间不长,也侧面反应当前的抢占锁的情况不强烈)
  2. 等待队列中没有等待的goroutine了。(等待队列已经空了)
数据结构
type Mutex struct {
	state int32 // 状态
	sema  uint32 //等待信号量
}
// 有下面的几种状态
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken  // 唤醒
	mutexStarving //饥饿

源码分析
加锁

在这里插入图片描述

释放锁

在这里插入图片描述

总结
  1. Mutex不是一个可重入锁
  2. 有两种模式,并且两种模式在Mutex运行期间是可以互相切换的。

读写锁(RWMutex)

分为读锁,写锁,并且两者互斥。

它允许多个 goroutine 同时读取共享资源,但只允许一个 goroutine 写入共享资源。

读取和写入被分别表示为 RLockLock 方法。RLock 方法允许多个 goroutine 同时获取读锁,而 Lock 方法则只允许一个 goroutine 获取写锁。当一个 goroutine 获取写锁时,即使已经有其他 goroutine 获取了读锁,也会被阻塞。

关于泛型看:https://blog.csdn.net/nihaihaoma/article/details/125601630

// 下面是利用RWMutex写的goroutine并发安全的map
// 重点在于读取或写入的时候调用的方法不同
package main

import "sync"

type SafeMap[K comparable,T any] struct {
	sync.RWMutex
	m map[K]T
}

func newSafeMap[K comparable,T any](size int) *SafeMap[K,T] {
	return &SafeMap[K,T]{
		m:       make(map[K]T,size),
	}
}

func (sm *SafeMap[K,T]) Get(key K) (T, bool) {
	sm.RLock()
	defer sm.RUnlock()
	value, ok := sm.m[key]
	return value, ok
}

func (sm *SafeMap[K,T]) Set(key K, value T) {
	sm.Lock()
	defer sm.Unlock()
	sm.m[key] = value
}

func (sm *SafeMap[K,T]) Delete(key K) {
	sm.Lock()
	defer sm.Unlock()
	delete(sm.m, key)
}

func (sm *SafeMap[K,T]) Len() int {
	sm.RLock()
	defer sm.RUnlock()
	return len(sm.m)
}
func main() {
	m := newSafeMap[string,string](10)
	m.Set("a","a")
	v, _ := m.Get("a")
	println(v)
}

数据结构

type RWMutex struct {
   w           Mutex  // held if there are pending writers  写操作加锁,互斥锁
   writerSem   uint32 // 写锁goroutine等待队列
   readerSem   uint32 // 读锁goroutine等待队列
   readerCount int32  // 当前持有读锁的goroutine的数量
   readerWait  int32  // 已经释放读锁的goroutine
}

RWMutex 中,读和写操作使用了不同的信号量和计数器,以保证读操作和写操作之间的互斥性。

在读操作中,如果没有等待的写操作,那么读取就会立即进行;

如果有等待的写操作,那么读取就会等待,直到写操作完成。

在写操作中,如果没有其他 goroutine 正在读或写,那么写入就会立即进行;

如果有其他 goroutine 正在读或写,那么写入就会等待,直到所有读操作完成。

最大的读操作goroutine的数量

const rwmutexMaxReaders = 1 << 30 // 2 的 30 次方,

读锁

获取
func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		//加读锁,增加readerCount,readerCount为负数说明已经被获取到了写锁,
		// readerCount为负是在 获取读锁之后,就把readcount -= maxReader

		// 有写锁,当前的goroutine在获取读锁的时候应该等待。
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}
释放
func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
	// 释放读锁,需要readerCount--,
	//readerCount为负数的时候说明有写锁
	// 持有写锁的goroutine readCount -= maxRead
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		// Outlined slow-path to allow the fast-path to be inlined
		rw.rUnlockSlow(r)
	}
	if race.Enabled {
		race.Enable()
	}
}

func (rw *RWMutex) rUnlockSlow(r int32) {
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		fatal("sync: RUnlock of unlocked RWMutex")
	}
     // readerWait是写锁持有期间等待的goroutine的数量
	if atomic.AddInt32(&rw.readerWait, -1) == 0 {
		// The last reader unblocks the writer.
		// 为0 说明读操作已经结束了,需要唤醒在写上park的goroutine
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

写锁

获取
func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// First, resolve competition with other writers.
    // lock之后下面的操作可就是一个goroutine了,剩下的操作就是等待所有的reader的goRoutine结束,写操作开始
	rw.w.Lock()
	// Announce to readers there is a pending writer.
	// cas操作将readCOunt变为负数
	// r就是当前持有的读锁的goRoutine
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// Wait for active readers.
	// 不为0说明当前还有正在持有读锁的goRoutine
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
		// 当前有reader,一直等待所有的持有读锁的goroutine释放掉,才会从这unpark
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}
释放
func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}

	// Announce to readers there is no active writer.
	// 释放的时候将readerCount复原
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	if r >= rwmutexMaxReaders { // 读的goroutine超过了max 报错
		race.Enable()
		fatal("sync: Unlock of unlocked RWMutex")
	}
	// Unblock blocked readers, if any.
	for i := 0; i < int(r); i++ {
		// 唤醒所有的读的goroutine
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// Allow other writers to proceed.
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

总结:

  1. 读操作不加锁,写操作加锁。
  2. 没有所谓的锁降级的概念,也就是先持有写锁,在获取读锁,最后释放写锁,最后在释放先前持有的写锁的过程(Java中ReadWriteLock中就是这个特性,主要是因为java的内存模型影响的,为了保证数据的可见性)
  3. 读锁和写锁是完全互斥的,意思是读锁全部释放之后,写锁才可继续操作,反过来也是。
  4. 写锁的实现是Mutex,他是不可重入的。读锁无所谓。

cond

这个和Java中的wait notify一样

提供了一种等待和通知的机制,可以用于协调多个 goroutine 的执行顺序。

在使用 cond 进行等待和通知时,通常需要与 mutex 配合使用,以确保并发安全。

当一个 goroutine 需要等待某个条件时,可以调用 cond.Wait() 方法,这个方法会释放当前的锁,并进入等待状态,直到其他 goroutine 通过调用 cond.Signal()cond.Broadcast() 方法来唤醒它。

当条件满足时,可以通过调用 cond.Signal()cond.Broadcast() 方法来通知其他等待的 goroutine 继续执行。

需要注意,在使用的时候必须是同一个lock。Java中是在任意一个对象上面wait的,wait notify也是Java面试很常见的了。

// 这是一个很简单的标志位退出的代码,两个goroutine,当isSuccess为true,退出
func main() {
	var lock sync.Mutex
	cond := sync.NewCond(&lock)
	isSuccess := false

	go func() {
		lock.Lock()
		defer lock.Unlock()
		for{
			if isSuccess{
				println("success1")
				return
			}
			cond.Wait()
		}
	}()

	go func() {
		lock.Lock()
		defer lock.Unlock()
		for{
			if isSuccess{
				println("success2")
				return
			}
			cond.Wait()
		}
	}()


	time.Sleep(1*time.Second)
	lock.Lock()
	println("main set value begin")
	isSuccess = true
	println("main set value end")

	cond.Broadcast() // 唤醒所有,Signal只唤醒一个
	lock.Unlock()

	time.Sleep(time.Hour)
	
}

重温一下生产者消费者的经典写法

package main

import (
	"math/rand"
	"sync"
	"time"
)

type Collection interface {
	offer(data int)
	poll() int
}
type SafeQueue struct {
	data []int
	lock *sync.Mutex // 这里需要注意,必须是指针,如果不是指针的话,这里就和cond里面的mutex不一样了,出现问题了。
	pCond *sync.Cond // 生产者等待队列
	cCond *sync.Cond // 消费者等待队列
	cap int
	size int
}

func newSafeQueue(cap int) *SafeQueue {
	var m sync.Mutex
	return &SafeQueue{
		data:  make([]int,cap),
		lock:  &m,
		pCond: sync.NewCond(&m),
		cCond: sync.NewCond(&m),
		cap:   cap,
		size:  0,
	}
}

func (q *SafeQueue) offer(data int) {
	q.lock.Lock()
	defer q.lock.Unlock()

	if q.size ==q.cap{
		println(data,"wait")
		q.pCond.Wait()
	}
	q.data[q.size] = data
	q.size++
	println("offer",data,"size",q.size)
	q.cCond.Signal()
}

func (q *SafeQueue) poll() int {
	q.lock.Lock()
	defer q.lock.Unlock()
	if q.size == 0 {
		println("poll wait")
		q.cCond.Wait()
	}
	q.size--
	res := q.data[q.size]
	println("poll ",res,"size",q.size)
	q.pCond.Signal()
	return res
}

func main() {
	queue := newSafeQueue(10)

	go func() {
		for  {
			queue.offer(rand.Int())
			time.Sleep(time.Second)
		}
	}()

	go func() {
		for {
			queue.poll()
		}
	}()
	time.Sleep(time.Hour)
}
// 用channel改写一下
// 所以channel很牛皮,就这么一点代码,完成了上面的功能
func main() {
	ints := make(chan int, 10)

	go func() {
		for {
			ints<-rand.Int()
			time.Sleep(time.Second)
		}
	}()

	for i := range ints {
		println(i)
	}
}

WaitGroup

相当于Java中CountDownlatch

它可以用于等待一组 goroutine 的结束。WaitGroup 维护了一个计数器,表示还有多少个 goroutine 没有结束,当计数器归零时,表示所有的 goroutine 都已经结束,可以继续执行后面的代码。

WaitGroup 提供了三个方法:

  • Add(delta int):将计数器加上 delta,表示有 delta 个 goroutine 需要等待。
  • Done():将计数器减去 1,表示一个 goroutine 已经结束。
  • Wait():等待计数器归零,即所有的 goroutine 都已经结束。

需要注意

  1. WaitGroup可以连续add,和Java中的countdown latch不一样。后者只能是在创建的时候指定,并且只能一次
  2. WaitGroup可以连续用,在Wait触发之后,计数器归0,重新开始。
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(10) // 等待10个goroutine结束
	for i := 0; i < 10; i++ {
		go func(i int) {
			defer wg.Done()
			fmt.Println("goroutine ", i, " done")
		}(i)
	}

	wg.Add(1) // 在wait之前还可以继续加,这里表示等待11个goroutine结束
	go func(i int) {
		defer wg.Done()
		fmt.Println("goroutine ", i, " done")
	}(11)

	wg.Wait()
	
	// wait触发之后,还可以继续用,只不过这个时候从0开始了
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			fmt.Println("goroutine ", i, " done")
		}(i)
	}

	wg.Wait()

	fmt.Println("all done")
}

Map

一个并发安全的 Map 类型。该类型被设计为用于在多个 goroutine 之间共享键值对,并且支持并发读取和写入操作。

sync.Map 类型内部使用了读写锁和哈希表实现。

它的主要特点是在写入时不会阻塞读取操作,因为写入操作会将数据存储在一个 “脏(dirty)” Map 中,而读取操作则始终从一个 “干净(read)” Map 中读取数据。当 “脏” Map 中的数据量达到一定阈值时,该 Map 会被转换为 “干净” Map,以便读取操作可以更快地访问数据。

与标准库中的 map 类型不同,sync.Map 不支持使用下标语法来访问数据。相反,它提供了 Load、Store、LoadOrStore、Delete 和 Range 等方法来操作数据。这些方法的使用方式与标准库中的 map 类型类似,但是需要传递一个接口类型的键作为参数,而不是直接使用下标。

sync.Map不需要make,声明就可使用

需要注意的是,sync.Map 中的键和值必须是可比较的类型,否则会在运行时引发 panic。sync.Map的性能肯定是比不上map的,支持在某些情形下使用,因此在选择使用它时需要根据具体情况进行评估。

结构

在这里插入图片描述

once

可以保证在程序运行期间,某个函数只会被执行一次。Once通常被用来实现单例模式或者延迟初始化。

Once只有一个Do方法,该方法接收一个函数作为参数,当且仅当该函数第一次被调用时才会执行。Once会确保这个函数在程序运行期间只会被执行一次,即使在多个Go协程中同时调用Do方法也是安全的。Once使用了一个互斥锁和一个标志位来实现这个功能。

单例模式


import (
	"fmt"
	"sync"
)

type Singleton struct {
	name string
}

var (
	once     sync.Once
	instance *Singleton
)

func NewSingleton() *Singleton {
	once.Do(func() {
		instance = &Singleton{name: "MySingleton"}
	})
	return instance
}

func main() {
	s1 := NewSingleton()
	s2 := NewSingleton()
	fmt.Println(s1 == s2) // true
}

源码分析

在这里插入图片描述

pool

用于管理可重用的临时对象。Pool通常被用来避免在高并发场景下频繁地分配和释放内存,从而提高程序的性能。

对于很多需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。频繁地分配、回收内存会给 GC 带来一定的负担,而 sync.Pool 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。

有两个方法:

  1. Get

    Get方法用于获取一个可重用的对象,如果Pool中已经有可重用的对象,则返回其中一个。否则,会调用New方法创建一个新的对象并返回

  2. Put。

    Put方法用于将一个对象放回Pool中,以便下次被重用。需要注意:对象的属性并不会清空。建议在put的时候先清空属性,在放进去。

此外还需要通过New来指定加载方法。

package main

import (
	"fmt"
	"sync"
)

type MyObject struct {
	name string
}

func NewMyObject(name string) *MyObject {
	return &MyObject{name: name}
}

func main() {
	pool := sync.Pool{
		New: func() interface{} {
			fmt.Println("Creating new object")
			return NewMyObject("MyObject")
		},
	}

	obj1 := pool.Get().(*MyObject) // 第一次加载,池中没有对象,会调用到New,创建一个对象
	fmt.Println(obj1.name)

	pool.Put(obj1) // 放进去

	obj2 := pool.Get().(*MyObject) // 再次加载,不会在创建对象了,但可以看到,对象的属性并没有清空
	fmt.Println(obj2.name)
}

这篇博客对pool分析的很到位:https://qcrao.com/post/dive-into-go-sync-pool/

atomic

atomic是原子操作。 原子操作就是不可中断的操作。

在实际使用的时候和循环配合的比较紧密。cas操作嘛

主要分为两大类

  1. 操作类
  2. Value(存储值)

操作类

按照不同的类型,主要分为下面的几个操作

  • 增减操作(AddXXType):保证对操作数的原子增减;
  • 载入操作(LoadXXType):保证读取到操作数前,没有其他routine对其进行更改操作;
  • 存储操作(StoreXXType):保证存储时的原子性(避免被其他线程读取到修改一半的数据);
  • 比较并交互操作(CompareAndSwapXXType):保证交换的CAS,只有原有值没被更改时才会交换;
  • 交换操作(SwapXXType):直接交换,不关心原有值。

这里举个例子

按照顺序打印1,2,3,4,5.多个goroutine实现,用无锁的方法实现

package main

import (
	"sync"
	"sync/atomic"
)

var (
	idx int32
   group sync.WaitGroup // 利用waitGroup做同步
)

// expect 想要设置的值
// next 设置的值
// 这里的思想是看idx是否是自己想要的值,如果是设置为下一个值,然后结束循环,否则一直在循环
func s (expect,next int32) {
	for{
		if  atomic.LoadInt32(&idx) == expect{
			println(expect)
			atomic.StoreInt32(&idx,next) // 将idx设置为新的值
			group.Done()
			return
		}
	}

}

func main() {
	for i := 0; i < 10; i++ {
		group.Add(1)
		go s(int32(i), int32(i+1))
	}
	group.Wait()
}

将上面的例子改为用channel实现

思路:让这一个goroutine等待上一个goroutine结束,用无缓存的channel做同步

// 1. 先写这个,写等待此channel里面的数据。接下来看怎么做channel同步
func s(inputC,outputC chan struct{},i int)  {
	<-inputC
	println(i)
	if outputC == nil{
		group.Done()
	}
	outputC<- struct{}{}
	group.Done()
}

func main() {
    // 这里的思路就是一个goroutine从一个channel中获取数据,在从另一个channel中写数据
	cList := make([]chan struct{}, 0, 5)
	for i := 0; i < 5; i++ {
		cList = append(cList, make(chan struct{}))
	}
	for idx := range cList {
		group.Add(1)
		if idx == 4{
			go s(cList[idx],nil,idx)
			continue
		}
		go s(cList[idx],cList[idx+1],idx)
	}
	cList[0]<- struct{}{}
	group.Wait()
}

存储

sync/atomic Value表示的是一个原子引用。配合Load,Store来原子的操作它。在go的Context包中使用了它。如下

在这里插入图片描述

关于atomic可看:https://blog.csdn.net/alwaysrun/article/details/125023283


到这里,文章结束了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值