前言
Golang本身实现了线程调度,对于并行来说需要程序运行环境物理设备多核处理器的加持 ,单核只能实现并发。
Goroutine是Go语言中的协程(Coroutine),称为Goroutine。
GPM是Golang的Goroutine调度框架,可以把M个Goroutine映射到N个系统线程中,最终被多核CPU调度执行。
Golang中多个Goroutine并发/行起来之后,通过使用原子操作(atomic)---->锁------>带锁环形队列(Channel)等技术实现并发/行安全;
同步/并发/并行概念
同步:2个/多个独立运行个体之间的执行顺序相互依赖,要么A的执行依赖B的执行结果,要么B的执行依赖A的执行结果。
并发:同一时间段内+交替执行多个任务,并发描述的是1个时间段。
并行:同一时刻+同时执行多个任务,不同的线程被不同的CPU执行,并行描述的是1个时间点。
真并发=并行:多核处理器的并发
伪并发:单核处理器的并发
由于Python的全局解释器锁(GIL)导致多线程最终被调度到同1个CPU上。称为伪并发
在硬件设备满足多核CPU的前提下,GPM-Goroutine调度框架可以把多个Goroutines同时调度到不同的CPU上执行,称为真并发/并行。
Goroutine
Goroutine本质是协程。
在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务。
同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。
那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?
我们原来的实现使用线程实现并发的方案流程是:
- 程序----》os线程池-----》os调度线程----->cpu
在Golang中
- 程序---》goroutine------》go's runtime调度goroutines--------》线程池-------》os线程接口-----》os调度线程----->cpu
1.使用goroutine
Goroutine有1个特性,一旦main函数结束,所有Goroutines也会全部消失。
因为mian函数结束相当于进程(资源单位)结束了,皮之不存毛将焉附?
package main import "fmt" func hello() { fmt.Println("hello") } //程序启动之后会主动创建1个main goroutine func main() { go hello() //开启1个独立的goroutine fmt.Println("main") //main函数结束之后由main函数启动的goroutine也全部结束 }
2.控制goroutine的执行顺序
sync.WaitGroup保证多个goroutine执行顺序package main import ( "fmt" "math/rand" "sync" "time" ) //waitGroup协调gortines顺序 var wg sync.WaitGroup func f() { //在go中生成随机数字(要加seed种子) rand.Seed(time.Now().UnixNano()) for i := 0; i < 5; i++ { n1 := rand.Intn(11) fmt.Println(n1) } } func f1(i int) { // goroutine结束就登记-1 defer wg.Done() //开启1个goroutine:睡300毫秒 time.Sleep(time.Millisecond * time.Duration(rand.Intn(300))) fmt.Printf("goroutine%d\n",i) } func main() { for i := 0; i < 10; i++ { // 启动一个goroutine就登记+1 wg.Add(1) go f1(i) } //如何等待10个goroutines全部完成,main函数再结束。 wg.Wait()//wg.Wait()等待计数器减为0 }
4.内核线程和Goroutines的关系
CPU执行的最小单位是OS线程,所有的goroutine最终都需要被runtime调度映射到真正的OS线程上,被CPU执行。
1个OS线程对应用户态N个Goroutine。
1个GO程序可以被Processor处理器使用多个OS线程。
Goroutines和OS线程是多对多的映射关系=M:N。
5.GMP并发模型
GMP的并发模型是Golang可以高效支持大并发的原因。
G:goroutine用户态定义的协程。
M:Macheine的意思,M和内核线程是1对1绑定的,且绑定关系固定不变。
P:Processor虚拟处理器的意思,P的数量不会多余M,把goroutine队列中等待执行的goroutine调度到M上执行,当goroutine执行遇到系统调用,P和会G接触绑定关系,当P空闲时,P会去其他P的goroutine队列中获取goroutine来执行,以分摊其他P的工作。
Runqueue:当所有的Processor都挂载了goroutine队列,没有可以的P使goroutine会被保存到该全局队列,等待空闲的P来获取和挂载到自己的goroutine队列中。
线程缓存:当M空闲时会被存放到线程缓存中,无需重新创建M。
6.Goroutine池
在Golang中可以轻松启动多个goroutine,但物极必反,无论我们启动多少个goroutine最终干活的还是os线程。
Goroutine池可以限制Goroutine的数量。
1个8核的服务器可以同时启动16个线程,但是golang中启动了1000个goritine。
16个os线程划分1000个goroutine无疑是增加了go runtime调度频率。并没有加速程序执行速度。
- 写端从channel接收数据时,没有写端来写,读端阻塞。
- Channel本身具有锁机制,是同步的,当多个Goroutines争抢1个channel的读/写操作权限时,同1时刻只能有1个Goroutine抢到。
1.Channel创建
每1个Channel都是1个具体类型的导管,叫作Channel的元素类型。
例如:1个有int类型元素的通道,写为
chan int
像map类型一样channel类型是1个通过make创建的引用类型。
当Channel作为参数传递到另1个函数时,复制的是channel引用,这样调用者和被调用者都会引用同1份数据结构。
和其他引用类型一样channel的零值为nil。
2.Channel比较运算
2个同类型的channel可以使用==符合进行比较运算。
var chan1 = make(chan struct{}, 1) var chan2 = make(chan struct{}, 1) func main() { fmt.Println(chan1 == chan2) }
但是只有2个同类型的channel都是同1个Channel的引用时,比较值=true。
var chan1 = make(chan struct{}, 1) var chan2 = make(chan struct{}, 1) //比较2个同类型的channel func isEqual(ch1, ch2 chan struct{}) (res bool) { res = ch1 == ch2 return } func main() { res1 := isEqual(chan2, chan1) fmt.Println(res1) //false //只有2个同类型的channel都是同1个Channel的引用时,比较值=true。 res2 := isEqual(chan1, chan1) fmt.Println(res2) //true res3 := isEqual(chan2, chan2) fmt.Println(res3) //true }
Channel也可以和nil进行比较。
var chan1 = make(chan struct{}, 1) func main() { fmt.Println(chan1 == nil) }
3.Channel操作
1.两个主要操作
channel有2个主要操作:发送(send)和接收(receive)两者结合在一起统称为1次通信。
var chan1 = make(chan struct{}, 1) func main() { //发送 chan1 <- struct{}{} //接收 <-chan1 }
2.关闭操作
Channel关闭代表着该通道写入完成
- channel关闭之后, 读取到的是通道元素类型的零值,不会引发异常。
- channel关闭之后,写入会引发 panic: send on closed channel异常。
- channel关闭之后,再次关闭channel会引发panic: close of closed channel异常。
close(chan1)
3.for range读操作
Channel关闭代表着该通道的写入完成
package main import ( "fmt" "golang.org/x/sys/windows" "time" ) var naturalNumberCh = make(chan int, 100) func write() { threadID := windows.GetCurrentThreadId() defer fmt.Printf("----write-%d结束\n", threadID) for i := 0; i <= 100; i++ { naturalNumberCh <- i } } func read() { threadID := windows.GetCurrentThreadId() defer fmt.Printf("----read-%d结束\n", threadID) for n := range naturalNumberCh { fmt.Printf("----read-%d读取到值%d\n", threadID, n) } } func main() { go write() go read() go read() go read() time.Sleep(10 * time.Second) fmt.Println("main关闭channel") //channel一旦关闭读取当前channel的3个read goroutie完成读取之后,立即结束阻塞,退出! close(naturalNumberCh) time.Sleep(10 * time.Second) }
Channel关闭后,使用for range读当前channel的全部Goroutines,完成读取---->结束阻塞----->退出for range循环。
- 在循环遍历channel时,如果channel已关闭,for循环正常遍历,正常退出!
- 在循环遍历channel时,如果channel未关闭,for range循环读完channel中的值之后还会继续读,for range循环不结束,导致当前Goroutine一直阻塞,无法正常退出,最终可能会造成死锁!
for range循环遍历channel引发的死锁问题
package main import ( "fmt" "sync" ) var wg sync.WaitGroup var naturalCh = make(chan int) var squareCh = make(chan int) func counter() { for i := 0; i < 100; i++ { naturalCh <- i } fmt.Println("counter协程结束") wg.Done() } func squarer() { for n := range naturalCh { squareCh <- n * n } //Channel不关闭,for range循环会一直读channel造成当前Goroutine一直阻塞,一直不结束 fmt.Println("squarer协程结束") wg.Done() } func printer() { for n := range squareCh { fmt.Println(n) } //Channel不关闭,for range循环会一直读channel造成当前Goroutine一直阻塞,一直不结束 fmt.Println("printer协程结束") wg.Done() } func main() { wg.Add(3) go counter() go squarer() go printer() fmt.Println("main协程结束") //wg一直等待squarer和sprinter结束,但是这2个Goroutine一直不结束!!! wg.Wait() }
5.循环监听读取
select可以同时监听多个channel是否可读
监听不能只监听1次,需要配合死循环进行循环监听
循环监听需要有死循环的结束条件,需要配合context
package main import ( "context" "fmt" "golang.org/x/sys/windows" "time" ) var naturalNumberCh = make(chan int, 100) func write() { //记得channel写入完成关闭,否则for range循环读一直不结束! defer close(naturalNumberCh) threadID := windows.GetCurrentThreadId() defer fmt.Printf("----write-%d结束\n", threadID) for i := 0; i <= 3; i++ { naturalNumberCh <- i } } func read(ctx context.Context) { threadID := windows.GetCurrentThreadId() for { select { case <-ctx.Done(): fmt.Printf("----read-%d结束------\n", threadID) return default: for n := range naturalNumberCh { fmt.Printf("----read-%d读取到值%d\n", threadID, n) } } } } func main() { ctx, cancel := context.WithCancel(context.Background()) go write() go read(ctx) go read(ctx) go read(ctx) time.Sleep(3 * time.Second) fmt.Println("main发送关闭信号给读取gorutine") cancel() time.Sleep(10 * time.Second) fmt.Println("mian结束") }
6.操作总结
Channel的读、写操作遵循供需、守恒原则,生产方Goroutine生产值的数量与消费方Goroutine消费值的数量比例为1:1
Channel的读写操作自带锁功能,同1个时刻只能有1个Goroutine执行channel的读/写操作。
使用for range循环读channel一定记得手动关闭channel,否则for range循环一直不结束,goroutine就会一直阻塞,进而导致死锁。
操作/状态 | channel=nil | 正常channel | 已关闭的channel |
---|---|---|---|
读 | 阻塞 | 成功或阻塞 | 读到零值 |
写 | 阻塞 | 成功或阻塞 | panic |
关闭 close(ch) | panic | 成功 | panic |
4.Channel分类
根据Channel容量,可以把Channel划分为有缓冲Channel和无缓冲channel。
- 有缓冲通道(BufferdChannel) :不能缓冲数据,容量=0
- 无缓冲通道:(unbufferdChannel):可以缓冲一定数量的数据,容量>0
根据Channel支持的读、写功能,可以把Channel划分为单向Channel和双向Channel。
- 单向channel:仅支持读或写,1种功能。
- 双向channel:同时支持读和写,2种功能。
1.无缓冲Channel
无缓冲channel:不能缓冲数据,容量=0
var unbufferdCh1 = make(chan struct{}) //无缓冲channel var unbufferdCh2 = make(chan struct{},0) //无缓冲channel var bufferdCh = make(chan struct{}, 1) //容量=1的有缓冲channel
当1个goroutine1向无缓冲channelA发送数据时,goroutine1会进行阻塞状态,直到另1个goroutine2从读无缓冲channelA读取数据。
此时1次通信操作完成,goroutine1和goroutine2都同时处于运行状态。
此时1次通信操作完成,goroutine2和goroutine1都同时处于运行状态。
2个goroutine使用无缓冲通道通信会导致goroutine同步化,因此无缓冲通道也称为同步通道。
经典案例:
基于1个无缓冲channel特性,使用2个Goroutine交替打印奇偶数。
package main import ( "fmt" "sync" ) var wg sync.WaitGroup //无缓冲Channel var ch = make(chan struct{}) //Goroutine写 func workerW(ch chan struct{}) { for i := 1; i <= 10; i++ { fmt.Println("workerW开始", i) ch <- struct{}{} fmt.Println("workerW结束", i) } wg.Done() } //Goroutine读 func workerR(ch chan struct{}) { for i := 1; i <= 10; i++ { fmt.Println("workerR开始", i) <-ch fmt.Println("workerR结束", i) } wg.Done() } func main() { wg.Add(2) go workerW(ch) go workerR(ch) wg.Wait() } /* 执行结果:假设workerR Goroutine先开始执行 ----------------------------------------- 1. workerR开始 i=1, 然后workerR读无缓冲Channel进入阻塞,workerR阻塞,workerW执行 ---workerR读阻塞 ----------------------------------------- 2. workerW开始 i=1 然后workerW写入无缓冲Channel,结束workerR的阻塞 ---workerW写不阻塞 3. workerW结束 i=1 workerW继续执行 4. workerW开始 i=2,然后workerW写无缓冲Channel进入阻塞,workerR执行 ---workerW写阻塞 ----------------------------------------- 5. workerR结束 i=1 workerR执行 6. workerR开始 i=2,然后workerR读无缓冲Channel,结束workerW的阻塞 ---workerR读不阻塞 7. workerR结束 i=2 workerR继续执行 8. workerR开始 i=3,然后workerR读无缓冲Channel进入阻塞,workerR阻塞,workerW执行 ---workerR读阻塞 ----------------------------------------- 9. workerW结束 i=2 workerW继续执行 10.workerW开始 i=3 然后workerW写无缓冲Channel,结束workerR的阻塞 ---workerW写不阻塞 11.workerW结束 i=3 workerW继续执行 12.workerW开始 i=4,然后workerW写无缓冲Channel进入阻塞,workerR执行 ---workerW写阻塞 ----------------------------------------- 13.workerR结束 i=3 workerR继续执行 14.workerR开始 i=4 然后workerR读无缓冲Channel,结束workerW的阻塞 ---workerR读不阻塞 15.workerR结束 i=4 workerR执行 16.workerR开始 i=5,然后workerR读无缓冲Channel进入阻塞,workerR阻塞,workerW执行 ---workerR读阻塞 ----------------------------------------- */
无缓冲channel控制多个Goroutine的执行顺序
package main import ( "fmt" ) func main() { ch1 := make(chan bool) ch2 := make(chan bool) go func() { fmt.Println("step1") <-ch1 }() go func() { ch1 <- true fmt.Println("step2") ch2 <- true }() <-ch2 fmt.Println("step3") }
2.单向Channel
当1个Channel用作函数的形参时,它几乎被有意地限制不能发送或者不能接收。
限制函数操作Channel的权限可以避免Channel被误用。
Go提供了单向Channel类型,仅支持发送 or 读取1种操作。
sync包
Golang除了提供channel 这种CSP机制达到goroutines之间共性数据目的之外,还提供了1个sync包实现并发安全。
sync包中提供了Mutex(互斥锁)、once(一次性操作)、waigroup(主线程等待所有goroutine结束再推出)、RWMutex(读写相互斥锁)等功能,帮助我们实现并发安全。
package main import ( "context" "fmt" "golang.org/x/sys/windows" "sync" ) var naturalNumberCh = make(chan int, 100) var wg sync.WaitGroup var donech = make(chan uint32, 3) func write() { threadID := windows.GetCurrentThreadId() defer func() { wg.Done() //记得channel写入完成关闭,否则for range循环读一直不结束! close(naturalNumberCh) fmt.Printf("----write-%d结束\n", threadID) }() for i := 0; i <= 100; i++ { naturalNumberCh <- i } } func read(ctx context.Context, once *sync.Once) { defer wg.Done() threadID := windows.GetCurrentThreadId() //循环监听 for { //监听多个channel select { //1.监听结束信号 case <-ctx.Done(): fmt.Printf("----read-goroutine-%d结束------\n", threadID) return //2.不结束即执行 default: //记得写完了关闭Channel,否则for range循环不结束! for n := range naturalNumberCh { fmt.Printf("----read-goroutine-%d读取到值%d\n", threadID, n) } once.Do(func() { donech <- threadID }) } } } func main() { defer fmt.Println("mian函数结束") ctx, cancel := context.WithCancel(context.Background()) readerCount := 3 writeCount := 1 //开1个写go程 for i := 0; i < writeCount; i++ { go write() } //开3个读go程执行结束后主动通知main函数,发请求结束的请求! for i := 0; i < readerCount; i++ { go read(ctx, &sync.Once{}) } gocount := readerCount + writeCount wg.Add(gocount) //mian函数收到了3个读goroutine发送的请求结束请求,调用cancel主动结束它们! for i := 0; i < readerCount; i++ { fmt.Printf("----read-goroutine-%d请求结束!\n", <-donech) } cancel() wg.Wait() }
1.goroutine资源争用现象
我们知道MySQL客户端用到的数据放在mysqld服务端的数据库中当多个客户端连接数据库时有事会需要加锁操作保证数据安全。
程序中用到变量数据在内存里,我开多个goroutine去同时对同1个全局变量进行修改,相当于多个MySQL的客户端同时对数据库同1条数据进行修改。
var wg sync.WaitGroup //定义1个全局变量 var number int64 //对全局变量进行+1操作 func add1() { for i := 0; i < 5000; i++ { //1.从内存中找到number变量对应的值 //2.进行+1操作 //3.把结果赋值给number写到内存 number++ } wg.Done() } func main() { wg.Add(2) go add1() go add1() //fmt.Println(number) wg.Wait() fmt.Println(number) //每次执行结果都不一致 }
2.sync.Mutex互斥锁
Mutex可以防止同1时刻,同1资源(全局变量)被多个goroutine操作。
互斥锁不区分是读、写操作,只要有1个goruitne拿到Mutax,其余的所有goroutines,无论是读还写,只能等待。
Mextex是使用struct实现的而在golang中struct属于value类型。
需要注意的是在使用sync.Mutex时如果把它当成参数传入到函数里面,mutax就会被copy生成2把不同的mutex。
var lock sync.Mutex lock.Lock() //加锁 lock.Lock() //加锁
1个公共资源被N个goroutines 操作引发的问题
package main import ( "fmt" "sync" ) //锁 var x = 0 var wg sync.WaitGroup //每次执行add增加5000 func add() { defer wg.Done() for i := 0; i < 5000; i++ { x++ } } func main() { wg.Add(2) //开启2个goroutines同时对x+1 go add() go add() /*2个goroutines如果同1时刻都去获取公共变量x=50, 然后在独自的栈中对x+1改变了x都=51 就少+了1次,导致结果计算不准! */ wg.Wait() fmt.Println(x) }
3.使用互斥锁
package main import ( "fmt" "sync" ) //锁 var x = 0 var wg sync.WaitGroup /* A Mutex must not be copied after first use. 使用互斥锁一定要确保该锁不是复制品(作为参数传递时一定要传指针) */ //互斥锁 var lock sync.Mutex //每次执行add增加5000 func add() { defer wg.Done() for i := 0; i < 5000; i++ { lock.Lock() //加锁 x++ //操作同1资源 lock.Unlock() //释放锁 } } func main() { wg.Add(2) //开启2个goroutines同时对x+1 go add() go add() wg.Wait() fmt.Println(x) }
4.RWMutex(读/写互斥锁)
使用数据库时我们大部分的场景都是读的频率高于写的频率,所以我们可以使用2个数据库,1个叫主库另1个叫从库,主库支持写操作,从库支持度操作,主从之间通过bin log同步数据。
如果现在数据在内存中放着也是读变量的频率远远高于修改变量的频率。我们可以使用RWmutex
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync
包中的RWMutex
类型。
读写锁分为两种:
读锁:当一个goroutine获取读锁之后,其他的goroutine
如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
写锁:当一个goroutine
获取写锁之后,其他的goroutine
无论是获取读锁还是写锁都会等待;
var rwlock sync.RWMutex //读锁 rwlock.RLock() rwlock.RUnlock() //写锁 rwlock.Lock() rwlock.Unlock()
Rwmutex区分goroutine读、写操作,仅在写时资源被lock,读的goroutines等待。(读并发、写串行)。
应用场景:所以使用RWMutex之后,在读操作大于写操作次数的场景下并发执行效率会比Mutex更快。
如果读和写的操作差别不大,读写锁的优势就发挥不出来。
package main import ( "fmt" "sync" "time" ) var x = 0 var lock sync.Mutex var rwlock sync.RWMutex var wg sync.WaitGroup //rwlock func read() { defer wg.Done() //加普通互斥锁 // lock.Lock() //加读锁 rwlock.RLock() fmt.Println(x) time.Sleep(time.Millisecond) //释放普通互斥锁 // lock.Unlock() //释放读锁 rwlock.RUnlock() } func write() { defer wg.Done() // lock.Lock() //加写锁 rwlock.Lock() x++ time.Sleep(10 * time.Millisecond) // lock.Unlock() //释放写锁 rwlock.Unlock() } func main() { start := time.Now() for i := 0; i < 10; i++ { go write() wg.Add(1) } //读的次数一定要大于写的次数 for i := 0; i < 1000; i++ { go read() wg.Add(1) } wg.Wait() fmt.Println(time.Now().Sub(start)) //Mutex:1.205s //RWMutex 194ms }
6.sync.WaitGroup
var wg sync.WaitGroup wg.Add(2) wg.Done(2) wg.Wait()
主goroutine结束之后,又它开启的其他goroutines会自动结束!!
如何做到让main goroutine等待它开启的goroutines结束之后,再结束呢?
main goroutine执行time.Sleep(duration)
肯定是不合适,因为我们无法精确预测出 goroutines到底会执行多久?
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 计数器+delta |
(wg *WaitGroup) Done() | 计数器-1 |
(wg *WaitGroup) Wait() | 阻塞直到计数器变为0 |
var wg sync.WaitGroup func hello() { defer wg.Done() fmt.Println("Hello Goroutine!") } func main() { wg.Add(1) go hello() // 启动另外一个goroutine去执行hello函数 fmt.Println("main goroutine done!") wg.Wait() }
7.sync.Once
如何确保某些操作在并发的场景下只执行1次,例如只加载一次配置文件、只执行1次close(channel)等。
func (o *Once) Do(f func()) {}
Onece的Do方法只能接受1个没有参数的函数作为它的参数, 如果要传递的func参数是有参数的func, 就需要搭配闭包来使用。
下面是借助sync.Once
实现的并发安全的单例模式:
package singleton import ( "sync" ) type singleton struct {} var instance *singleton var once sync.Once func GetInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance }
8.sync.Map
Golang内置的Map数据类型是非线程安全的Map;
sync包提供了1个线程安全的Map即sync.map。
sync.map内部使用2个map即read和dirty,实现读写分离的机制;
type Map struct { mu Mutex // 把read看成一个安全的只读快照表,实际对应的是readOnly, read atomic.Value // readOnly // dirty需要使用上面的mu加锁才能访问里面的元素, //dirty中包含所有在read字段中但未被expunged(删除)的元素, //重点包含最新的 KV 对,等时机成熟,dirty 会被转换为 read, 然后该字段会被置为空 dirty map[interface{}]*entry // misses是一个计数器,记录在从read中读取数据的时候,没有命中的次数, //每次从 read 中没找到回到 dirty 中查询都会导致 misses 自增一, //当misses > len(dirty) 时,就会触发dirty转换 misses int }
在读取时不需要加锁,在写入时则会进行细粒度的锁定,以保证数据的一致性和并发安全性;
var syncMap sync.Map //新增 syncMap.Store(key, n) //删除 syncMap.Delete(key) //改 syncMap.LoadOrStore(key) //遍历 syncMap.Range(walk)
golang中的map在并发情况下: 只读是线程安全的,但是写线程不安全,所以为了并发安全 & 高效,官方帮我们实现了另1个sync.map。
fatal error: concurrent map writes //go内置的map只能支持20个并发写!
package main import ( "fmt" "strconv" "sync" ) var m = make(map[string]int) func get(key string) int { return m[key] } func set(key string, value int) { m[key] = value } func main() { wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go func(n int) { key := strconv.Itoa(n) //设置1个值 set(key, n) //获取1个值 fmt.Printf("k=:%v,v:=%v\n", key, get(key)) wg.Done() }(i) } wg.Wait() }
就支持20个并发也太少了!
Go语言的sync
包中提供了一个开箱即用的并发安全版map–sync.Map
。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。
同时sync.Map
内置了诸如Store
、Load
、LoadOrStore
、Delete
、Range
等操作方法。
package main
import (
"fmt"
"strconv"
"sync"
)
var syncMap sync.Map
var wg sync.WaitGroup
func walk(key, value interface{}) bool {
fmt.Println("即将删除Key =", key, "Value =", value)
syncMap.Delete(key)
return true
}
func main() {
for i := 0; i < 200; i++ {
//开启20个协程去syncMap并发写操作,也是可以顺利写进去的的!
key := strconv.Itoa(i)
wg.Add(1)
go func(n int) {
//设置key
syncMap.Store(key, n)
//通过key获取value
value, ok := syncMap.Load(key)
if !ok {
fmt.Println("没有该key", key)
}
fmt.Println(value)
wg.Done()
}(i)
}
//使用for 循环或者 for range 循环无法遍历所有syncMap只能使用syncMap.Range()
//不幸运的Go没有提供sync.Map的Length的方法,需要自己实现!!
syncMap.Range(walk)
wg.Wait()
}
atomic包
Golang的sync.Mutex锁的底层都是通过atomic来实现的;
原子操作概念
在程序中执行1行内容为a=a+1的代码时,计算机底层的CPU其实是分了多个步骤去处理它;
- 从内存中获取a变量原值
- 对a变量原值进行+1操作
- +1操作结果赋值给变量a
以上多个步骤处理,那么就意味着有中间状态(操作中、没操作完的状态);
而原子操作,它是一个不可分割的整体,没有中间状态,要么成功了、要么失败了。
原子操作需要通过给底层的CPU发送原子操作指令去实现。
在并发读写模式下,在变量级别使用原子操作的好处是在多Goroutine并发操作的同1个变量时
- 可以保证当前变量值一致性
- 比sync.Mutex锁的粒度更小,性能更快
原子操作函数
atimic包提供了一组原子操作函数,用于对值类型的变量进行原子操作;
方法 | 解释 |
---|---|
func LoadInt32(addr *int32) (val int32) func LoadInt64(addr *int64) (val int64) func LoadUint32(addr *uint32) (val uint32) func LoadUint64(addr *uint64) (val uint64) func LoadUintptr(addr *uintptr) (val uintptr) func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) |
读取操作 |
func StoreInt32(addr *int32, val int32) func StoreInt64(addr *int64, val int64) func StoreUint32(addr *uint32, val uint32) func StoreUint64(addr *uint64, val uint64) func StoreUintptr(addr *uintptr, val uintptr) func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) |
写入操作 |
func AddInt32(addr *int32, delta int32) (new int32) func AddInt64(addr *int64, delta int64) (new int64) func AddUint32(addr *uint32, delta uint32) (new uint32) func AddUint64(addr *uint64, delta uint64) (new uint64) func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) |
修改操作 |
func SwapInt32(addr *int32, new int32) (old int32) func SwapInt64(addr *int64, new int64) (old int64) func SwapUint32(addr *uint32, new uint32) (old uint32) func SwapUint64(addr *uint64, new uint64) (old uint64) func SwapUintptr(addr *uintptr, new uintptr) (old uintptr) func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) |
交换操作 |
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool) func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) |
比较并交换操作 |
ackage main import ( "fmt" "sync" "sync/atomic" "time" ) type Counter interface { Inc() Load() int64 } // 普通版 type CommonCounter struct { counter int64 } func (c CommonCounter) Inc() { c.counter++ } func (c CommonCounter) Load() int64 { return c.counter } // 互斥锁版 type MutexCounter struct { counter int64 lock sync.Mutex } func (m *MutexCounter) Inc() { m.lock.Lock() defer m.lock.Unlock() m.counter++ } func (m *MutexCounter) Load() int64 { m.lock.Lock() defer m.lock.Unlock() return m.counter } // 原子操作版 type AtomicCounter struct { counter int64 } func (a *AtomicCounter) Inc() { atomic.AddInt64(&a.counter, 1) } func (a *AtomicCounter) Load() int64 { return atomic.LoadInt64(&a.counter) } func test(c Counter) { var wg sync.WaitGroup start := time.Now() for i := 0; i < 1000; i++ { wg.Add(1) go func() { c.Inc() wg.Done() }() } wg.Wait() end := time.Now() fmt.Println(c.Load(), end.Sub(start)) } func main() { c1 := CommonCounter{} // 非并发安全 test(c1) c2 := MutexCounter{} // 使用互斥锁实现并发安全 test(&c2) c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高 test(&c3) }