同步

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. 【Java】Maven 常用命令

    Maven 常用命令 mvn compile 编译,生成target文件夹,里边有classes文件夹,存放.class文件 mvn test 执行测试 mvn package 打包,在targert ...

  2. eclipse之插件添加

    在ftp.properties文件中,中文出现十六进制显示情况,如下: 解决该问题只需要在eclipse中下载一个插件即可解决, 步骤: help  ---> install new softw ...

  3. js函数获取ev对象

    今天工作中遇到一个问题,就是平时获取一个ev(event)对象时候一般直接在调用方法里面写一个ev参数,即可直接拿到这个对象,但是有时候会遇到函数调用不是直接加在一个dom对象的后面,如: var o ...

  4. Invoke 和 BeginInvoke 的区别(转发)

    在Invoke或者BeginInvoke的使用中无一例外地使用了委托Delegate. 一.为什么Control类提供了Invoke和BeginInvoke机制? 关于这个问题的最主要的原因已经是do ...

  5. nbtscan ip地址

    查找网络(192.168.1.0)中netbios名字信息,对应命令如下: nbtscan 192.168.1.1-254 找到有netbios名字后,可以使用如下的命令查看这些主机运行的服务. nb ...

  6. pecl install msgpack

    Before the beginning: There are two php version, php5.5, php7.1. we need to install msgpack under ph ...

  7. vs2010 opencv2.4.10 配置过程出现的问题 & mfc打开图片

    配置参考网址: http://blog.csdn.net/zy122121cs/article/details/49180541 无法启动程序,系统找不到指定的文件:原因是程序编译有错误(不是路径之类 ...

  8. 使用SAP云平台 + JNDI访问Internet Service

    以Internet Service http://maps.googleapis.com/maps/api/distancematrix/xml?origins=Walldorf&destin ...

  9. Bezier贝塞尔曲线的原理、二次贝塞尔曲线的实现

    Bezier曲线的原理 Bezier曲线是应用于二维图形的曲线.曲线由顶点和控制点组成,通过改变控制点坐标可以改变曲线的形状. 一次Bezier曲线公式: 一次Bezier曲线是由P0至P1的连续点, ...

  10. World Wind Java开发之十一——加载热点信息(仿Google Earth)(转)

    在GE的图层中有一个照片图层,在浏览时可以看到各地的一些图片,我们称之为热点信息,如下图所示: 再来看下本文的实现效果: 效果是不是很像呢,其实实现这个很简单,参照examples中的Balloons ...