Go-同步原语

本文详细介绍了Go语言中的同步原语,包括sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once和sync.Cond的使用,以及sync.Pool的原理和应用。通过这些原语,可以有效地管理并发状态下的资源冲突,实现线程安全的资源访问。
摘要由CSDN通过智能技术生成

一、同步原语 - Sync

这些基本原语提高了较为基础的同步功能,但是它们是一种相对原始的同步机制,在多数情况下,我们都应该使用抽象层级的更高的 Channel 实现同步。

1-1 并发状态下的资源冲突

由于引用传递,在并发状态下的数据资源获取无序,导致最终结果重复或者错误。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
   
    var a = 0

    for i := 0; i < 10; i++ {
   
        go func(idx int) {
   
            a += 1
            fmt.Printf("goroutine %d, a=%d\n", idx, a)
        }(i)
    }

    // 等待 1s 结束主程序 确保所有协程执行完
    time.Sleep(time.Second)
}
/*
goroutine 2, a=4
goroutine 0, a=1
goroutine 4, a=2
goroutine 3, a=10
goroutine 5, a=8
goroutine 8, a=5
goroutine 6, a=9
goroutine 1, a=3
goroutine 7, a=7
goroutine 9, a=7
*/

二、 sync.Mutex - 互斥锁

一份互斥锁对一个资源加锁,只能同时被一个 goroutine 锁定,其他 goroutine 阻塞等待资源释放。

注意:对一个未锁定的互斥锁解锁,会抛错;首次使用后不能复制复制该互斥锁

2-1 锁结构

type Mutex struct {
   
	state int32
	sema  uint32
}
  • state:互斥锁的当前状态
    • mutexLocked — 表示互斥锁的锁定状态;
    • mutexWoken — 表示从正常模式被从唤醒;
    • mutexStarving — 当前的互斥锁进入饥饿状态;
    • waitersCount — 当前互斥锁上等待的 Goroutine 个数;
  • sema:控制锁状态
    • 正常模式 - 锁的等待者会按照先进先出的顺序获取资源。
      • 刚被唤起的 goroutine 与新建的 goroutine 竞争资源的时,大概率竞争失败而无法获取锁资源,所以若 goroutine 超过 1ms 没有获取到锁,互斥锁会自动被切换成饥饿模式,防止 goroutine 没有资源。
    • 饥饿模式 - 互斥锁会直接将资源交给等待队列最前的 goroutine,新创建的 goroutine 会被至于等待最尾端。目的是为了确保互斥锁的公平性。
      • 若一个 goroutine 获得了互斥锁,并且它在队列尾端,或者等待的时间少于 1ms ,则互斥锁会自动切换成正常模式
      • 饥饿模式可以有效的避免 goroutine 陷入由于等待无法获取锁的高危延时。

2-2 应用举例

2-2-1 资源的有序化

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
   
	var a = 0

	var lock sync.Mutex
	for i := 0; i < 10; i++ {
   
		go func(idx int) {
   
			lock.Lock()
			defer lock.Unlock()
			a += 1
			fmt.Printf("goroutine %d, a=%d\n", idx, a)
		}(i)
	}

	// 等待 1s 结束主程序
	// 确保所有协程执行完
	time.Sleep(time.Second)
}
/*
goroutine 0, a=1
goroutine 7, a=2
goroutine 5, a=3
goroutine 1, a=4
goroutine 2, a=5
goroutine 3, a=6
goroutine 4, a=7
goroutine 8, a=8
goroutine 9, a=9
goroutine 6, a=10
*/

2-2-2 资源的占有

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
   
	ch := make(chan struct{
   }, 2)

	var l sync.Mutex
	go func() {
   
		l.Lock()
		defer l.Unlock()
		fmt.Println("goroutine1: 锁定 2s")
		time.Sleep(time.Second * 2)
		fmt.Println("goroutine1: 解锁资源")
		ch <- struct{
   }{
   }
	}()

	go func() {
   
		fmt.Println("goroutine2: 等待解锁")
		l.Lock()
		defer l.Unlock()
		fmt.Println("goroutine2: 获取资源,锁定资源")
		ch <- struct{
   }{
   }
	}()

	// 等待 goroutine 执行结束
	for i := 0; i < 2; i++ {
   
		<-ch
	}
}
/*
goroutine2: 等待解锁
goroutine1: 锁定 2s
goroutine1: 解锁资源
goroutine2: 获取资源,锁定资源
*/

2-3 互斥锁总结

加锁过程

  • 如果互斥锁处于初始化状态,就会直接通过置位 mutexLocked 加锁;
  • 如果互斥锁处于 mutexLocked 并且在普通模式下工作,就会进入自旋,执行 30 次 PAUSE 指令消耗 CPU 时间等待锁的释放;
  • 如果当前 Goroutine 等待锁的时间超过了 1ms,互斥锁就会切换到饥饿模式
  • 互斥锁在正常情况下会通过 sync.runtime_SemacquireMutex 函数将尝试获取锁的 Goroutine 切换至休眠状态,等待锁的持有者唤醒当前 Goroutine;
  • 如果当前 Goroutine 是互斥锁上的最后一个等待的协程或者等待的时间小于 1ms,当前 Goroutine 会将互斥锁切换回正常模式

解锁过程

  • 当互斥锁已经被解锁时,那么调用 sync.Mutex.Unlock 会直接抛出异常;
  • 当互斥锁处于饥饿模式时,会直接将锁的所有权交给队列中的下一个等待者,等待者会负责设置 mutexLocked 标志位;
  • 当互斥锁处于普通模式时,如果没有 Goroutine 等待锁的释放或者已经有被唤醒的 Goroutine 获得了锁,就会直接返回;在其他情况下会通过 sync.runtime_Semrelease 唤醒对应的 Goroutine;

三、sync.RWMutex - 读写互斥锁

读写互斥锁是细粒度的互斥锁,不限制资源的并发读,但是可以对 读、写 操作进行锁定。

通常在大量读操作,少量写操作的业务场景下提高服务的性能。进行读写资源的操作分离,提高服务的性能。

3-1 锁结构

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值