1、前言
编写正确的程序自己就不容易,编写正确的并发程序更是难中之难,那么并发编程究竟难道哪里那?本节咱们就来一探究竟。java
2、数据竞争的存在
当两个或者多个线程(goroutine)在没有任何同步措施的状况下同时读写同一个共享资源时候,这多个线程(goroutine)就处于数据竞争状态,数据竞争会致使程序的运行结果超出写代码的人的指望。下面咱们来看个例子:编程
package mainimport ( "fmt")var a int//goroutine1func main() { //1,gouroutine2 go func(){ a = 1//1.1 }() //2 if 0 == a{//2.1 fmt.Println(a)//2.2 }}
安全
微信
并发
app
函数
flex
ui
atom
如上代码首先建立了一个int类型的变量,默认被初始化为0值,运行main函数会启动一个进程和这个进程中的一个运行main函数的goroutine(轻量级线程)
在main函数内使用go语句建立了一个新的goroutine(该goroutine运行匿名函数里面的内容)并启动运行,匿名函数内给变量赋值为1
main函数里面代码2判断若是变量a的值为0,则打印a的值。
运行main函数后,启动的进程里面存在两个并发运行的线程,分别是开启的新goroutine(起名为goroutine2)和main函数所在的goroutine(起名为goroutine1),前者试图修改共享变量a,后者试图读取共享变量a,也就是存在两个线程在没有任何同步的状况下对同一个共享变量进行读写访问,这就出现了数据竞争,因为数据竞争存在,致使上面程序可能会有下面三种输出:
输出0,因为运行时调度系统的随机性,会存在goroutine1的2.2代码比goroutine2的代码1.1先执行
输出1,当存在goroutine1先执行代码2.1,而后goroutine2在执行代码1.1,最后goroutine1在执行代码2.2的时候
什么都不输出,当goroutine2执行先于goroutine1的2.1代码时候。
因为数据竞争的存在上面一段很短的代码会有三种可能的输出,究其缘由是goroutine1和groutine2的运行时序是不肯定的,也就是没有对他们的操做作同步,以便让这些内存操做变为能够预知的顺序执行。
这里编写程序者或许受单线程模型的影响认为代码1.1会先于代码2.1执行,当发现输出不符合预期时候,或许会在代码2.1前面让goroutine1 休眠一会确保goroutine2执行完毕1.1后在让goroutine1执行2.1,这看起来或许有效,可是这是很是低效,而且并非全部状况下均可以解决的。
正确的作法可使用信号量等同步措施,保证goroutine2执行完毕再让goroutine1执行代码2.1,以下面代码,咱们使用sync包的WaitGroup来保证goroutine2执行完毕代码2.1后,goroutine1才能够执行步骤4.1,关于WaitGroup后面章节咱们具体会讲解:
package mainimport ( "fmt" "sync")var a intvar wg sync.WaitGroup//信号量//goroutine1func main() { //1. wg.Add(1);//一个信号 //2. goroutine1 go func(){ a = 1//2.1 wg.Done() }() wg.Wait()//3. 等待goroutine1运行结束 //4 if 0 == a{//4.1 fmt.Println(a)//4.2 }}
3、操做的原子性
所谓原子性操做是指当执行一系列操做时候,这些操做那么所有被执行,那么所有不被执行,不存在只执行其中一部分的状况。在设计计数器时候通常都是先读取当前值,而后+1,而后更新,这个过程是读-改-写的过程,若是不能保证这个过程是原子性,那么就会出现线程安全问题。以下代码是线程不安全的,由于不能保证a++是原子性操做:
package mainimport ( "fmt" "sync")var count int32var wg sync.WaitGroup //信号量const THREAD_NUM = 1000//goroutine1func main() { //1.信号 wg.Add(THREAD_NUM) //2. goroutine for i := 0; i < THREAD_NUM; i++ { go func() { count++//2.1 wg.Done()//2.2 }() } wg.Wait() //3. 等待goroutine运行结束 fmt.Println(count) //4输出计数}
如上代码在main函数所在为goroutine内建立了THREAD_NUM个goroutine,每一个新的goroutine执行代码2.1对变量count计数增长1。
这里建立了THREADNUM个信号量,用来在代码3处等待THREADNUM个goroutine执行完毕,而后输出最终计数,执行上面代码咱们 指望输出1000,可是实际却不是。
这是由于a++操做自己不是原子性的,其等价于b := count;b=b+1;count=b;是三步操做,因此可能致使致使计数不许确,以下表:
假如当前count=0那么t1时刻线程A读取了count值到变量countA,而后t2时刻递增countA值为1,同时线程B读取count的值0放到内存countB值为0(由于countA尚未写入主内存),t3时刻线程A才把countA为1的值写入主内存,至此线程A一次计数完毕,同时线程B递增CountB值为1,t4时候线程B把countB值1写入内存,至此线程B一次计数完毕。明明是两次计数,最后结果是1而不是2。
上面的程序须要保证count++的原子性才是正确的,后面章节会知道使用sync/atomic包的一些原子性函数或者锁能够解决这个问题。
package mainimport ( "fmt" "sync" "sync/atomic")var count int32var wg sync.WaitGroup //信号量const THREAD_NUM = 1000//goroutine1func main() { //1.信号 wg.Add(THREAD_NUM) //2. goroutine for i := 0; i < THREAD_NUM; i++ { go func() { //count++// atomic.AddInt32(&count, 1)//2.1 wg.Done()//2.2 }() } wg.Wait() //3. 等待goroutine运行结束 fmt.Println(count) //4输出计数}
如上代码使用原子性操做能够保证每次输出都是1000
4、内存访问同步
上节原子性操做第一个例子有问题是由于count++操做是被分解为相似b := count;b=b+1;count =b; 的三部操做,而多个goroutine同时执行count++时候并非顺序执行者三个步骤的,而是可能交叉访问的。因此若是能对内存变量的访问添加同步访问措施,就能够避免这个问题:
package mainimport ( "fmt" "sync")var count int32var wg sync.WaitGroup //信号量var lock sync.Mutex //互斥锁const THREAD_NUM = 1000//goroutine1func main() { //1.信号 wg.Add(THREAD_NUM) //2. goroutine for i := 0; i < THREAD_NUM; i++ { go func() { lock.Lock() //2.1 count++ //2.2 lock.Unlock() //2.3 wg.Done() //2.4 }() } wg.Wait() //3. 等待goroutine运行结束 fmt.Println(count) //4输出计数}
如上代码建立了一个互斥锁lock,而后goroutine内在执行count++前先获取锁,执行完毕后在释放锁。
当1000个goroutine同时执行到代码2.1时候只有一个线程能够获取到锁,其余的线程被阻塞,直到获取到锁的goroutine释放了锁。也就是这1000个线程的并发行使用锁转换为了串行执行,也就是对共享内存变量的访问施加了同步措施。
5、总结
本文咱们从数据竞争、原子性操做、内存同步三个方面探索了并发编程到底难在哪里,后面章节咱们会结合go的内存模型和happen-before原则在具体探索这些难点如何解决。
本文分享自微信公众号 - 技术原始积累(gh_805ebfd2deb0)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。