同步

Go 程序可以使用通道进行多个 goroutine 间的数据交换,但这仅仅是数据同步中的一种方法。通道内部的实现依然使用了各种锁,因此优雅代码的代价是性能。在某些轻量级的场合,原子访问(atomic包)、互斥锁(sync.Mutex)以及等待组(sync.WaitGroup)能最大程度满足需求。

当多线程并发运行的程序竞争访问和修改同一块资源时,会发生竞态问题。

下面的代码中有一个 ID 生成器,每次调用生成器将会生成一个不会重复的顺序序号,使用 10 个并发生成序号,观察 10 个并发后的结果。

竞态检测:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync/atomic"
  6. )
  7.  
  8. var (
  9. // 序列号
  10. seq int64
  11. )
  12.  
  13. // 序列号生成器
  14. func GenID() int64 {
  15.  
  16. // 尝试原子的增加序列号
  17. atomic.AddInt64(&seq, 1)
  18. return seq
  19. }
  20.  
  21. func main() {
  22.  
  23. // 10个并发序列号生成
  24. for i := 0; i < 10; i++ {
  25. go GenID()
  26. }
  27.  
  28. fmt.Println(GenID())
  29. }

  

代码说明如下:

  • 第10行,序列号生成器中的保存上次序列号的变量。
  • 第17行,使用原子操作函数atomic.AddInt64()对seq()函数加1操作。不过这里故意没有使用atomic.AddInt64()的返回值作为GenID()函数的返回值,因此会造成一个竞态问题。
  • 第25行,循环10次生成10个goroutine调用GenID()函数,同时忽略GenID()的返回值。
  • 第28行,单独调用一次GenID()函数。

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

  1. # go run -race racedetect.go
  2. ==================
  3. WARNING: DATA RACE
  4. Write at 0x0000005d3f10 by goroutine 7:
  5. sync/atomic.AddInt64()
  6. E:/go/src/runtime/race_amd64.s:276 +0xb
  7. main.GenID()
  8. D:/go_work/src/chapter09/racedetect/racedetect.go:17 +0x4a
  9.  
  10. Previous read at 0x0000005d3f10 by goroutine 6:
  11. main.GenID()
  12. D:/go_work/src/chapter09/racedetect/racedetect.go:18 +0x5a
  13.  
  14. Goroutine 7 (running) created at:
  15. main.main()
  16. D:/go_work/src/chapter09/racedetect/racedetect.go:25 +0x56
  17.  
  18. Goroutine 6 (finished) created at:
  19. main.main()
  20. D:/go_work/src/chapter09/racedetect/racedetect.go:25 +0x56
  21. ==================
  22. 10
  23. Found 1 data race(s)
  24. exit status 66

  

代码运行发生宕机,根据报错信息,第18行有竞态问题,根据atomic.AddInt64()的参数声明,这个函数会将修改后的值以返回值方式传出:

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

  

再次运行:

  1. # go run -race racedetect.go
  2. 10

  

没有发生竞态问题,程序运行正常。

本例中只是对变量进行增减操作,虽然可以使用互斥锁(sync.Mutex)解决竞态问题,但是对性能消耗较大。在这种情况下,推荐使用原子操作(atomic)进行变量操作。

互斥锁(sync.Mutex)和读写互斥锁(sync.RWMutex)

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。在Go程序中的使用非常简单,参见下面的代码:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "sync"
  6. )
  7.  
  8. var (
  9. // 逻辑中使用的某个变量
  10. count int
  11.  
  12. // 与变量对应的使用互斥锁
  13. countGuard sync.Mutex
  14. )
  15.  
  16. func GetCount() int {
  17.  
  18. // 锁定
  19. countGuard.Lock()
  20.  
  21. // 在函数退出时解除锁定
  22. defer countGuard.Unlock()
  23.  
  24. return count
  25. }
  26.  
  27. func SetCount(c int) {
  28. countGuard.Lock()
  29. count = c
  30. countGuard.Unlock()
  31. }
  32.  
  33. func main() {
  34.  
  35. // 可以进行并发安全的设置
  36. SetCount(1)
  37.  
  38. // 可以进行并发安全的获取
  39. fmt.Println(GetCount())
  40.  
  41. }

  

代码说明如下:

  • 第10行是某个逻辑步骤中使用到的变量,无论是包级的变量还是结构体成员字段,都可以。
  • 第13行,一般情况下,建议将互斥锁的粒度设置得越小越好,降低因为共享访问时等待的时间。
  • 第16行是一个获取count值的函数封装,通过这个函数可以并发安全的访问变量count。
  • 第19行,尝试对countGuard互斥量进行加锁。一旦countGuard发生加锁,如果另外一个goroutine尝试继续加锁时将会发生阻塞,直到这个countGuard被解锁。
  • 第22行使用defer将countGuard的解锁进行延迟调用,解锁操作将会发生在GetCount()函数返回时。
  • 第27行在设置count值时,同样使用countGuard进行加锁、解锁操作,保证修改count值的过程是一个原子过程,不会发生并发访问冲突。

在读多写少的环境中,可以优先使用读写互斥锁(sync.RWMutex),它比互斥锁更加高效。sync包中的RWMutex提供了读写互斥锁的封装。

我们将互斥锁例子中的一部分代码修改为读写互斥锁,参见下面代码:

  1. var (
  2. // 逻辑中使用的某个变量
  3. count int
  4.  
  5. // 与变量对应的使用互斥锁
  6. countGuard sync.RWMutex
  7. )
  8.  
  9. func GetCount() int {
  10.  
  11. // 锁定
  12. countGuard.RLock()
  13.  
  14. // 在函数退出时解除锁定
  15. defer countGuard.RUnlock()
  16.  
  17. return count
  18. }

  

代码说明如下:

  • 第6行,在声明countGuard时,从sync.Mutex互斥锁改为sync.RWMutex读写互斥锁。
  • 第12行,获取count的过程是一个读取count数据的过程,适用于读写互斥锁。在这一行,把countGuard.Lock()换做countGuard.RLock(),将读写互斥锁标记为读状态。如果此时另外一个goroutine并发访问了countGuard,同时也调用了countGuard.RLock()时,并不会发生阻塞。
  • 第15行,与读模式加锁对应的,使用读模式解锁。

等待组(sync.WaitGroup)

除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务

等待组有下面几个方法可用,如表1-2所示。

表1-2   等待组的方法
方法名 功能
(wg * WaitGroup) Add(delta int) 等待组的计数器+1
(wg *WaitGroup) Done() 等待组的计数器-1
(wg *WaitGroup) Wait() 当等待组计数器不等于0时阻塞直到变0

等待组内部拥有一个计数器,计数器的值可以通过方法调用实现计数器的增加和减少。当我们添加了N个并发任务进行工作时,就将等待组的计数器值增加N。每个任务完成时,这个值减1。同时,在另外一个goroutine中等待这个等待组的计数器值为0时,表示所有任务已经完成。

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "net/http"
  6. "sync"
  7. )
  8.  
  9. func main() {
  10.  
  11. // 声明一个等待组
  12. var wg sync.WaitGroup
  13.  
  14. // 准备一系列的网站地址
  15. var urls = []string{
  16. "http://www.github.com/",
  17. "https://www.qiniu.com/",
  18. "https://www.golangtc.com/",
  19. }
  20.  
  21. // 遍历这些地址
  22. for _, url := range urls {
  23.  
  24. // 每一个任务开始时, 将等待组增加1
  25. wg.Add(1)
  26.  
  27. // 开启一个并发
  28. go func(url string) {
  29.  
  30. // 使用defer, 表示函数完成时将等待组值减1
  31. defer wg.Done()
  32.  
  33. // 使用http访问提供的地址
  34. _, err := http.Get(url)
  35.  
  36. // 访问完成后, 打印地址和可能发生的错误
  37. fmt.Println(url, err)
  38.  
  39. // 通过参数传递url地址
  40. }(url)
  41. }
  42.  
  43. // 等待所有的任务完成
  44. wg.Wait()
  45.  
  46. fmt.Println("over")
  47. }

  

代码说明如下:

  • 第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语言之并发编程(四)的更多相关文章

  1. Go语言 7 并发编程

    文章由作者马志国在博客园的原创,若转载请于明显处标记出处:http://www.cnblogs.com/mazg/ Go学习群:415660935 今天我们学习Go语言编程的第七章,并发编程.语言级别 ...

  2. Go并发编程(四)

        并发基础   多进程  多线程 基于回调的非阻塞/异步IO     协程  协程  与传统的系统级线程和进程相比,协程的最大优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭, ...

  3. 【Java并发编程四】关卡

    一.什么是关卡? 关卡类似于闭锁,它们都能阻塞一组线程,直到某些事件发生. 关卡和闭锁关键的不同在于,所有线程必须同时到达关卡点,才能继续处理.闭锁等待的是事件,关卡等待的是其他线程. 二.Cycli ...

  4. Java 并发编程(四):如何保证对象的线程安全性

    01.前言 先让我吐一句肺腑之言吧,不说出来会憋出内伤的.<Java 并发编程实战>这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住.因为第四章"对象的组合 ...

  5. 并发编程>>四种实现方式(三)

    概述 1.继承Thread 2.实现Runable接口 3.实现Callable接口通过FutureTask包装器来创建Thread线程 4.通过Executor框架实现多线程的结构化,即线程池实现. ...

  6. Go语言之并发编程(三)

    Telnet回音服务器 Telnet协议是TCP/IP协议族中的一种.它允许用户(Telnet客户端)通过一个协商过程与一个远程设备进行通信.本例将使用一部分Telnet协议与服务器进行通信. 服务器 ...

  7. Go语言之并发编程(二)

    通道(channel) 单纯地将函数并发执行是没有意义的.函数与函数间需要交换数据才能体现并发执行函数的意义.虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题 ...

  8. Go语言之并发编程(一)

    轻量级线程(goroutine) 在编写socket网络程序时,需要提前准备一个线程池为每一个socket的收发包分配一个线程.开发人员需要在线程数量和CPU数量间建立一个对应关系,以保证每个任务能及 ...

  9. Java并发编程 (四) 线程安全性

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.线程安全性-原子性-atomic-1 1.线程安全性 定义: 当某个线程访问某个类时,不管运行时环境 ...

随机推荐

  1. C#类型简述

    一.值类型 1.布尔类型 bool,范围 true false 2.整数类型 sbyte,范围 -128~127 byte,范围 0~255 short,范围 -32768~32767 ushort, ...

  2. LeetCode Add Digits (规律题)

    题意: 将一个整数num变成它的所有十进制位的和,重复操作,直到num的位数为1,返回num. 思路: 注意到答案的范围是在区间[0,9]的自然数,而仅当num=0才可能答案为0. 规律在于随着所给自 ...

  3. Cygwin 下的 自动安装工具 apt-cyg

    类似 于apt-get 或者 yum Cygwin可以在Windows下使用unix环境Bash和各种功能强大的工具,对于Linux管理员来说不想使用Linux桌面是必备的工具. Cygwin下也有类 ...

  4. IE Proxy Swich - IE 代理切换工具

    通过此工具可方便的切换计算机系统代理设置的开关,无需重启IE 来激活设置 下载 环境要求: 可能需要.NET 4.0 以上平台, 其他平台未测试 截图与功能如下 支持快捷方式参数 我个人习惯是在桌面 ...

  5. 怎样在github里面写个人主页

    1 登录你的账号 打开

  6. 使用HANA Web-based Development Workbench创建最简单的Server Side JavaScript

    服务器端的JavaScript, 看下wikipedia的介绍: https://en.wikipedia.org/wiki/JavaScript#Server-side_JavaScript Ser ...

  7. Android(java)学习笔记152:采用get请求提交数据到服务器(qq登录案例)

    1.GET请求:    组拼url的路径,把提交的数据拼装url的后面,提交给服务器. 缺点:(1)安全性(Android下提交数据组拼隐藏在代码中,不存在安全问题)  (2)长度有限不能超过4K(h ...

  8. app之间的跳转和传参问题

    app 之间跳转和传参: 首先 创建2个app   formApp (需要跳转到另外app的项目)     toApp(被跳转的项目) 一:在toApp 项目中的操作: 1:创建URLSchemes ...

  9. C语言 流缓冲 Stream Buffering

    From : https://www.gnu.org/software/libc/manual/html_node/Stream-Buffering.html 译者:李秋豪 12.20 流缓冲 通常情 ...

  10. Vue之Vue-touch的使用

    最近项目中,有的页面发现设置返回键看起来怪怪的,感觉与整体不协调,于是就考虑使用手势滑动事件来实现返回功能~ 开叉查阅资料~找到了vue-touch,使用起来可谓是简单粗暴啊,适合我这样的快速开发人员 ...