同步

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所示。

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

  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. jquery jquery选择器总结 转自(永远的麦子)

    jQuery选择器总结 阅读目录 1, 基本选择器? 2, 层次选择器? 3, 过滤选择器? 4, 表单选择器? jQuery选择器共有四大类,分别为基本选择器,层次选择器,过滤选择器和表单选择器.下 ...

  2. 程序运行的cpu时间

    time.clock() 测量CPU时间,比较精准,通过比较程序运行前后的CPU时间差,得出程序运行的CPU时间.

  3. vim常用命令大全

    在命令状态下对当前行用== (连按=两次), 或对多行用n==(n是自然数)表示自动缩进从当前行起的下面n行.你可以试试把代码缩进任意打乱再用n==排版,相当于一般IDE里的code format.使 ...

  4. Vue系列:关于侵权前言

    因为一开始学习 Vue 的时候,秉持的是"好记性不如烂笔头"的思想,所以这些知识点都是当时查阅然后选择性摘录下来的. 当时并没有很刻意去记录原文出处(现在我知错了..),所以..如 ...

  5. weexpack 创建项目, 打包Android 和 ios

    1: 首先确保 node.js版本大于6.0, 不然使用 weexpack 时会报错(部分使用es6语言,node版本太低不支持es6) 下面所有都是命令行执行的命令 2: 全局安装 weexpack ...

  6. JavaWeb前端笔记

    day06 回顾: bootstrap: css框架,html/css/js集于一身,ie 6/7/8兼容有问题 开发响应式页面,使用于不同的上网设备 使用步骤: 1.导入bootstrap.css ...

  7. NC57访问报错:java.sql.SQLException: Io 异常: Got minus one from a read call

    一.报错信息 1.  前端登录界面 2.  后台应用日志 报错信息一致为: $$callid= $$thread=[Service Monitor and Runtime Enroment] $$ho ...

  8. 【来龙去脉系列】RSA算法原理

    如果你问我,哪一种算法最重要? 我可能会回答"公钥加密算法". 因为它是计算机通信安全的基石,保证了加密数据不会被破解.你可以想象一下,信用卡交易被破解的后果. 进入正题之前,我先 ...

  9. js 中//<![CDATA[ 意义

    CDATA内部所有东西都会被解析器忽略,加入文本中包含了大量< 和 $符号,就像编程中经常出现的情况一样,那么这个元素就可以被定义为一个CDATA部分 ,CDATA 区段开始于 "&l ...

  10. signal函数:void (*signal(int,void(*)(int)))(int);

    http://blog.chinaunix.net/uid-20178794-id-1972862.html signal函数:void (*signal(int,void(*)(int)))(int ...