golang控制并发的方式有两种

  • waitGroup
  • Context

    Sync包

    WaitGroup

    多个goroutine执行同一件事情
  1. var x = 0
  2. var wg sync.WaitGroup
  3. func add(){
  4. for i:=0;i<5000;i++{
  5. x=x+1
  6. }
  7. wg.done()
  8. }
  9. func main() {
  10. wg.add(2)
  11. go add()
  12. go add()
  13. wg.Wait()
  14. }

以上这种处理线程不安全,会出现每次执行的结果不同,需要使用锁来实现

互斥锁

互斥锁是一种常用的控制狗共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。
并发 - 图1

  • Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。
  • Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。
  • Starving:表示该Mutex是否处理饥饿状态, 0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms。
  • Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。

协程之间抢锁实际上是抢给Locked赋值的权利,能给Locked域置1,就说明抢锁成功。抢不到的话就阻塞等待Mutex.sema信号量,一旦持有锁的协程解锁,等待的协程会依次被唤醒。

加锁后,锁被其他协程占用时
并发 - 图2
waiter计数器会增加1,这时候它阻塞,知道locked变为0才可以唤醒(这是一种自旋锁)

加锁后要立即使用defer对其解锁,可以有效避免死锁

  1. var x = 0
  2. var wg sync.WaitGroup
  3. var lock sync.Mutex
  4. func add(){
  5. for i:=0;i<5000;i++{
  6. lock.Lock()
  7. x=x+1
  8. lock.Unlock()
  9. }
  10. wg.done()
  11. }
  12. func main() {
  13. wg.add(2)
  14. go add()
  15. go add()
  16. wg.Wait()
  17. }

读写互斥锁

互斥锁是完全互斥的,但很多情况下都是读多写少的。这时候可以使用读写互斥锁RWMutex

sync.Map

map在goroutine中使用是不安全的,因此考虑使用sync.Map

  1. sync.Map的核心实现 - 两个map,一个用于写,一个用于读,这样的设计思路可以类比缓存与数据库
  2. sync.Map的局限性 - 如果写远高于读,dirty -> readOnly 这个类似于刷数据的频率就比较高,不如直接用mutex + map的组合
  3. sync.Map的设计思路,保证高频率读的无锁结构,空间换时间 ```go var m sync.Map

// 保存kv m.Store(“a”,1)

// 加载key b := m.Load(“a”)

// 有key就加载,没有key就保存 c := m.LoadOrStore(“a”,1)

// 删除kv d := m.Delete(“a”)

// 遍历,遇到false就退出 m.Range(func(key, value interface{}) bool { fmt.Println(key, value) return false })

  1. <a name="JYss8"></a>
  2. # Once
  3. 在某些场景下确保高并发时只执行一次操作,比如只加载一次配置文件,只关闭一次通道等。
  4. <a name="HYkfv"></a>
  5. ## sync.Map
  6. go语言中的map不是并发安全的,要使用sync.map()
  7. <a name="Context"></a>
  8. # Context
  9. 用于处理有多个`goroutine`或`goroutine`里有`goroutine`
  10. ```bash
  11. ctx, cancel := context.WithCancel(context.Background())
  12. go worker(ctx, "job1")
  13. go worker(ctx, "job2")
  14. go worker(ctx, "job3")
  15. cancel()

多路复用select

某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会阻塞。虽然可以使用死循环尝试取值,但性能并不是很好。于是go内置了select关键字,可以同时相应多个通道的操作。

select原理:
IO多路复用,每个线程或者进程都注册,然后阻塞,当注册的线程或进程准备好数据后,会去取值,无需在对额外的线程或进程进行管理

golang的case语句是以下结构体

  1. type scase struct {
  2. c *hchan // chan
  3. elem unsafe.Pointer // 读或者写的缓冲区地址
  4. kind uint16 //case语句的类型,是default、传值写数据(channel <-) 还是 取值读数据(<- channel)
  5. pc uintptr // race pc (for race detector / msan)
  6. releasetime int64
  7. }

其中hchan是channel的指针,在一个select中,所有case语句会构成一个scase结构体的数组
并发 - 图3
然后select语句就是调用func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, nool)函数
并发 - 图4

  • cas0 为上文提到的case语句抽象出的结构体scase数组的第一个元素地址
  • order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列lockorder。
  • nncases表示scase数组的长度

执行结果相当于,函数传入case语句,返回值返回一个选中的case语句。它相当于打乱所有case的实例,然后锁住channel,遍历所有channel看有无可读或可写,如果有,解锁所有channel,返回选中channel数据,如果没有,也没有default,则该goroutine进入阻塞,等待唤醒

atomic

原子处理,自动加锁(对于比较常用的操作)

读取,写入,修改,交换,比较并交换(汇编操作)