Golang的锁机制与使用技巧小结

目录
  • 1. sync.Mutex详解
  • 2. RWMutex详解
  • 3. sync.Map详解
  • 4. 原子操作 atomic.Value
  • 5. 使用小技巧

1. sync.Mutex详解

sync.Mutex是Go中的互斥锁,通过.lock()方法上锁,.unlock()方法解锁。需要注意的是,因为Go函数值传递的特点,sync.Mutex通过函数传递时,会进行一次拷贝,所以传递过去的锁是一把全新的锁,大家在使用时要注意这一点,另外sync.Mutex是非重入锁,这一点要与Java中的锁区分。

type Mutex {
    state int32
    sema  uint32
}

上面数据结构中的state最低三位分别表示 mutexLocked、mutexWoken 和 mutexStarving,剩下的位置用来表示当前有多少个 Goroutine 等待互斥锁的释放:

32                                               3             2             1             0
 |                                               |             |             |             |
 |                                               |             |             |             |
 v-----------------------------------------------v-------------v-------------v-------------+
 |                                               |             |             |             v
 |                 waitersCount                  |mutexStarving| mutexWoken  | mutexLocked |
 |                                               |             |             |             |
 +-----------------------------------------------+-------------+-------------+-------------+                                                                                                              
  • mutexLocked — 表示互斥锁的锁定状态;
  • mutexWoken — 表示从正常模式被从唤醒;
  • mutexStarving — 当前的互斥锁进入饥饿状态;
  • waitersCount — 当前互斥锁上等待的 goroutine 个数;

2. RWMutex详解

type RWMutex struct {
	w           Mutex  // 复用互斥锁
	writerSem   uint32 // 写锁监听读锁释放的信号量
	readerSem   uint32 // 读锁监听写锁释放的信号量
	readerCount int32  // 当前正在执行读操作的数量
	readerWait  int32  // 当写操作被阻塞时,需要等待读操作完成的个数
}
  • 读操作如何防止并发读写问题的?

RLock(): 申请读锁,每次执行此函数后,会对readerCount++,此时当有写操作执行Lock()时会判断readerCount>0,就会阻塞。

RUnLock(): 解除读锁,执行readerCount–,释放信号量唤醒等待写操作的goroutine。

  • 写操作如何防止并发读写、并发写写问题?

Lock(): 申请写锁,获取互斥锁,此时会阻塞其他的写操作。并将readerCount 置为 -1,当有读操作进来,发现readerCount = -1, 即知道有写操作在进行,阻塞。

Unlock(): 解除写锁,会先通知所有阻塞的读操作goroutine,然后才会释放持有的互斥锁。

  • 写操作的饥饿问题?

这是由于写操作要等待读操作结束后才可以获得锁,而写操作在等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能会一直阻塞,这种现象称之为写操作被饿死。

通过RWMutex结构体中的readerWait属性可完美解决这个问题。

当写操作到来时,会把RWMutex.readerCount值拷贝到RWMutex.readerWait中,用于标记排在写操作前面的读者个数。

前面的读操作结束后,除了会递减RWMutex.readerCount,还会递减RWMutex.readerWait值,当RWMutex.readerWait值变为0时唤醒写操作。

3. sync.Map详解

一般情况下解决并发读写 map 的思路是加一把大锁,或者把一个 map 分成若干个小 map,对 key 进行哈希,只操作相应的小 map。前者锁的粒度比较大,影响效率;后者实现起来比较复杂,容易出错。

而使用 sync.map 之后,对 map 的读写,不需要加锁。并且它通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。

type Map struct {
	mu Mutex
	read atomic.Value // readOnly
	dirty map[interface{}]*entry
	misses int
}

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
}

type entry struct {
	p unsafe.Pointer // *interface{}
}

在进行读操作的时候,会先在read中找,没有命中的话会锁住dirty并且寻找,如果找到了miss计数+1,超过阈值时将dirty赋值给read;

在进行添加操作时,直接在dirty中添加;

在进行修改操作时,先改read,再改dirty;

在进行删除操作时,将read中加上amended标记,dirty中直接删除。

4. 原子操作 atomic.Value

愿此操作的底层是靠 MESI 缓存一致性协议来维持的。

Go的 atomic.Value 需要注意应该放入只读对象。

//atomic.Value源码

type Value struct {
	v interface{} // 所以可以存储任何类型的数据
}

// 空 interface{} 的内部表示格式,作用是将interface{}类型分解,得到其中两个字段
type ifaceWords struct {
	typ  unsafe.Pointer
	data unsafe.Pointer
}

// 取数据就是正常走流程
func (v *Value) Load() (x interface{}) {
	vp := (*ifaceWords)(unsafe.Pointer(v))
	typ := LoadPointer(&vp.typ)
	if typ == nil || uintptr(typ) == ^uintptr(0) {
		// 第一次还没写入
		return nil
	}
  // 构造新的interface{}返回出去
	data := LoadPointer(&vp.data)
	xp := (*ifaceWords)(unsafe.Pointer(&x))
	xp.typ = typ
	xp.data = data
	return
}

// 写数据(如何保证数据完整性)
func (v *Value) Store(x interface{}) {
	if x == nil {
		panic("sync/atomic: store of nil value into Value")
	}
  // 绕过 Go 语言类型系统的检查,与任意的指针类型互相转换
	vp := (*ifaceWords)(unsafe.Pointer(v)) // 旧值
	xp := (*ifaceWords)(unsafe.Pointer(&x)) // 新值
	for { // 配合CompareAndSwap达到乐观锁的功效
		typ := LoadPointer(&vp.typ)
		if typ == nil { // 第一次写入
			runtime_procPin() // 禁止抢占
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
				runtime_procUnpin() // 没有抢到锁,说明已经有别的线程抢先完成赋值,重新进入循环
				continue
			}
			// 首次赋值
			StorePointer(&vp.data, xp.data)
			StorePointer(&vp.typ, xp.typ)
			runtime_procUnpin() // 写入成功,解除占用状态
			return
		}
		if uintptr(typ) == ^uintptr(0) {
			// 第一次写入还未完成,继续等待
			continue
		}
		// 两次需要写入相同类型
		if typ != xp.typ {
			panic("sync/atomic: store of inconsistently typed value into Value")
		}
		StorePointer(&vp.data, xp.data)
		return
	}
}

// 禁止抢占,标记当前G在M上不会被抢占,并返回当前所在P的ID。
func runtime_procPin()
// 解除G的禁止抢占状态,之后G可被抢占。
func runtime_procUnpin()

5. 使用小技巧

  • 减小临界区域(减少锁的持有时间)
var m sync.Mutex

func DoSth() {
    // do sth1
    func() {
       u.lock()
       defer m.unlock()
       // do sth2
    }()
    // do sth3
}

如上所示,如果do sth3中是很费时的io操作,使用这个技巧可以将临界区减小,提高性能,不过,如果本身临界区就不大,锁操作后续没有什么费时操作,那么也就没有必要这样操作了。

  • 减小锁的粒度

在高并发场景下,用锁的数量来换取并发效率,类似于java中ConcurrentHashmap的分段锁思想,增加锁的数量,减少一把锁控制的数据量。

  • 读写分离(读写锁): RWMutex,sync.Map

在读多写少的情景下,可以使用读写锁,提高读操作的并发性能。

  • 使用原子操作

原子操作是CPU指令级的操作,不会触发g调度机制。,不阻塞执行流

到此这篇关于Golang的锁机制与使用技巧精选的文章就介绍到这了,更多相关Golang 锁机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go如何利用orm简单实现接口分布式锁

    在开发中有些敏感接口,例如用户余额提现接口,需要考虑在并发情况下接口是否会发生问题.如果用户将自己的多条提现请求同时发送到服务器,代码能否扛得住呢?一旦没做锁,那么就真的会给用户多次提现,给公司带来损失.我来简单介绍一下在这种接口开发过程中,我的做法. 第一阶段: 我们使用的orm为xorm,提现表对应的结构体如下 type Participating struct { ID uint `xorm:"autoincr id" json:"id,omitempty"`

  • Golang全局变量加锁的问题解决

    如果全局变量只读取 那自然是不需要加锁的 如果全局变量多进程读,多进程写,那自然是需要加读写锁的 但是如果全局变量只有一个进程写,其他进程读呢? 如果采用COW的方式,写进程只是通过单次赋值的方式来更新变量,是否就可以不加锁了呢? 就第三种情况而言: 当然我们通过 go build -race 或者 go run -race 就会出现 WARNING: DATA RACE. 但是出现 data race 就证明一定有问题么? 其实核心点在于这个赋值是否是原子的.也就是说是否存在 p1 = p2

  • Golang并发操作中常见的读写锁详析

    互斥锁简单粗暴,谁拿到谁操作.今天给大家介绍一下读写锁,读写锁比互斥锁略微复杂一些,不过我相信我们今天能够把他拿下! golang读写锁,其特征在于 读锁:可以同时进行多个协程读操作,不允许写操作 写锁:只允许同时有一个协程进行写操作,不允许其他写操作和读操作 读写锁有两种模式.没错!一种是读模式,一种是写模式.当他为写模式的话,作用和互斥锁差不多,只允许有一个协程抢到这把锁,其他协程乖乖排队.但是读模式就不一样了,他允许你多个协程读,但是不能写.总结起来就是: 仅读模式: 多协程可读不可写 仅

  • Go 语言下基于Redis分布式锁的实现方式

    分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 项目地址: https://github.com/Spongecaptain/redisLock 1. Go 原生的互斥锁 Go 原生的互斥锁即 sync 包下的 M

  • Go语言实现互斥锁、随机数、time、List

    Go语言实现互斥锁.随机数.time.List import ( "container/list" "fmt" "math/rand" //备注2:随机数的包 "sync" //备注1:异步任务的包 "time" ) type INFO struct { lock sync.Mutex //备注1:异步锁 Name string Time int64 } var List *list.List = list

  • 详解Golang互斥锁内部实现

    go语言提供了一种开箱即用的共享资源的方式,互斥锁(sync.Mutex), sync.Mutex的零值表示一个没有被锁的,可以直接使用的,一个goroutine获得互斥锁后其他的goroutine只能等到这个gorutine释放该互斥锁,在Mutex结构中只公开了两个函数,分别是Lock和Unlock,在使用互斥锁的时候非常简单,本文并不阐述使用. 在使用sync.Mutex的时候千万不要做值拷贝,因为这样可能会导致锁失效.当我们打开我们的IDE时候跳到我们的sync.Mutex 代码中会发现

  • Golang的锁机制与使用技巧小结

    目录 1. sync.Mutex详解 2. RWMutex详解 3. sync.Map详解 4. 原子操作 atomic.Value 5. 使用小技巧 1. sync.Mutex详解 sync.Mutex是Go中的互斥锁,通过.lock()方法上锁,.unlock()方法解锁.需要注意的是,因为Go函数值传递的特点,sync.Mutex通过函数传递时,会进行一次拷贝,所以传递过去的锁是一把全新的锁,大家在使用时要注意这一点,另外sync.Mutex是非重入锁,这一点要与Java中的锁区分. ty

  • Golang的锁机制使用及说明

    目录 踩坑点 互斥锁 Mutex 读写锁 RWMutex 谨防锁拷贝 查看数据竞争 总结 golang中的锁分为互斥锁.读写锁.原子锁即原子操作. 在 Golang 里有专门的方法来实现锁,就是 sync 包,这个包有两个很重要的锁类型.一个叫 Mutex, 利用它可以实现互斥锁. 一个叫 RWMutex,利用它可以实现读写锁. 全局锁 sync.Mutex,是同一时刻某一资源只能上一个锁,此锁具有排他性,上锁后只能被此线程使用,直至解锁.加锁后即不能读也不能写.全局锁是互斥锁,即 sync.M

  • MySQL锁机制与用法分析

    本文实例讲述了MySQL锁机制与用法.分享给大家供大家参考,具体如下: MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制.比如,MyISAM和MEMORY存储引擎采用的是表级锁:BDB存储引擎采用的是页面锁,但也支持表级锁:InnoDB存储引擎既支持行级锁,也支持表级锁,但默认情况下采用行级锁. MySQL这3种锁的特性可大致归纳如下: (1)表级锁:开销小,加锁快:不会出现死锁:锁定粒度大,发生锁冲突的概率最高,并发度最低. (2)行级锁:开销大,加锁慢:会出现死锁:

  • Java锁机制Lock用法示例

    本文实例讲述了Java锁机制Lock用法.分享给大家供大家参考,具体如下: package com.expgiga.JUC; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 一.用于解决多线程安全问题的方式: * 1.同步代码块 synchronized 隐式锁 * 2.同步方法 synchronized 隐式锁 * 3.同步锁Lock (jdk1.5以后

  • golang 自旋锁的实现

    CAS算法(compare and swap) CAS算法是一种有名的无锁算法.无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization).CAS算法涉及到三个操作数 需要读写的内存值V 进行比较的值A 拟写入的新值B 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作).一般情况下是一个自旋操作,即不断的重试.

  • PHP+redis实现的悲观锁机制示例

    本文实例讲述了PHP+redis实现的悲观锁.分享给大家供大家参考,具体如下: 锁机制 通常使用的锁分为乐观锁,悲观锁这两种,简单介绍下这两种锁,作为本文的背景知识,对这类知识已经有足够了解的同学可以跳过这部分. 乐观锁 先来看下百度百科上的解释:大多是基于数据版本( Version )记录机制实现.何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 "version" 字段来实现.读取出数据时,将此版本号一同读出,之后更新时,对此版本

  • Golang自旋锁的相关介绍

    目录 自旋锁 golang实现自旋锁 可重入的自旋锁和不可重入的自旋锁 自旋锁的其他变种 1. TicketLock 2. CLHLock 3. MCSLock 4. CLHLock 和 MCSLock 自旋锁与互斥锁 总结 自旋锁 获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting. 它是为实现保护共享资源而提出的一种锁机制.其实,自旋锁与互斥锁比较类似,它们都是为了解决某项资源的互斥使用.无论是互斥锁,还是自旋锁,在任何时刻,最多只能由一个保

  • Java并发编程之显式锁机制详解

    我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加锁和解锁.而我们本篇将要介绍的显式锁是一种手动式的实现方式,程序员控制锁的具体实现,虽然现在越来越趋向于使用synchronized直接实现原子操作,但是了解了Lock接口的具体实现机制将有助于我们对synchronized的使用.本文主要涉及以下一些内容: 接口Lock的基本组成成员 可重入锁Re

  • JS常用函数和常用技巧小结

    学习和工作的过程中总结的干货,包括常用函数.常用js技巧.常用正则表达式.git笔记等.为刚接触前端的童鞋们提供一个简单的查询的途径,也以此来缅怀我的前端学习之路. Ajax请求 jquery ajax函数 我自己封装了一个ajax的函数,代码如下: var Ajax = function(url, type success, error) { $.ajax({ url: url, type: type, dataType: 'json', timeout: 10000, success: fu

  • Oracle数据完整性和锁机制简析

    本课内容属于Oracle高级课程范畴,内容略微偏向理论性,但是与数据库程序开发和管理.优化密切相关:另外本课的部分内容在前面章节已经涉及,请注意理论联系实际. 事务 事务(Transaction)从 通讯的角度看:是用户定义的数据库操作序列,这些操作要么全做.要么全不做,是不可分割的一个工作单元.事务控制语句称为TCL,一般包括Commit和Rollback. 事务不是程序,事务和程序分属两个概念.在RDBMS中,一个事务可以有一条SQL语句.一组SQL语句或者整个程序:一个应用程序又通常包含多

随机推荐