系列文章目录
😯编程就是手艺活,看了就要练,看了就要写。不写不练浪费时间撒。
🚗🚗🚗🚗今天先搞起一个Go并发技术错误,并使用一些常见方法实现正确并发编程。
目录
一、go语言并发简介
并发编程的目的主要利用多核CPU并发执行,提升程序计算性能。使用多核CPU带来了一个挑战就是,每一个CPU都有自己的缓存,CPU执行运算时,都是把数据先加载到自己的缓存中使用,加载过程中,或者计算过程中,其他CPU已经对这个变量进行了修改,导致自己没办法拿到最新的数据计算。
简单的概括就是:🚗使用并发编程时,不可避免的就是临界区(公共资源的修改),抽象的数据操作行为是RAW,WAR,WAW。🥤🥤🥤
go语言为其线程创造了一个独特名字:goroutine。所有goroutline都使用同一块内存,所以goroutine当中使用的数据都必须要加锁使用,否则就会出现数据冲突🦴🦴🦴。
这样:
二、🍻go语言并发小试牛刀
在第一天里面,主要是初步了解go当中的并发工具,以及并发编程应该是什么样的一个感观认识。
2.1🍺🍺🍺 原生态并发计数
代码exam3实现的是并发开启十个线程计数,每个线程分别计数10次,正确的计数结果应该是1000次。
func exam1() {
group := sync.WaitGroup{}
count := 0
for i := 0; i < 100; i++ {
group.Add(1)
go func() {
for i := 0; i < 10; i++ {
count++
}
group.Done()
}()
}
group.Wait()
fmt.Printf("100 goroutine count 10 times is %v times", count)
}
实际结果却是: 990次
这就奇怪了,不应该呀。应该是1000的。
值对不上的原因就是:
当go开启一个goroutine的时候,每一个goroutine都对count变量进行修改。👾没有加锁!
原因分析:
下面就使用go -race检一下哪里有冲突。
哦豁,go -race检查以后,居然是一个正确的结果!!为啥呢
从下文前两行,可以看出0x00c00012e058 这个地址发生了RAW操作。虽然这个结果最后是1000次,这是操作系统自身缓存的原因优化出来的结果,属于运气。goroutine内部计数次数变化的话,count总值就不对了!
WARNING: DATA RACE
Read at 0x00c00012e058 by goroutine 8:
main.exam1.func1()
/MyOwnExercise.go:51 +0x4c
Previous write at 0x00c00012e058 by goroutine 7:
main.exam1.func1()
/MyOwnExercise.go:51 +0x64
Goroutine 8 (running) created at:
main.exam1()
/MyOwnExercise.go:49 +0xf3
main.main()
/MyOwnExercise.go:14 +0x36
Goroutine 7 (finished) created at:
main.exam1()
/MyOwnExercise.go:49 +0xf3
main.main()
/MyOwnExercise.go:14 +0x36
==================
100 goroutine count 10 times is 1000 timesFound 1 data race(s)
exit status 66
2.1 🍭🍭🍭遇事不决,加把锁
加锁,使用mutex解决线程并发问题 。加锁通俗的说就是对资源上锁,拿到钥匙的人才能访问,控制住了钥匙,⚓️就能控制住访问顺序。
func exam2() {
//增加锁
mutex := sync.Mutex{}
group := sync.WaitGroup{}
count := 0
for i := 0; i < 100; i++ {
group.Add(1)
go func( ) {
//加锁
mutex.Lock()
for i := 0; i < 10000; i++ {
count++
}
//解锁
mutex.Unlock()
group.Done()
}()
}
group.Wait()
fmt.Printf("100 goroutine count 1000 times is %v times", count)
}
执行结果:显然。正确答案,数据无冲突。
go run -race .\MyOwnExercise.go
100 goroutine count 1000 times is 1000000 times
2.2 🍖🍖🍖加锁慢又麻,原子操作好快省
原理:就是每次对数据的操作,都不经过缓存,直接在数据地址上对数据进行操🌆作。
func atomicExam3() {
group := sync.WaitGroup{}
var count int32
for i := 0; i < 100; i++ {
group.Add(1)
go func( ) {
for i := 0; i < 10000; i++ {
atomic.AddInt32(&count,1)
}
group.Done()
}()
}
group.Wait()
fmt.Printf("100 goroutine count 1000 times is %v times", atomic.LoadInt32(&count))
}
结论:那当然是正确了。博客里面怎么会有不对的答案
go run -race .\MyOwnExercise.go
100 goroutine count 1000 times is 1000000 times
2.3 🍟🍟🍟channel,go并发灵魂
嗯,原理就是你发过来,我收着,就是一个半双工通信的模式。通过半双工🌠🌠,实现对修改数据的并发操作。
func channelExam3() {
group := sync.WaitGroup{}
resChann := make(chan int,1)
resChann<-0
for i := 0; i < 100; i++ {
group.Add(1)
go func( ) {
for i := 0; i < 10000; i++ {
i2 := <-resChann
i2++
resChann<-i2
}
group.Done()
}()
}
group.Wait()
fmt.Printf("100 goroutine count 1000 times is %v times", <-resChann)
}
执行结果:毫无意外,又是正确执行。
go run -race .\MyOwnExercise.go
100 goroutine count 1000 times is 1000000 times
三、🍏🍏🍏总结
今天介绍了一下,并发编程为什么会失败,为什么会有并发编程。如何通过加锁、原子操作、通道来分别实现并发次数。后续的文章中,会继续拓展、深化go并发编程原理和生产案例💪🏻⛽️~
🧧🧧🧧感谢诸位大佬的阅读,点个关注,收藏一下呗~