Go 学习笔记(23)— 并发(02)[竞争,锁资源,原子函数sync/atomic、互斥锁sync.Mutex]
本文参考 《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
的机制,就是对共享资源加锁。如果需要顺序访问一个整型变量或者一段代码, atomic
和 sync
包里的函数提供了很好的解决方案。
下面我们了解一下 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
都会自动根据所引用的变量做同步处理。
另外两个有用的原子函数是 LoadInt64
和 StoreInt64
。这两个函数提供了一种安全地读和写一个整型值的方式。
如下代码示例程序使用 LoadInt64
和 StoreInt64
来创建一个同步标志,这个标志可以向程序里多个 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]相关推荐
- Java学习笔记---多线程并发
Java学习笔记---多线程并发 (一)认识线程和进程 (二)java中实现多线程的三种手段 [1]在java中实现多线程操作有三种手段: [2]为什么更推荐使用Runnable接口? [3][补充知 ...
- 多线程编程学习笔记——使用并发集合(三)
接上文 多线程编程学习笔记--使用并发集合(一) 接上文 多线程编程学习笔记--使用并发集合(二) 四. 使用ConcurrentBag创建一个可扩展的爬虫 本示例在多个独立的即可生产任务又可消费 ...
- StatQuest学习笔记23——RNA-seq简介
StatQuest学习笔记23--RNA-seq简介 前言--主要内容 这篇笔记是StatQuest系列笔记的第58节,主要内容是讲RNA-seq的原理.StatQuest系列教程的58到62节是协录 ...
- 区块链学习笔记23——ETH反思
区块链学习笔记23--ETH反思 学习视频:北京大学肖臻老师<区块链技术与应用> 笔记参考:北京大学肖臻老师<区块链技术与应用>公开课系列笔记--目录导航页 智能合约真的智能吗 ...
- GAMES101-现代计算机图形学学习笔记(作业02)
GAMES101-现代计算机图形学学习笔记(作业02) Assignment 02 GAMES101-现代计算机图形学学习笔记(作业02) 作业 作业描述 需要补充的函数 思路 结果 原课程视频链接以 ...
- 学习笔记-Java并发(一)
学习笔记-Java并发(一) 目录 学习笔记-Java并发一 目录 Executer Callable和Future 后台线程 线程加入 小计 今天看了这一篇 Java编程思想-java中的并发(一) ...
- MIPS汇编语言学习笔记23:if 语句分支指令
C语言 #include<stdio.h> int main() {int i = 3;if (i < 5){printf("yes!\n");}else{pri ...
- r语言c函数怎么用,R语言学习笔记——C#中如何使用R语言setwd()函数
在R语言编译器中,设置当前工作文件夹可以用setwd()函数. > setwd("e://桌面//") > setwd("e:\桌面\") > ...
- 【Java】Java学习笔记(2)——Java面向对象基础作业函数题
本人私人博客:Megalomania,大部分文章会现在博客上传,有不足之处欢迎指正. 学校小学期Java课程的练习题,留个档便于以后需要时候有例子可以回忆,写的烂的地方请多多包含 1.求两个数值之和 ...
- python eval 入门_Python学习笔记整理3之输入输出、python eval函数
Python学习笔记整理3之输入输出.python eval函数 来源:中文源码网 浏览: 次 日期:2018年9月2日 Python学习笔记整理3之输入输出.python eval函数 ...
最新文章
- A definition for the symbol 'symbolName' could not be located
- windows主机用scp命令向Linux服务器上传和下载文件
- linux服务器nvidia驱动的安装与卸载
- kotlin学习笔记——扩展函数(anko)和网络请求
- ap drawing 课件_ILITEK TP AP introduction.ppt
- html解析のBeautifulSoup
- 7-42 行编辑器 (10 分)
- 网站登录页面php代码,一个简单的网页密码登陆php代码
- Mac下使用Wine安装PowerDesigner15
- jdbc数据库配置mysql数据库_JDBC连接MySQL数据库(一)——数据库的基本连接
- .Net 开源项目资源大全
- Linux(Fedora 20) EFI 启动Windows出错 \EFI\Microsoft\Boot\bootmgfw.efi is missing
- 嵌入式系统内存泄漏检测
- 关于SVN更新时文件加锁的小结
- Java实现自动映射原生JDBC查询出的数据库字段
- boost升压斩波电路 分析
- css video 样式,css自定义video播放器样式的方法
- rocketMq配置外网IP
- OpenERP中商品销售的处理及案例解析
- jabref java_Jabref安装及使用教程
热门文章
- kotlin重写构造方法编译报错:Primary constructor call expected
- 解决谷歌浏览器在非https下限制获取多媒体对象(音视频)的解决方式
- 【VS实践】VS解决方案中出现无法生成DLL文件
- 受用一生的高效 PyCharm 使用技巧(二)pycharm 指定参数运行文件
- [实现] 利用 Seq2Seq 预测句子后续字词 (Pytorch)
- 2021年华为与小康-北汽-长安
- Arm Cortex-M23 MCU,Arm Cortex-M33 MCU与RISC-V MCU技术
- PyTorch 自动微分示例
- NNVM AI框架编译器
- Thrift架构与使用方法