用Go语言实现简单的自旋锁

一、锁的方案简介

锁是并发编程中的同步原语,他可以保证多线程在访问同一片内存时不会出现竞争来保证并发安全。对于获取锁,一般来讲有两种方案,一种是不断地自旋+CAS,另一种就是阻塞+唤醒。两种方式各有优劣。Go语言结合了这两种方案,自动的判断当前锁的竞争情况,先尝试自旋几次,如果锁一直没被释放,再加入阻塞队列。

锁竞争方案优势劣势适用场景
阻塞/唤醒精准打击,不浪费 CPU 时间片需要挂起协程,进行上下文切换,操作较重并发竞争激烈的场景
自旋+CAS无需阻塞协程,短期来看操作较轻长时间争而不得,会浪费 CPU 时间片并发竞争强度低的场景

此外,Go语言的锁还支持正常模式和饥饿模式,因此整体的实现有一定的复杂性。下面我们依据自旋锁的原理来实现一版简单的互斥锁,以便对锁的实现有一个初步的理解。

 

二、自旋锁实现(Golang版本)

从自旋锁的定义可知,要想实现自旋锁初步需要关注四个地方:加锁、解锁、锁的状态值、循环尝试获取锁。主干流程:

  • 通过锁内一个状态值标识锁的状态,例如,取 0 表示未加锁,1 表示已加锁;

  • 上锁:把 0 改为 1;

  • 解锁:把 1 置为 0.

  • 上锁时,假若已经是 1,则上锁失败,需要等他人解锁,将状态改为 0.

代码实现

package mutex

import (
	"runtime"
	"sync/atomic"
)

type Mutex struct {
	state int32
}

const (
	mutexLocked = 1
)

// 自旋条件如下:GOMAXPROCS>1,否则会死锁
func canSpin() bool {
	if runtime.GOMAXPROCS(0) <= 1 {
		return false
	}
	return true
}

//让协程忙等待
func doSpin() {
	for i, j := 0, 0; i < 30; i++ {
		j = j + 2
	}
	return
}

func (m *Mutex) Lock() {
	//通过原子操作加锁
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		return
	}

	for ;m.state&mutexLocked == mutexLocked && canSpin(); { // 满足自旋条件
		doSpin()
		if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
			return
		}
	}
	panic("cant lock with one core cpu!")
}

func (m *Mutex) Unlock() {
	//通过原子操作解锁
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new == 0 {
		return
	}

	//解锁时倘若发现 Mutex 此前未加锁,直接panic
	if (new+mutexLocked)&mutexLocked == 0 {
		panic("unlock of unlocked mutex")
	}

}

三、运行测试

此处我们通过一个经典的例子来对锁的性能进行验证:

package main

import (
	"fmt"
	"mySpinMutex/mutex"
	"sync"
)

var count int
var mu mutex.Mutex
var wg sync.WaitGroup

func increment() {
	mu.Lock()
	count++
	mu.Unlock()
	wg.Done()
}
func main() {
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go increment()
	}
	wg.Wait()
	fmt.Println("Final count:", count)
}

运行结果:

c0a42cc18d204df9a37a39cdb1f19a95.png

四、结语

到此自旋锁就的功能便实现完成了,但还有些点值得我们去思考:

1、在 Go 1.18 中,为 sync.Mutex 新增了一个新的方法 TryLock(),它是一种非阻塞模式的取锁操作。当调用 TryLock() 时,该函数仅简单地返回 true 或者 false,代表是否加锁成功。它的应用场景并不常见,并且也不被鼓励使用。那么使用TryLock()会产生什么并发的问题呢?

2、当协程A 自旋锁获取成功后,协程B尝试获取锁,但在协程A持有锁的阶段,协程B一直失败,此时协程B很可能会陷入饥饿状态,如何避免此种情况发生?

3、对于Go来说有内置的协程调度器GMP,对于协程B来说,如果协程B为了获取锁一直自旋,那协程B的本地队列里的其他协程就无法执行。如何在使用锁的同时不影响协程的调度效率呢?

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
自旋锁是AUTOSAR操作系统中的一种同步机制,用于保护临界区资源,确保多个任务或中断之间的互斥访问。AUTOSAR OS中的自旋锁具有一些特殊的功能和限制。 根据引用,AUTOSAR OS在同一个内核上避免了自旋锁造成的死锁问题。当一个任务或中断占用自旋锁时,AUTOSAR OS会自动挂起所有中断,不会被同一内核上的其他任务或中断抢占。然而,如果核间任务嵌套请求占用自旋锁,就有可能导致任务的相互锁死。为了避免这种情况,AUTOSAR建议在系统设计时禁止回环嵌套使用自旋锁,或者在需要嵌套使用自旋锁时,严格按照顺序请求自旋锁。 另外,根据引用,长时间占用自旋锁的任务可能会导致其他内核资源的浪费,因此不建议使用自旋锁来保护时间较长的任务。 总结来说,自旋锁是AUTOSAR操作系统中的一种同步机制,用于保护临界区资源。AUTOSAR OS通过禁止回环嵌套和严格按照顺序请求自旋锁来避免死锁问题。同时,长时间占用自旋锁的任务可能会导致其他内核资源浪费,因此应该谨慎使用自旋锁。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [AUTOSAR多核操作系统(一)](https://blog.csdn.net/m0_51456295/article/details/116333521)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [golang 自旋锁实现](https://download.csdn.net/download/weixin_38669091/12865982)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值