Go语言之并发编程(四)
同步
Go 程序可以使用通道进行多个 goroutine 间的数据交换,但这仅仅是数据同步中的一种方法。通道内部的实现依然使用了各种锁,因此优雅代码的代价是性能。在某些轻量级的场合,原子访问(atomic包)、互斥锁(sync.Mutex)以及等待组(sync.WaitGroup)能最大程度满足需求。
当多线程并发运行的程序竞争访问和修改同一块资源时,会发生竞态问题。
下面的代码中有一个 ID 生成器,每次调用生成器将会生成一个不会重复的顺序序号,使用 10 个并发生成序号,观察 10 个并发后的结果。
竞态检测:
- package main
- import (
- "fmt"
- "sync/atomic"
- )
- var (
- // 序列号
- seq int64
- )
- // 序列号生成器
- func GenID() int64 {
- // 尝试原子的增加序列号
- atomic.AddInt64(&seq, 1)
- return seq
- }
- func main() {
- // 10个并发序列号生成
- for i := 0; i < 10; i++ {
- go GenID()
- }
- fmt.Println(GenID())
- }
代码说明如下:
- 第10行,序列号生成器中的保存上次序列号的变量。
- 第17行,使用原子操作函数atomic.AddInt64()对seq()函数加1操作。不过这里故意没有使用atomic.AddInt64()的返回值作为GenID()函数的返回值,因此会造成一个竞态问题。
- 第25行,循环10次生成10个goroutine调用GenID()函数,同时忽略GenID()的返回值。
- 第28行,单独调用一次GenID()函数。
在运行程序时,为运行参数加入-race参数,开启运行时(runtime)对竞态问题的分析,命令如下:
- # go run -race racedetect.go
- ==================
- WARNING: DATA RACE
- Write at 0x0000005d3f10 by goroutine 7:
- sync/atomic.AddInt64()
- E:/go/src/runtime/race_amd64.s:276 +0xb
- main.GenID()
- D:/go_work/src/chapter09/racedetect/racedetect.go:17 +0x4a
- Previous read at 0x0000005d3f10 by goroutine 6:
- main.GenID()
- D:/go_work/src/chapter09/racedetect/racedetect.go:18 +0x5a
- Goroutine 7 (running) created at:
- main.main()
- D:/go_work/src/chapter09/racedetect/racedetect.go:25 +0x56
- Goroutine 6 (finished) created at:
- main.main()
- D:/go_work/src/chapter09/racedetect/racedetect.go:25 +0x56
- ==================
- 10
- Found 1 data race(s)
- exit status 66
代码运行发生宕机,根据报错信息,第18行有竞态问题,根据atomic.AddInt64()的参数声明,这个函数会将修改后的值以返回值方式传出:
- func GenID() int64 {
- // 尝试原子的增加序列号
- return atomic.AddInt64(&seq, 1)
- }
再次运行:
- # go run -race racedetect.go
- 10
没有发生竞态问题,程序运行正常。
本例中只是对变量进行增减操作,虽然可以使用互斥锁(sync.Mutex)解决竞态问题,但是对性能消耗较大。在这种情况下,推荐使用原子操作(atomic)进行变量操作。
互斥锁(sync.Mutex)和读写互斥锁(sync.RWMutex)
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。在Go程序中的使用非常简单,参见下面的代码:
- package main
- import (
- "fmt"
- "sync"
- )
- var (
- // 逻辑中使用的某个变量
- count int
- // 与变量对应的使用互斥锁
- countGuard sync.Mutex
- )
- func GetCount() int {
- // 锁定
- countGuard.Lock()
- // 在函数退出时解除锁定
- defer countGuard.Unlock()
- return count
- }
- func SetCount(c int) {
- countGuard.Lock()
- count = c
- countGuard.Unlock()
- }
- func main() {
- // 可以进行并发安全的设置
- SetCount(1)
- // 可以进行并发安全的获取
- fmt.Println(GetCount())
- }
代码说明如下:
- 第10行是某个逻辑步骤中使用到的变量,无论是包级的变量还是结构体成员字段,都可以。
- 第13行,一般情况下,建议将互斥锁的粒度设置得越小越好,降低因为共享访问时等待的时间。
- 第16行是一个获取count值的函数封装,通过这个函数可以并发安全的访问变量count。
- 第19行,尝试对countGuard互斥量进行加锁。一旦countGuard发生加锁,如果另外一个goroutine尝试继续加锁时将会发生阻塞,直到这个countGuard被解锁。
- 第22行使用defer将countGuard的解锁进行延迟调用,解锁操作将会发生在GetCount()函数返回时。
- 第27行在设置count值时,同样使用countGuard进行加锁、解锁操作,保证修改count值的过程是一个原子过程,不会发生并发访问冲突。
在读多写少的环境中,可以优先使用读写互斥锁(sync.RWMutex),它比互斥锁更加高效。sync包中的RWMutex提供了读写互斥锁的封装。
我们将互斥锁例子中的一部分代码修改为读写互斥锁,参见下面代码:
- var (
- // 逻辑中使用的某个变量
- count int
- // 与变量对应的使用互斥锁
- countGuard sync.RWMutex
- )
- func GetCount() int {
- // 锁定
- countGuard.RLock()
- // 在函数退出时解除锁定
- defer countGuard.RUnlock()
- return count
- }
代码说明如下:
- 第6行,在声明countGuard时,从sync.Mutex互斥锁改为sync.RWMutex读写互斥锁。
- 第12行,获取count的过程是一个读取count数据的过程,适用于读写互斥锁。在这一行,把countGuard.Lock()换做countGuard.RLock(),将读写互斥锁标记为读状态。如果此时另外一个goroutine并发访问了countGuard,同时也调用了countGuard.RLock()时,并不会发生阻塞。
- 第15行,与读模式加锁对应的,使用读模式解锁。
等待组(sync.WaitGroup)
除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务
等待组有下面几个方法可用,如表1-2所示。
方法名 | 功能 |
(wg * WaitGroup) Add(delta int) | 等待组的计数器+1 |
(wg *WaitGroup) Done() | 等待组的计数器-1 |
(wg *WaitGroup) Wait() | 当等待组计数器不等于0时阻塞直到变0 |
等待组内部拥有一个计数器,计数器的值可以通过方法调用实现计数器的增加和减少。当我们添加了N个并发任务进行工作时,就将等待组的计数器值增加N。每个任务完成时,这个值减1。同时,在另外一个goroutine中等待这个等待组的计数器值为0时,表示所有任务已经完成。
- package main
- import (
- "fmt"
- "net/http"
- "sync"
- )
- func main() {
- // 声明一个等待组
- var wg sync.WaitGroup
- // 准备一系列的网站地址
- var urls = []string{
- "http://www.github.com/",
- "https://www.qiniu.com/",
- "https://www.golangtc.com/",
- }
- // 遍历这些地址
- for _, url := range urls {
- // 每一个任务开始时, 将等待组增加1
- wg.Add(1)
- // 开启一个并发
- go func(url string) {
- // 使用defer, 表示函数完成时将等待组值减1
- defer wg.Done()
- // 使用http访问提供的地址
- _, err := http.Get(url)
- // 访问完成后, 打印地址和可能发生的错误
- fmt.Println(url, err)
- // 通过参数传递url地址
- }(url)
- }
- // 等待所有的任务完成
- wg.Wait()
- fmt.Println("over")
- }
代码说明如下:
- 第12行,声明一个等待组,对一组等待任务只需要一个等待组,而不需要每一个任务都使用一个等待组。
- 第15行,准备一系列可访问的网站地址的字符串切片。
- 第22行,遍历这些字符串切片。
- 第25行,将等待组的计数器加1,也就是每一个任务加1。
- 第28行,将一个匿名函数开启并发。
- 第31行,在匿名函数结束时会执行这一句以表示任务完成。wg.Done()方法等效于执行wg.Add(-1)。
- 第34行,使用http包提供的Get()函数对url进行访问,Get()函数会一直阻塞直到网站响应或者超时。
- 第37行,在网站响应和超时后,打印这个网站的地址和可能发生的错误。
- 第40行,这里将url通过goroutine的参数进行传递,是为了避免url变量通过闭包放入匿名函数后又被修改的问题。
- 第44行,等待所有的网站都响应或者超时后,任务完成,Wait就会停止阻塞。
Go语言之并发编程(四)的更多相关文章
- Go语言 7 并发编程
文章由作者马志国在博客园的原创,若转载请于明显处标记出处:http://www.cnblogs.com/mazg/ Go学习群:415660935 今天我们学习Go语言编程的第七章,并发编程.语言级别 ...
- Go并发编程(四)
并发基础 多进程 多线程 基于回调的非阻塞/异步IO 协程 协程 与传统的系统级线程和进程相比,协程的最大优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭, ...
- 【Java并发编程四】关卡
一.什么是关卡? 关卡类似于闭锁,它们都能阻塞一组线程,直到某些事件发生. 关卡和闭锁关键的不同在于,所有线程必须同时到达关卡点,才能继续处理.闭锁等待的是事件,关卡等待的是其他线程. 二.Cycli ...
- Java 并发编程(四):如何保证对象的线程安全性
01.前言 先让我吐一句肺腑之言吧,不说出来会憋出内伤的.<Java 并发编程实战>这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住.因为第四章"对象的组合 ...
- 并发编程>>四种实现方式(三)
概述 1.继承Thread 2.实现Runable接口 3.实现Callable接口通过FutureTask包装器来创建Thread线程 4.通过Executor框架实现多线程的结构化,即线程池实现. ...
- Go语言之并发编程(三)
Telnet回音服务器 Telnet协议是TCP/IP协议族中的一种.它允许用户(Telnet客户端)通过一个协商过程与一个远程设备进行通信.本例将使用一部分Telnet协议与服务器进行通信. 服务器 ...
- Go语言之并发编程(二)
通道(channel) 单纯地将函数并发执行是没有意义的.函数与函数间需要交换数据才能体现并发执行函数的意义.虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题 ...
- Go语言之并发编程(一)
轻量级线程(goroutine) 在编写socket网络程序时,需要提前准备一个线程池为每一个socket的收发包分配一个线程.开发人员需要在线程数量和CPU数量间建立一个对应关系,以保证每个任务能及 ...
- Java并发编程 (四) 线程安全性
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.线程安全性-原子性-atomic-1 1.线程安全性 定义: 当某个线程访问某个类时,不管运行时环境 ...
随机推荐
- C#类型简述
一.值类型 1.布尔类型 bool,范围 true false 2.整数类型 sbyte,范围 -128~127 byte,范围 0~255 short,范围 -32768~32767 ushort, ...
- LeetCode Add Digits (规律题)
题意: 将一个整数num变成它的所有十进制位的和,重复操作,直到num的位数为1,返回num. 思路: 注意到答案的范围是在区间[0,9]的自然数,而仅当num=0才可能答案为0. 规律在于随着所给自 ...
- Cygwin 下的 自动安装工具 apt-cyg
类似 于apt-get 或者 yum Cygwin可以在Windows下使用unix环境Bash和各种功能强大的工具,对于Linux管理员来说不想使用Linux桌面是必备的工具. Cygwin下也有类 ...
- IE Proxy Swich - IE 代理切换工具
通过此工具可方便的切换计算机系统代理设置的开关,无需重启IE 来激活设置 下载 环境要求: 可能需要.NET 4.0 以上平台, 其他平台未测试 截图与功能如下 支持快捷方式参数 我个人习惯是在桌面 ...
- 怎样在github里面写个人主页
1 登录你的账号 打开
- 使用HANA Web-based Development Workbench创建最简单的Server Side JavaScript
服务器端的JavaScript, 看下wikipedia的介绍: https://en.wikipedia.org/wiki/JavaScript#Server-side_JavaScript Ser ...
- Android(java)学习笔记152:采用get请求提交数据到服务器(qq登录案例)
1.GET请求: 组拼url的路径,把提交的数据拼装url的后面,提交给服务器. 缺点:(1)安全性(Android下提交数据组拼隐藏在代码中,不存在安全问题) (2)长度有限不能超过4K(h ...
- app之间的跳转和传参问题
app 之间跳转和传参: 首先 创建2个app formApp (需要跳转到另外app的项目) toApp(被跳转的项目) 一:在toApp 项目中的操作: 1:创建URLSchemes ...
- C语言 流缓冲 Stream Buffering
From : https://www.gnu.org/software/libc/manual/html_node/Stream-Buffering.html 译者:李秋豪 12.20 流缓冲 通常情 ...
- Vue之Vue-touch的使用
最近项目中,有的页面发现设置返回键看起来怪怪的,感觉与整体不协调,于是就考虑使用手势滑动事件来实现返回功能~ 开叉查阅资料~找到了vue-touch,使用起来可谓是简单粗暴啊,适合我这样的快速开发人员 ...