Go 资源竞争的解决方案 (1. 原子函数 / 互斥锁 2. 通道)

2,105 阅读4分钟

资源竞争

如果多个goroutine在没有互相同步的情况,访问某个共享的资源, 并试图同时读和写这个资源,就处于相互竞争的状态, 这种情况被称作竞争状态(race candition)。 竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必 须是原子化的。(即同一时刻只能有一个 goroutine 对共享资源进行读和写操作)

代码解析

// 这个示例程序展示如何在程序里造成竞争状态
// 实际上不希望出现这种情况
package main

import (
	"fmt"
	"runtime"
	"sync"
)
var (
	counter int             // counter 是所有 goroutine 都要增加其值的变量
	wg sync.WaitGroup       // wg 用来等待程序结束
)

// main 是所有 Go 程序的入口
func main() {
	// 计数加 ,表示要等待两个 goroutine
	wg.Add()
	// 创建两个 goroutine
	go incCounter()
	go incCounter()
	// 等待 goroutine 结束
	wg.Wait()
	fmt.Println("Final Counter:", counter)
}

// incCounter 增加包里 counter 变量的值
func incCounter(id int) {
	// 在函数退出时调用 Done 来通知 main 函数工作已经完成
	defer wg.Done()

	for count :=; count <; count++ {
		// 捕获 counter 的值
		value := counter

		// 当前 goroutine 从线程退出,并放回到队列
		runtime.Gosched()

		// 增加本地 value 变量的值
		value++

		// 将该值保存回 counter
		counter = value
	}
}

/*
    在这段代码中
    变量counter会进行4次读和写操作,每个goroutine执行两次。
    但是,程序终止时,counter变量的值为 2。
    下图 提供了为什么会这样的线索。
    每个 goroutine 都会覆盖另一个 goroutine 的工作。
    这种覆盖发生在 goroutine 切换的时候。
    每个 goroutine 创造了一个 counter 变量的副本,
    之后就切换到另一个 goroutine。当这个 goroutine再次运行的时候,
    counter 变量的值已经改变了,但是 goroutine 并没有更新自己的那个副本的值,
    而是继续使用这个副本的值,用这个值递增,并存回 counter 变量
    结果覆盖了另一个goroutine 完成的工作。
*/

Go 语言中通过三种方式 处理竞争状态的情况

锁住共享资源

1. 原子函数

使用atmoic 包内的相关函数执行原子操作 对数据进行安全的读和写 
保证数据不会出现覆的情况 
atomic.LoadInt64(&shutdown)        // 同步获取该数据的值 
atomic.StoreInt64(&shutdown, 1);   // 同步写入该数据的值

2. 互斥锁

/*
    通过 mutex.Lock() mutex.Unlock() 划分临界区
    被包裹在临界区内的代码 同一个时间点只能被一个goroutine 执行
*/

// incCounter 使用互斥锁来同步并保证安全访问,
// 增加包里 counter 变量的值

func incCounter(id int) {
	// 在函数退出时调用 Done 来通知 main 函数工作已经完成
	defer wg.Done()
	for count := 0; count < 2; count++ {
		// 同一时刻只允许一个 goroutine 进入
		// 这个临界区
		mutex.Lock()     // <<<<<<<<<<<<<<<<<<<<< 互斥锁开始语句
		{
			// 捕获 counter 的值
			value := counter
			// 当前 goroutine 从线程退出,并放回到队列
			runtime.Gosched()
			// 增加本地 value 变量的值
			value++
			// 将该值保存回 counter
			counter = value
		}
		mutex.Unlock()   // <<<<<<<<<<<<<<<<<<<<< 互斥锁结束语句
		// 释放锁,允许其他正在等待的 goroutine
		// 进入临界区
	}
}

3. 通道

通过使用通道发送和接收需要共享的资源,可以在goroutine之间做数据同步。
当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,
并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。
可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
// 无缓冲的整型通道
unbuffered := make(chan int)
// 有缓冲的字符串通道
buffered := make(chan string, 10)


// 通过通道发送一个字符串
buffered <- "Gopher"
// 从通道接收一个字符串
value := <-buffered

对于有缓冲和无缓冲的通道的区别可以看下面的这张图 详细地解释其中的差异性

无缓冲 的情况下 需要接送方与发送方 同时连接上. 若一方不存在 则会处于阻塞状态

有缓冲 的情况下 不需要双方同时连接上 只要有让发送者有空间可以存放 接收者有数据可以取出 就不会出现阻塞