X

Go并发编程-并发编程难在哪里

一、前言

编写正确的程序本身就不容易,编写正确的并发程序更是难中之难,那么并发编程究竟难道哪里那?本节我们就来一探究竟。

二、数据竞争的存在

当两个或者多个线程(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 }}

  • 如上代码首先创建了一个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 }}三、操作的原子性

所谓原子性操作是指当执行一系列操作时候,这些操作那么全部被执行,那么全部不被执行,不存在只执行其中一部分的情况。在设计计数器时候一般都是先读取当前值,然后+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;是三步操作,所以可能导致导致计数不准确,如下表: