sync.Mutex与sync.RWMutex

保证多个并发线程对共享资源的访问是串行的,否则很容易出现争用和冲突的情况,这时需要使用互斥量来保证在同一时刻只有一个goroutine访问共享资源,其中sync.Mutex与sync.RWMutex类型就是互斥量,也称互斥锁,

当有goroutine进入临界区时,我们对他进行锁定,当他离开时我们进行解锁操作,锁定操作可以用Lock方法实现,解锁操作可以用Unlock方法

var mu sync.Mutex //定义互斥锁
mu.Lock()...             //临界区  mu.Unlock()

1:不要重复锁定互斥锁

2:不要忘记解锁,适当使用defer语句

3:不要在多个函数传递互斥锁 (因为值传递时会产生副本,副本也是独立的互斥锁)

4:不要对未锁定的互斥锁解锁

sync.RWMutex就是读写互斥锁,比起sync.Mutex更加细致,读操作和写操作分别保护,多个写操作不能同时进行,读操作和写操作也不可以同时进行,但是多个读操作可以同时进行。

1:写锁已经锁定时,试图锁定读锁或者锁定写锁都会阻塞当前goroutine

2:读锁已经锁定时,试图锁定写锁时会阻塞goroutine

读锁写锁的锁定解锁分别用RLock(),RUnlock(),Lock(),Unlock().

sync.Cond

条件变量是基于互斥锁的,有互斥锁才能发挥作用,但是条件变量的主要作用并不是保护共享资源的,是用来通知给别的goroutine共享资源的状态。

条件变量的方法:wait(等待通知),signal(单发通知),broadcast(广发通知)

sync.Cond类型需要初始化才可以使用,传入的值需要是sync.Locker类型的参数,sync.Locker是一个接口,实现的方法为Lock(),UnLock(),sync.Mutex与sync.RWMutex都拥有这两个方法,但是是指针方法,所以传入的参数也要是指针类型

代码详见:Golang_Puzzlers/demo61.go at master · hyper0x/Golang_Puzzlers (github.com)

package mainimport ("log""sync""time"
)func main() {// mailbox 代表信箱。// 0代表信箱是空的,1代表信箱是满的。var mailbox uint8// lock 代表信箱上的锁。var lock sync.RWMutex// sendCond 代表专用于发信的条件变量。sendCond := sync.NewCond(&lock)// recvCond 代表专用于收信的条件变量。recvCond := sync.NewCond(lock.RLocker())// sign 用于传递演示完成的信号。sign := make(chan struct{}, 3)max := 5go func(max int) { // 用于发信。defer func() {sign <- struct{}{}}()for i := 1; i <= max; i++ {time.Sleep(time.Millisecond * 500)lock.Lock()for mailbox == 1 {sendCond.Wait()}log.Printf("sender [%d]: the mailbox is empty.", i)mailbox = 1log.Printf("sender [%d]: the letter has been sent.", i)lock.Unlock()recvCond.Signal()}}(max)go func(max int) { // 用于收信。defer func() {sign <- struct{}{}}()for j := 1; j <= max; j++ {time.Sleep(time.Millisecond * 500)lock.RLock()for mailbox == 0 {recvCond.Wait()}log.Printf("receiver [%d]: the mailbox is full.", j)mailbox = 0log.Printf("receiver [%d]: the letter has been received.", j)lock.RUnlock()sendCond.Signal()}}(max)<-sign<-sign
}

wait方法具体操作:

1:把当前goroutine加入到条件变量等待队列

2:把当前条件变量基于的锁解锁

3:收到通知时决定是否唤醒当前等待的goroutine,

4:唤醒goroutine之后重新锁定互斥锁

Signal与Broadcast方法不同之处在于前者只会唤醒一个等待的goroutine,后者会唤醒所有等待的goroutine,Signal唤醒的goroutine一般都是最早等待的那个。这两个方法不需要互斥锁保护的时候进行,相反解锁之后进行会对程序更有利

原子操作

go语言在开启多个goroutine时,同一时刻在底层支持的数量可能不会超过核心线程数,所以调度器会频繁的运行/停止这些goroutine,所以还是会影响运行的效率,因为会被打断。真正实现原子性执行的只有原子操作,原子操作进行时不会被打断,这代表执行速度更快,不过正因为不会被打断,所以他需要更简单更快速,因此支持的只有整数和二进制位的原子操作。

在sync/atomic包中的函数提供了修改、读取、写入、交换、比较并交换

比较并交换操作,也叫CAS操作,与互斥锁不同的是,互斥锁在假设goroutine频繁更改共享资源而使用,而CAS操作恰恰相反,它往往是假设共享资源更改不频繁而使用,也被称为乐观锁。

代码举例:

var value int32
func AddValue(delta int32)  {for {v:= valueif atomic.CompareAndSwapInt32(&value,v,(v+delta)) {break}}
}

由于原子操作类型过于局限,互斥锁往往更常用一些

sync/atomic.Value

它只有两个方法,Store和Load用来提供原子性的读写值操作,可以说是原子性的一个容器

注意:

1:我们不能用Store方法传入nil,会引发panic,(如果有一个接口类型的变量,它的动态值是nil,但动态类型却不是nil,那么它的值就不等于nil。这样一个变量的值是可以被存入原子值的。)

2:我们向这里存储的第一个值的类型决定了只能存储什么类型的值

还有,我们尽量不要传入引用类型的值,这样是不安全的,因为我们可以绕过原子值去修改原子值内部的引用值,比如:


var box6 atomic.Value
v6 := []int{1, 2, 3}
box6.Store(v6)
v6[1] = 4 // 注意,此处的操作不是并发安全的!

解决方法,我们可以把引用类型的值副本传入原子值


store := func(v []int) {replica := make([]int, len(v))copy(replica, v)box6.Store(replica)
}
store(v6)
v6[2] = 5 // 此处的操作是安全的。

sync.WaitGroup和sync.Once

sync包中的WautGroup类型更适合实现一对多的goroutine协作流程,他有三个指针方法:Add、Done、Wait

Add:一般时候用来记录需要等待的goroutine的数量,进行计数器加操作

Done:与Add相对进行计数器的减操作

wait:阻塞当前goroutine,直到计数器为0

代码对比:(Golang_Puzzlers/demo65.go at master · hyper0x/Golang_Puzzlers (github.com))

func coordinateWithChan() {sign := make(chan struct{}, 2)num := int32(0)fmt.Printf("The number: %d [with chan struct{}]\n", num)max := int32(10)go addNum(&num, 1, max, func() {sign <- struct{}{}})go addNum(&num, 2, max, func() {sign <- struct{}{}})<-sign<-sign
}func coordinateWithWaitGroup() {var wg sync.WaitGroupwg.Add(2)num := int32(0)fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)max := int32(10)go addNum(&num, 3, max, wg.Done)go addNum(&num, 4, max, wg.Done)wg.Wait()
}// addNum 用于原子地增加numP所指的变量的值。
func addNum(numP *int32, id, max int32, deferFunc func()) {defer func() {deferFunc()}()for i := 0; ; i++ {currNum := atomic.LoadInt32(numP)if currNum >= max {break}newNum := currNum + 2time.Sleep(time.Millisecond * 200)if atomic.CompareAndSwapInt32(numP, currNum, newNum) {fmt.Printf("The number: %d [%d-%d]\n", newNum, id, i)} else {fmt.Printf("The CAS operation failed. [%d-%d]\n", id, i)}}
}

注意当WaitGroup中的计数器值小于0时会·引发panic,也不要用不同的goroutine去分别进行Add/Done和Wait,会有几率引发panic

go语言sync代码包中waitgroup_test.go部分代码,展示了异常情况的发生条件

func TestWaitGroupMisuse(t *testing.T) {defer func() {err := recover()if err != "sync: negative WaitGroup counter" {t.Fatalf("Unexpected panic: %#v", err)}}()wg := &WaitGroup{}wg.Add(1)wg.Done()wg.Done()t.Fatal("Should panic")
}

sync.Once类型也是开箱即用的,其中的Do方法只接受一个参数,类型必须是func(),是一个无参数声明和结果声明的函数,这个方法只会执行首次被调用时传入的函数,之后不会执行任何函数参数,Once内部包含的done字段用来判断Do方法是否调用完成,所以值只是0或1

1:Do方法会在参数函数执行完毕时把done字段置为1

2:done值的修改和读取都是原子操作,所以就算参数函数引发panic,程序也无法在用这个Once值去执行他了

sync.Pool

go语言中的临时对象池,被用来存储临时对象,可以针对数据的缓存使用,这个类型只有两个方法,Get和Put

Get:用于获取当前池中的临时对象

Put:用于存放临时对象

如果Get方法使用时,池中没有对象那么这个方法会用sync.Pool类型的New字段创建对象并返回

New的类型是func()  interface{}

New字段需要初始化对象池时就给定一个值,fmt包中就使用到了。

var ppFree = sync.Pool{New: func() interface{} { return new(pp) },
}

有关临时对象池的清理,引用郝林老师的一段话:

sync包在被初始化的时候,会向 Go 语言运行时系统注册一个函数,这个函数的功能就是清除所有已创建的临时对象池中的值。我们可以把它称为池清理函数。
一旦池清理函数被注册到了 Go 语言运行时系统,后者在每次即将执行垃圾回收时就都会执行前者。
另外,在sync包中还有一个包级私有的全局变量。这个变量代表了当前的程序中使用的所有临时对象池的汇总,它是元素类型为*sync.Pool的切片。我们可以称之为池汇总列表。
通常,在一个临时对象池的Put方法或Get方法第一次被调用的时候,这个池就会被添加到池汇总列表中。正因为如此,池清理函数总是能访问到所有正在被真正使用的临时对象池。
更具体地说,池清理函数会遍历池汇总列表。对于其中的每一个临时对象池,它都会先将池中所有的私有临时对象和共享临时对象列表都置为nil,然后再把这个池中的所有本地池列表都销毁掉。
最后,池清理函数会把池汇总列表重置为空的切片。如此一来,这些池中存储的临时对象就全部被清除干净了。

sync.Map

用不同的goroutine操作原生字典是不安全的,所以诞生了并发安全字典

并发安全字典同样对键值类型有要求,不能是函数类型,字典类型,切片类型。我们可以用类型断言表达式或者反射来保证类型正确

引用郝林极客时间《Go语言核心三十六讲》:

代码:Golang_Puzzlers/demo72.go at master · hyper0x/Golang_Puzzlers (github.com)


type IntStrMap struct {m sync.Map
}func (iMap *IntStrMap) Delete(key int) {iMap.m.Delete(key)
}func (iMap *IntStrMap) Load(key int) (value string, ok bool) {v, ok := iMap.m.Load(key)if v != nil {value = v.(string)}return
}func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {a, loaded := iMap.m.LoadOrStore(key, value)actual = a.(string)return
}func (iMap *IntStrMap) Range(f func(key int, value string) bool) {f1 := func(key, value interface{}) bool {return f(key.(int), value.(string))}iMap.m.Range(f1)
}func (iMap *IntStrMap) Store(key int, value string) {iMap.m.Store(key, value)
}

编写了一个名为IntStrMap的结构体类型,它代表了键类型为int、值类型为string的并发安全字典。在这个结构体类型中,只有一个sync.Map类型的字段m。并且,这个类型拥有的所有方法,都与sync.Map类型的方法非常类似。

两者对应的方法名称完全一致,方法签名也非常相似,只不过,与键和值相关的那些参数和结果的类型不同而已。在IntStrMap类型的方法签名中,明确了键的类型为int,且值的类型为string。

显然,这些方法在接受键和值的时候,就不用再做类型检查了。另外,这些方法在从m中取出键和值的时候,完全不用担心它们的类型会不正确,因为它的正确性在当初存入的时候,就已经由 Go 语言编译器保证了。

稍微总结一下。第一种方案适用于我们可以完全确定键和值的具体类型的情况。在这种情况下,我们可以利用 Go 语言编译器去做类型检查,并用类型断言表达式作为辅助,就像IntStrMap那样。

第二种方案:

type ConcurrentMap struct {m         sync.MapkeyType   reflect.TypevalueType reflect.Type
}func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {if keyType == nil {return nil, errors.New("nil key type")}if !keyType.Comparable() {return nil, fmt.Errorf("incomparable key type: %s", keyType)}if valueType == nil {return nil, errors.New("nil value type")}cMap := &ConcurrentMap{keyType:   keyType,valueType: valueType,}return cMap, nil
}func (cMap *ConcurrentMap) Delete(key interface{}) {if reflect.TypeOf(key) != cMap.keyType {return}cMap.m.Delete(key)
}func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {if reflect.TypeOf(key) != cMap.keyType {return}return cMap.m.Load(key)
}func (cMap *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {if reflect.TypeOf(key) != cMap.keyType {panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))}if reflect.TypeOf(value) != cMap.valueType {panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))}actual, loaded = cMap.m.LoadOrStore(key, value)return
}func (cMap *ConcurrentMap) Range(f func(key, value interface{}) bool) {cMap.m.Range(f)
}func (cMap *ConcurrentMap) Store(key, value interface{}) {if reflect.TypeOf(key) != cMap.keyType {panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))}if reflect.TypeOf(value) != cMap.valueType {panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))}cMap.m.Store(key, value)
}

相比于第一种方案,第二种方案的键和值的类型更改更灵活,主要应用反射的知识,但是这样可能也会影响程序的性能,另外为了提高并发字典的性能,其中包含了read,和dirty字段,是两个原生字典,这里就不过多介绍了

Golang并发操作入门相关推荐

  1. Golang并发编程入门教程

    时间单位 1S = 1000ms 1ms = 1000us 1us = 1000ns 并发与并行 并行: 借助多核 cpu 实现. (真 并行) 并发: 宏观:用户体验上,程序在并行执行. 微观:多个 ...

  2. Golang并发模型:轻松入门流水线FAN模式

    前一篇文章<Golang并发模型:轻松入门流水线模型>,介绍了流水线模型的概念,这篇文章是流水线模型进阶,介绍FAN-IN和FAN-OUT,FAN模式可以让我们的流水线模型更好的利用Gol ...

  3. Golang并发模型:轻松入门协程池

    goroutine是非常轻量的,不会暂用太多资源,基本上有多少任务,我们可以开多少goroutine去处理.但有时候,我们还是想控制一下. 比如,我们有A.B两类工作,不想把太多资源花费在B类务上,而 ...

  4. Golang并发:再也不愁选channel还是选锁

    周末又到了,为大家准备了一份实用干货:如何使用channel和Mutex解决并发问题,利用周末的好时光,配上音乐,思考一下吧?. 来,问自己个问题:面对并发问题,是用channel解决,还是用Mute ...

  5. ​Golang 并发编程指南

    分享 Golang 并发基础库,扩展以及三方库的一些常见问题.使用介绍和技巧,以及对一些并发库的选择和优化探讨. go 原生/扩展库 提倡的原则 不要通过共享内存进行通信;相反,通过通信来共享内存. ...

  6. Golang并发模型:合理退出并发协程

    goroutine作为Golang并发的核心,我们不仅要关注它们的创建和管理,当然还要关注如何合理的退出这些协程,不(合理)退出不然可能会造成阻塞.panic.程序行为异常.数据结果不正确等问题.这篇 ...

  7. Java零基础并发编程入门

    Java零基础并发编程入门 并发编程主要包括: 线程,同步,future,锁,fork/join, volatile,信号量,cas(原子性,可见性,顺序一致性),临界性,分布式 了解基础: JMM: ...

  8. 4种Golang并发操作中常见的死锁情形

    摘要:什么是死锁,在Go的协程里面死锁通常就是永久阻塞了,你拿着我的东西,要我先给你然后再给我,我拿着你的东西又让你先给我,不然就不给你.我俩都这么想,这事就解决不了了. 本文分享自华为云社区< ...

  9. 并发编程入门(五):Java并发包和Java8并发

    目录 前言 JUC(Java.util.concurrent) 1.Java并发包之原子类 1.1.AtomicInteger 1.2.AtomicReference 1.3.AtomicStampe ...

最新文章

  1. mysql以及mysql bench安装教程
  2. 5年以上的Java程序员,千万别忽略这一点
  3. Nat. Genet. | 基于CRISPRi技术检测增强子与启动子相互作用
  4. leetcode算法题--有序链表转换二叉搜索树★
  5. es搜索热度属性_是不是场吊打局?奥迪A6L对比雷克萨斯ES
  6. 酱油和gbt酱油哪个好_酱油可不是越贵越好?看清瓶身上的5个字,教你1分钟买到好酱油...
  7. 【Pytorch神经网络理论篇】 29 图卷积模型的缺陷+弥补方案
  8. java static method_java 中static的几种用法
  9. 英特尔放出Linux微代码以修复Meltdown和Spectre漏洞
  10. 查找文件夹下图片的数量
  11. JavaVM和JNIEnv
  12. 微信小程序实验一 ——— 简单计算器与秒表
  13. Chrome卸载重装
  14. python变成灰色_Python怎么把彩色图像转换成灰色图像?
  15. wincc逻辑运算符_wincc逻辑运算符_wincc中表达式及公式
  16. 管中窥豹之淘宝大数据平台
  17. 收发电子邮件属于计算机在方面的应用( ),收发电子邮件属于计算机在( )方面的应用...
  18. 案例2:丙类仓库建筑防火案例分析
  19. 水果店开业怎样宣传自己的水果店,新开水果店怎么发朋友圈宣传
  20. JavaScript图片轮播图

热门文章

  1. java数组最大值索引_java中的权限修饰符有哪些?怎么获取数组最大值?
  2. Powershell快速生成指定尺寸的Kindle屏保图
  3. 研控步进电机Canopen通讯测试
  4. linux shell curl get 请求头 和多参数问题及解决
  5. 密集场景下的行人跟踪替代算法,头部跟踪算法 | CVPR 2021
  6. SSL 1577——泽泽在中国
  7. CEVA:先不要着急把数据搬到云端
  8. c/c++保留两位小数的方法
  9. CVPR 2020 SEPC论文解析:使用尺度均衡金字塔卷积做目标检测
  10. 根文件系统的构建和移植