本文参考 《Go 语言实战》

1. 竞争状态简述

如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。

对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。

当某些东西被认为是原子的,或者具有原子性的时候,这意味着在它运行的环境中,它是不可分割的或不可中断的。

// 这个示例程序展示如何在程序里造成竞争状态
// 实际上不希望出现这种情况
package mainimport ("fmt""runtime""sync"
)var (// counter是所有goroutine都要增加其值的变量counter int// wg用来等待程序结束wg sync.WaitGroup
)// main是所有Go程序的入口
func main() {// 计数加2,表示要等待两个goroutinewg.Add(2)// 创建两个goroutinego incCounter(1)go incCounter(2)// 等待goroutine结束wg.Wait()fmt.Println("Final Counter:", counter)
}// incCounter增加包里counter变量的值
func incCounter(id int) {// 在函数退出时调用Done来通知main函数工作已经完成defer wg.Done()for count := 0; count < 2; count++ {// 捕获counter的值value := counter// 当前goroutine从线程退出,并放回到队列/*用于将 goroutine 从当前线程退出,给其他 goroutine 运行的机会。在两次操作中间这样做的目的是强制调度器切换两个 goroutine,以便让竞争状态的效果变得更明显。*/runtime.Gosched()// 增加本地value变量的值value++// 将该值保存回countercounter = value}
}

输出:

Final Counter: 2

变量 counter 会进行 4 次读和写操作,每个 goroutine 执行两次。但是,程序终止时, counter 变量的值为2。

每个 goroutine 都会覆盖另一个 goroutine 的工作。这种覆盖发生在 goroutine 切换的时候。每个 goroutine 创造了一个 counter 变量的副本,之后就切换到另一个 goroutine

当这个 goroutine 再次运行的时候, counter 变量的值已经改变了,但是 goroutine 并没有更新自己的那个副本的值,而是继续使用这个副本的值,用这个值递增,并存回 counter 变量,结果覆盖了另一个 goroutine 完成的工作。

图: 竞争状态下程序行为的图像表达

2. 锁住共享资源

Go 语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。如果需要顺序访问一个整型变量或者一段代码, atomicsync 包里的函数提供了很好的解决方案。

下面我们了解一下 atomic 包里的几个函数以及 sync 包里的 mutex 类型。

2.1 原子函数

原子函数能够以很底层的加锁机制来同步访问整型变量和指针.

示例 1:

package mainimport ("fmt""sync/atomic"
)var (// 序列号seq int64
)// 序列号生成器
func GenID() int64 {// 尝试原子的增加序列号// 这里故意没有使用 atomic .Addlnt64()的返回值作 为 GenID () 函数的返// 回值,因此会造成一个竞态问题atomic.AddInt64(&seq, 1)return seq
}func main() {// 10个并发序列号生成for i := 0; i < 10; i++ {go GenID()}fmt.Println(GenID())
}

在运行程序时,为运行参数加入 -race 参数,开启运行时( runtime )对竞态问题的分析,命令如下:

wohu@wohu-dev:~/gocode/src$ go run -race temp.go
==================
WARNING: DATA RACE
Write at 0x0000005f8178 by goroutine 8:sync/atomic.AddInt64()/usr/local/go/src/runtime/race_amd64.s:276 +0xbmain.GenID()/home/wohu/gocode/src/temp.go:16 +0x43Previous read at 0x0000005f8178 by goroutine 7:main.GenID()/home/wohu/gocode/src/temp.go:17 +0x53Goroutine 8 (running) created at:main.main()/home/wohu/gocode/src/temp.go:23 +0x4fGoroutine 7 (finished) created at:main.main()/home/wohu/gocode/src/temp.go:23 +0x4f
==================
4
Found 1 data race(s)
exit status 66

修改该函数为下面即可正常。

// 序列号生成器
func GenID() int64 {// 尝试原子的增加序列号return atomic.AddInt64(&seq, 1)
}

示例代码 2:

// 这个示例程序展示如何使用atomic包来提供
// 对数值类型的安全访问
package mainimport ("fmt""runtime""sync""sync/atomic"
)var (// counter是所有goroutine都要增加其值的变量counter int32// wg用来等待程序结束wg sync.WaitGroup
)// main是所有Go程序的入口
func main() {// 计数加2,表示要等待两个goroutinewg.Add(2)// 创建两个goroutinego incCounter(1)go incCounter(2)// 等待goroutine结束wg.Wait()fmt.Println("Final Counter:", counter)
}// incCounter增加包里counter变量的值
func incCounter(id int) {// 在函数退出时调用Done来通知main函数工作已经完成defer wg.Done()for count := 0; count < 2; count++ {// 安全地对counter加1atomic.AddInt32(&counter, 1)// 当前goroutine从线程退出,并放回到队列/*用于将 goroutine 从当前线程退出,给其他 goroutine 运行的机会。在两次操作中间这样做的目的是强制调度器切换两个 goroutine,以便让竞争状态的效果变得更明显。*/runtime.Gosched()}
}

输出:

Final Counter: 4

程序的第43行使用了 atmoic 包的 AddInt64 函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 goroutine 运行并完成这个加法操作。

goroutine 试图去调用任何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。

另外两个有用的原子函数是 LoadInt64StoreInt64 。这两个函数提供了一种安全地读和写一个整型值的方式。

如下代码示例程序使用 LoadInt64StoreInt64 来创建一个同步标志,这个标志可以向程序里多个 goroutine 通知某个特殊状态。

// 这个示例程序展示如何使用atomic包里的
// Store和Load类函数来提供对数值类型
// 的安全访问
package mainimport ("fmt""sync""sync/atomic""time"
)var (// shutdown是通知正在执行的goroutine停止工作的标志shutdown int64// wg用来等待程序结束wg sync.WaitGroup
)// main是所有Go程序的入口
func main() {// 计数加2,表示要等待两个goroutinewg.Add(2)// 创建两个goroutinego doWork("A")go doWork("B")// 给定goroutine执行的时间time.Sleep(1 * time.Second)// 该停止工作了,安全地设置shutdown标志fmt.Println("Shutdown Now")atomic.StoreInt64(&shutdown, 1)// 等待goroutine结束wg.Wait()
}// doWork用来模拟执行工作的goroutine,
// 检测之前的shutdown标志来决定是否提前终止
func doWork(name string) {// 在函数退出时调用Done来通知main函数工作已经完成defer wg.Done()for {fmt.Printf("Doing %s Work\n", name)time.Sleep(250 * time.Millisecond)// 要停止工作了吗?if atomic.LoadInt64(&shutdown) == 1 {fmt.Printf("Shutting %s Down\n", name)break}}
}

2.2 互斥锁

另一种同步访问共享资源的方式是使用互斥锁( mutex )。互斥锁这个名字来自互斥(mutual exclusion)的概念。

互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。

// 这个示例程序展示如何使用互斥锁来
// 定义一段需要同步访问的代码临界区
// 资源的同步访问
package mainimport ("fmt""runtime""sync"
)var (// counter是所有goroutine都要增加其值的变量counter int// wg用来等待程序结束wg sync.WaitGroup// mutex 用来定义一段代码临界区mutex sync.Mutex
)// main是所有Go程序的入口
func main() {// 计数加2,表示要等待两个goroutinewg.Add(2)// 创建两个goroutinego incCounter(1)go incCounter(2)// 等待goroutine结束wg.Wait()fmt.Printf("Final Counter: %d\n", counter)
}// 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++// 将该值保存回countercounter = value}mutex.Unlock()// 释放锁,允许其他正在等待的goroutine// 进入临界区}
}

counter 变量的操作在第 46 行和第 60 行的 Lock()Unlock() 函数调用定义的临界区里被保护起来。

同一时刻只有一个 goroutine 可以进入临界区。之后,直到调用 Unlock() 函数之后,其他 goroutine 才能进入临界区。当第 52 行强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运行。当程序结束时,我们得到正确的值 4,竞争状态不再存在。

2.3 读写互斥锁 sync.RWMutex

在读多写少的环境中,可以优先使用读写互斥锁, sync 包中的 RWMutex 提供了读写互斥锁的封装。

package mainimport ("fmt""sync""time"
)var (count int// 变量对应的读写互斥锁countGuard sync.RWMutex
)func GetCount() int {countGuard.RLock()defer countGuard.RUnlock()return count
}func SetCount(c int) {countGuard.Lock(){count += c}countGuard.Unlock()
}func main() {// 可以进行并发安全的设置for i := 0; i < 10; i++ {go SetCount(2)}time.Sleep(2 * time.Second)// 可以进行并发安全的读取fmt.Println(GetCount())
}

Go 学习笔记(23)— 并发(02)[竞争,锁资源,原子函数sync/atomic、互斥锁sync.Mutex]相关推荐

  1. Java学习笔记---多线程并发

    Java学习笔记---多线程并发 (一)认识线程和进程 (二)java中实现多线程的三种手段 [1]在java中实现多线程操作有三种手段: [2]为什么更推荐使用Runnable接口? [3][补充知 ...

  2. 多线程编程学习笔记——使用并发集合(三)

    接上文 多线程编程学习笔记--使用并发集合(一) 接上文 多线程编程学习笔记--使用并发集合(二) 四.   使用ConcurrentBag创建一个可扩展的爬虫 本示例在多个独立的即可生产任务又可消费 ...

  3. StatQuest学习笔记23——RNA-seq简介

    StatQuest学习笔记23--RNA-seq简介 前言--主要内容 这篇笔记是StatQuest系列笔记的第58节,主要内容是讲RNA-seq的原理.StatQuest系列教程的58到62节是协录 ...

  4. 区块链学习笔记23——ETH反思

    区块链学习笔记23--ETH反思 学习视频:北京大学肖臻老师<区块链技术与应用> 笔记参考:北京大学肖臻老师<区块链技术与应用>公开课系列笔记--目录导航页 智能合约真的智能吗 ...

  5. GAMES101-现代计算机图形学学习笔记(作业02)

    GAMES101-现代计算机图形学学习笔记(作业02) Assignment 02 GAMES101-现代计算机图形学学习笔记(作业02) 作业 作业描述 需要补充的函数 思路 结果 原课程视频链接以 ...

  6. 学习笔记-Java并发(一)

    学习笔记-Java并发(一) 目录 学习笔记-Java并发一 目录 Executer Callable和Future 后台线程 线程加入 小计 今天看了这一篇 Java编程思想-java中的并发(一) ...

  7. MIPS汇编语言学习笔记23:if 语句分支指令

    C语言 #include<stdio.h> int main() {int i = 3;if (i < 5){printf("yes!\n");}else{pri ...

  8. r语言c函数怎么用,R语言学习笔记——C#中如何使用R语言setwd()函数

    在R语言编译器中,设置当前工作文件夹可以用setwd()函数. > setwd("e://桌面//") > setwd("e:\桌面\") > ...

  9. 【Java】Java学习笔记(2)——Java面向对象基础作业函数题

    本人私人博客:Megalomania,大部分文章会现在博客上传,有不足之处欢迎指正. 学校小学期Java课程的练习题,留个档便于以后需要时候有例子可以回忆,写的烂的地方请多多包含 1.求两个数值之和 ...

  10. python eval 入门_Python学习笔记整理3之输入输出、python eval函数

    Python学习笔记整理3之输入输出.python eval函数 来源:中文源码网    浏览: 次    日期:2018年9月2日 Python学习笔记整理3之输入输出.python eval函数 ...

最新文章

  1. A definition for the symbol 'symbolName' could not be located
  2. windows主机用scp命令向Linux服务器上传和下载文件
  3. linux服务器nvidia驱动的安装与卸载
  4. kotlin学习笔记——扩展函数(anko)和网络请求
  5. ap drawing 课件_ILITEK TP AP introduction.ppt
  6. html解析のBeautifulSoup
  7. 7-42 行编辑器 (10 分)
  8. 网站登录页面php代码,一个简单的网页密码登陆php代码
  9. Mac下使用Wine安装PowerDesigner15
  10. jdbc数据库配置mysql数据库_JDBC连接MySQL数据库(一)——数据库的基本连接
  11. .Net 开源项目资源大全
  12. Linux(Fedora 20) EFI 启动Windows出错 \EFI\Microsoft\Boot\bootmgfw.efi is missing
  13. 嵌入式系统内存泄漏检测
  14. 关于SVN更新时文件加锁的小结
  15. Java实现自动映射原生JDBC查询出的数据库字段
  16. boost升压斩波电路 分析
  17. css video 样式,css自定义video播放器样式的方法
  18. rocketMq配置外网IP
  19. OpenERP中商品销售的处理及案例解析
  20. jabref java_Jabref安装及使用教程

热门文章

  1. kotlin重写构造方法编译报错:Primary constructor call expected
  2. 解决谷歌浏览器在非https下限制获取多媒体对象(音视频)的解决方式
  3. 【VS实践】VS解决方案中出现无法生成DLL文件
  4. 受用一生的高效 PyCharm 使用技巧(二)pycharm 指定参数运行文件
  5. [实现] 利用 Seq2Seq 预测句子后续字词 (Pytorch)
  6. 2021年华为与小康-北汽-长安
  7. Arm Cortex-M23 MCU,Arm Cortex-M33 MCU与RISC-V MCU技术
  8. PyTorch 自动微分示例
  9. NNVM AI框架编译器
  10. Thrift架构与使用方法