内容简介:Golang最为让人熟知的并发模型当属CSP并发模型,也就是由goroutine和channel构成的GMP并发模型,具体内容不在赘述了,可以翻回之前的文章查看。在这里,要讲讲Golang的其他并发方式。Golang不仅可以使用CSP并发模式,还可以使用传统的共享数据的并发模式。这是传统语言比较常用的的方式,即加锁。加锁使其线程同步,每次只允许一个goroutine进入某个代码块,此代码块区域称之为”
Golang最为让人熟知的并发模型当属CSP并发模型,也就是由goroutine和channel构成的GMP并发模型,具体内容不在赘述了,可以翻回之前的文章查看。在这里,要讲讲Golang的其他并发方式。
Golang不仅可以使用CSP并发模式,还可以使用传统的共享数据的并发模式。
临界区(critical section)
这是传统语言比较常用的的方式,即加锁。加锁使其线程同步,每次只允许一个goroutine进入某个代码块,此代码块区域称之为” 临界区(critical section) ”。
Golang为 临界区(critical section) 提供的是互斥锁的包和条件变量的包。
互斥锁
就是通常使用的锁,用来让线程串行用的。Golang提供了互斥锁 sync.Mutex
和读写互斥锁 sync.RWMutex
,用法极其简单:
var s sync.Mutex s.Lock() // 这里的代码就是串行了,吼吼吼。。。 s.Unlock()
Lock和Unlock
sync.Mutex
和 sync.RWMutex
的区别
没啥大的区别,只不过 sync.RWMutex
更加细腻,可以将“读操作”和“写操作”区别对待。
sync.RWMutex
中的Lock和unLock针对写操作
var s sync.RWMutex s.Lock() // 上写锁了,吼吼 s.Unlock()
sync.RWMutex
中的RLock和RUnLock针对读操作
var s sync.RWMutex s.RLock() // 上读锁了,吼吼.. s.RUnlock()
读写锁有以下规则:
- 写锁被锁定,(再试图进行)读锁和写锁都阻塞
- 读锁被锁定,(再试图进行)写锁阻塞,(再试图进行)读锁不阻塞
即:多个写操作不能同时进行,写操作和读操作也不能同时进行,多个读操作可以同时进行
注意事项:
-
不要重复锁定互斥锁;因为代码写起来麻烦,容易出错,万一死锁(deadlock)了就废了。Go语言运行时系统自己抛出的panic都属于致命错误,都是无法恢复的,调用
recover
函数对它们起不到任何作用。一旦产生死锁,程序必然崩溃。 -
锁定和解锁一定要成对出现,如果怕忘记解锁,最好是使用
defer
语句来解锁;但是,一定不要对未锁定的或者已经锁定的互斥锁解锁,因为会触发panic
,而且此panic
和死锁一样,属于致命错误,程序肯定崩溃 -
sync.Mutex
是个结构体,尽量不要其当做参数,在多个函数直接传播。因为没啥意义,Golang的参数都是副本,多个副本之间都是相互独立的。
条件变量Cond
互斥锁是用来锁住资源,“创造”临界区的。而条件变量Cond可以认为是用来自行调度线程(在此即为groutine)的,当某个状态时,阻塞等待,当状态改变时,唤醒。
Cond的使用,离不开互斥锁,即离不开 sync.Mutex
和 sync.RWMutex
。
Cond初始化都需要有个互斥锁。(ps:哪怕初始化不需要,就应用场景而言,也得需要个互斥锁)
Cond
提供Wait、Signal、Broadcast 三种方法。
Wait表示线程(groutine)阻塞等待;
Signal表示唤醒等待的groutine;
Broadcast表示唤醒等待的所有groutine;
初始化:
cond := sync.NewCond(&sync.Mutex{})
在其中一个groutine中:
cond.L.Lock() for status == 0 { cond.Wait() } //状态改变,goroutine被唤醒后,干点啥。。。 cond.L.Unlock()
以上算是模板
在另外一个groutine中:
cond.L.Lock() status = 1 cond.Signal() // 或者使用cond.Broadcast()来唤醒以上groutine中沉睡的groutine cond.L.Unlock()
原子操作(atomicity)
原子操作是硬件芯片级别的支持,所以可以保证绝对的线程安全。而且执行效率比其他方式要高出好几个数量级。
Go语言的原子操作当然也是基于CPU和操作系统的,Go语言提供的原子操作的包是 sync/atomic
,此包提供了加(Add)、CAS(交换并比较 compare and swap)、成对出现的存储(store)和加载(load)以及交换(swap)。
此包提供的大多数函数针对的数据类型也非常的单一:只有整型!使用方式十分的简单,看着函数直接调用就好。
var a int32 a = 1 a = atomic.AddInt32(&a, 2) //此处是原子操作,就这么简单,吼吼
在此特别强调一下CAS,CAS对应的函数前缀是“CompareAndSwap”,含义和用法正如英文翻译:比较并交换。在进行CAS操作的时候,函数会先判断被操作变量的当前值是否与我们预期的旧值相等,如果相等,它就把新值赋给该变量,并返回true,反之,就忽略此操作,并返回false。
可能是Golang提供的原子操作的数据类型实在是有限,Go又补充了一个结构体 atomic.Value
,此结构体相当于一个小容器,可以提供原子操作的存储 store
和提取 load
var atomicVal atomic.Value str := "hello" atomicVal.Store(str) //此处是原子操作哦 newStr := atomicVal.Load() //此处是原子操作哦
其他
为了能更好的调度goroutine,Go提供了 sync.WaitGroup
和 sync.Once
两个包。
sync.WaitGroup
sync.WaitGroup
的作用就是在多goroutine并发程序中,让主goroutine等待所有goroutine执行结束。(直接查看代码注释)
sync.WaitGroup
提供了三个函数 Add
、 Done
和 Wait
三者用法如下:
- Add 写在主goroutine中,参数为将要运行的goroutine的数量
- Done 写在各个非主goroutine中,表示运行结束
- Wait 写在主goroutine中,block主goroutine,等待所有其他goroutine运行结束
var wait sync.WaitGroup wait.Add(2) //必须是运行的goroutine的数量 go func() { //TODO 一顿小操作 defer wait.Done() // done函数用在goroutine中,表示goroutine操作结束 }() go func() { //TODO 一顿小操作 defer wait.Done() // done函数用在goroutine中,表示goroutine操作结束 }() wait.Wait() // block住了,直到所有goroutine都结束
注意
sync.WaitGroup
中有一个计数器,记录的是需要等待的goroutine的数量,默认值是0,可以通过Add方法来增加或者减少值,但是切记,千万不能让计数器的值小于零,会触发panic!
sync.WaitGroup
调用Wait方法的时候, sync.WaitGroup
中计数器的值一定要为0。因此Add中的值一定要等于非主goroutine的数量!
且不要把Add和Wait方法放到不同的goroutine中执行!
sync.Once
真真正正的只执行一次。
sync.Once
只要一个方法: Do
,里面就一个参数: func
。多说无益,复制下面代码,猜猜执行结果就知道了。
package main import ( "fmt" "sync" ) func main() { var once sync.Once onceBody := func() { fmt.Println("Only once") } done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } for i := 0; i < 10; i++ { <-done } }
执行结果
Only once
没错,只有一行。真只执行了一次。
以上所述就是小编给大家介绍的《Golang非CSP并发模型外的其他并行方法总结》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- golang下的并发、并行优化
- 高并发'大杀器'异步化、并行化
- 高并发的“大杀器”:异步化、并行化
- 如何向纯洁的女朋友解释并发与并行的区别?
- 15分钟读懂进程线程、同步异步、阻塞非阻塞、并发并行,太实用了!
- sqltoy-orm-4.17.6 发版,支持 Greenplum、并行查询可设置并行数量
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。