概览

Golang虽然原生支持并发,但是在并发情形下仍然存在竞争状态。在多线程未同步的状态下同时读或者写同一个变量时,此时往往会出现数据结果与预期不一致的情况。

数据竞争状态举例

/*
Author: William Kennedy(Go语言实战作者)
Date:2019/5/11
 */

package main;

import (
    "fmt"
    "sync"
    "runtime"
)

var(
    counters int64
    wg sync.WaitGroup
)

func main(){
    wg.Add(2)
    go incCounters(1)
    go incCounters(2)

    wg.Wait()
    fmt.Printf("Final value is %d", counters)

}

func incCounters(id int){
    defer wg.Done()

    for count := 0; count < 2; count++{
        value := counters
        runtime.Gosched()
        value++
        counters = value
    }
}

在第17行我们声明了一个WaitGroup变量,该变量实际是个计数器。我们可以通过该变量的Add()设置该计数器的初始值,当该计数器不为0时,它将阻塞。每当调用一次该变量的Done()方法,该计数器的值就会-1。通过WaitGroup,我们可以保证main函数不会在Goroutine还没运行完毕的时候退出。

在第16行我们声明了一个int64类型的counters变量,然后在incCounters函数中我们对counters变量进行连续两次自增操作。在每次自增过程中,我们首先将counters变量赋值给value,然后调用了runtime.Gosched函数,该函数会使当前Goroutine放弃当前cpu时间,重新进入Runable队列,等待调度器再次调度,而此时的cpu将会让给其他Goroutine继续运行,另一个Goroutine同样执行相似的流程,当之前的Goroutine再次获得cpu时间时,此时value值加1,然后将value变量赋值给counters。所以上述的代码执行流程可以简化为下图。

![](/assets/wp-content/uploads/2019/05/untitled.png)
数据竞争流程图(摘自《Go语言实战》)
![](/assets/wp-content/uploads/2019/05/数据竞争结果.png)
数据竞争结果

我们一共对counters变量进行了4次自增操作,按照我们预期,counters变量本该最终值为4,但是实际最终的值为2。这是因为我们在两个线程对同一个变量进行同时读写的时候没有同步造成的。

同步方法

原子操作

原子操作采用的底层的加锁机制,保证从动作的开始到动作的结束,不会被线程调度机制打断。

package main;

import (
    "fmt"
    "sync"
    "runtime"
    "sync/atomic"
)

var(
    counters int64
    wg sync.WaitGroup
)

func main(){
    wg.Add(2)
    go incCounters(1)
    go incCounters(2)

    wg.Wait()
    fmt.Printf("Final value is %d", counters)

}

func incCounters(id int){
    defer wg.Done()

    for count := 0; count < 2; count++{

        atomic.AddInt64(&counters, 1)
        runtime.Gosched()
    }
}

相比较于上一段代码,本段代码的for循环不再是赋值给中间变量,中间变量自增后再赋值给原变量。而是直接调用了atomic包的AddInt64函数,该函数第一个传入参数是需要修改的变量的地址,第二个参数是需要增加的值。在该函数的调度过程中,不会被调度器打断。

![](/assets/wp-content/uploads/2019/05/捕获.png)
原子操作结果

互斥操作

互斥操作通过添加互斥锁来保证一段代码在同一时间内只有一个Goroutine在执行。

/*
Author: William Kennedy(Go语言实战作者)
Date:2019/5/11
 */

package main;

import (
    "fmt"
    "sync"
    "runtime"
)

var(
    counters int64
    wg sync.WaitGroup
    mutex sync.Mutex
)

func main(){
    wg.Add(2)
    go incCounters(1)
    go incCounters(2)

    wg.Wait()
    fmt.Printf("Final value is %d", counters)

}

func incCounters(id int){
    defer wg.Done()

    for count := 0; count < 2; count++{
        mutex.Lock()
        value := counters
        runtime.Gosched()
        value++
        counters = value
        mutex.Unlock()
    }
}

在第18行,我们声明了一个互斥锁变量mutex,第35行我们通过mutex.Lock操作对之后的代码段加锁,在这段代码之间,同一时刻只允许一个Goroutine运行。在第40行,我们通过mutex.Unlock操作释放锁,此时其他Goroutine可以通过mutex.Lock操作获得锁资源,从而执行该段代码。

代码执行结果如下图

![](/assets/wp-content/uploads/2019/05/捕获.png)
加互斥锁操作结果

通道

通过预先声明指定类型的通道,在Goroutine之间提高了一种数据同步的机制。

在Golang中,通道分为有缓冲的通道和无缓冲的通道。我们一般通过make函数事先声明一个传递指定类型数据的通过。例如:通过make(chan int),我们可以生成一个可以传递int类型通道。而通过make(chan int, 10)可以创建一个包含10个值的缓冲int通道。无缓冲的通道需要生产者和消费者同步进行生产和消费的动作,如果其中一方尚未准备好,那么已经准备好的一方将会进行阻塞,直到对方准备完成再进行交换。而具有缓冲的通道是无需两方进行同步的,只要缓冲区中有足够的空间,生产者在任意时间都可以向通道中生产产品,而消费者则只要在缓冲区中有产品的情况下都可以进行消费操作。下面我们将演示最简单的在没有缓冲的通道下生产者和消费者的问题。

/*
Author: Spike
Date:2019/5/13
 */

package main

import(
    "fmt"
)

func consumer(done chan bool,data chan int){
    for{
        num, ok := <- data
        if ok{
            fmt.Printf("Consumed NO: %d\n", num)
        }else {
                        fmt.Println("All production consumed, exit!")
            break
                }
    }
    done <- true
}

func producer(data chan int){
    for i:=1; i<10; i++{
        fmt.Printf("Production NO: %d\n", i)
        data <- i
    }
    close(data)
}

func main(){
    data := make(chan int)
    done := make(chan bool)
    go producer(data)
    go consumer(done, data)
    if  <-done{
        close(done)
    }
}
![](/assets/wp-content/uploads/2019/05/捕获-1.png)
Golang生产者消费者运行结果