本篇文章将介绍 hello world 的并发实现,其中涉及到的知识有:

  • 并发与并行
  • GPM 调度系统

并发与并行

并发不是并行。并发是同时管理很多事情,这些事情可能只做了一半就被暂停做别的事情了。而并行是同时做很多事情,让不同的代码段同时在不同的物理处理器上执行。

在很多情况下,并发要比并行好,它符合 Go 语言的涉及哲学: 使用较少的资源做更多的事情。

Go 的 GPM 调度系统

GPM 是 Go 自己实现的一套调度系统,区别于操作系统层面的线程调度系统。

  • G 是 Goroutine 的缩写,goroutine 相当于操作系统的进程控制块,它是一个独立的工作单元。
  • P(Processor) 是一个抽象的概念,并不是真正的 CPU,它管理着一组 goroutine 队列(暂停占用较长 CPU 时间的 goroutine,运行等待的 goroutine 等),当管理的 goroutine 队列都执行完则从全局队列里取任务,如果全局队列也没有任务,则去其它 P 的队列里抢任务。
  • M(Machine) 是 Go 运行时(runtime) 对操作系统内核线程的虚拟,M 与内核线程是一一映射的关系(M 和 P 一般也是一一对应),一个 goroutine 最终要调度到 M 上执行。

1. hello world 的并发实现

package main

import (
"fmt"
"runtime"
"sync"
) var wg sync.WaitGroup func say_hello(value interface{}) {
defer wg.Done()
fmt.Printf("%v", value)
} func common_say_hello() {
wg.Add(5)
go say_hello("w")
go say_hello("o")
go say_hello("r")
go say_hello("l")
go say_hello("d")
} func main() {
runtime.GOMAXPROCS(1)
common_say_hello()
wg.Wait()
}

代码介绍:

  • runtime 包的 GOMAXPROCS 函数允许程序更改调度器可以使用的逻辑处理器数量,逻辑处理器和操作系统线程是一一绑定的关系。这里仅使用 1 个逻辑处理器处理并发运行的 goroutine。
  • 实现 goroutine 很简单只需要在函数名前加 go 即可让该函数独立于其它函数运行,Go 会将其视为一个独立的工作单元,这个单元会被调度到可用的逻辑处理器上执行。
  • 使用 sync 包结构体 WaitGroup 的 Add/Wait/Done 方法来等待 goroutine 的完成。如果不加等待, main 函数会在 goroutine 运行前终止。

代码运行结果如下:

dworl

为什么 d 会打印在最前面而 worl 则依次打印呢?

<<Go 语言实战>> 给出的解释是“第一个 goroutine 完成所有显示需要花的时间很短,以至于调度器切换到第二个 goroutine之前就完成了所有任务”。那么,这里的第一个 goroutine 是 “go say_hello("d")” 吗?第二个,第三个 goroutine.. 又是哪个呢?调度器根据怎么的顺序来调度 goroutine 呢?这些问题留给我们后续解答,有知道的朋友还请不吝赐教,感谢。

上面的代码限定了逻辑处理器的数量为 1,所以这里其实实现的是并发而不是并行。当设置逻辑处理器的数量大于 1 时,即实现了并行也实现了并发。更改逻辑处理器数量为 3,查看程序运行情况:

dorlw
dowlr
ldorw

执行了三次每次打印的输出都不一样。

那么是不是到这里就结束了呢?没有。有一点需要说明的是: 一个正在运行的 goroutine 可以被停止并重新调度。如果 goroutine 长时间占用逻辑处理器,调度器会停止该 goroutine,并给其它 goroutine 运行的机会。

基于上述分析,更改 hello world 代码,使每个 goroutine 占用较长的逻辑处理器时间,查看 goroutine 是否被调度器切换,代码如下:

func multi_hello(prefix string) {
defer wg.Done() next:
for outer := 2; outer < 5000; outer++ {
for inter := 2; inter < outer; inter++ {
if outer%inter == 0 {
continue next
}
}
fmt.Println("say %s: %d times", prefix, outer)
}
} func crazy_say_hello() {
wg.Add(5)
go multi_hello("w")
go multi_hello("o")
go multi_hello("r")
go multi_hello("l")
go multi_hello("d")
} func main() {
runtime.GOMAXPROCS(1)
crazy_say_hello()
wg.Wait()
}

查看代码执行结果:

say r: 4327 times
say r: 4337 times
say r: 4339 times
say w: 4493 times
say w: 4507 times
say w: 4513 times
...
say w: 4999 times
say r: 4349 times
say r: 4357 times
...

这里仅截取部分执行结果。可以看到,执行 r goroutine 第 4349 次的时候调度器切换 “r goroutine” 到 “w goroutine” ,然后执行 w goroutine 4999 次的时候调度再切换回 “r goroutine”。

上述 hello world 的 goroutine 均不涉及对共享资源的访问,因此它们能和谐共存,互不干扰。如果涉及到共享资源的访问,goroutine 将变得相当“野蛮”也即出现相互竞争访问共享资源的状态,这种情况称为“竞争”状态。

2. 竞争状态的 goroutine

进一步改写 hello world 程序如下:

var helloTimes int32

func cal_hello_num(prefix string) {
defer wg.Done() value := helloTimes
runtime.Gosched() value++
helloTimes = value
fmt.Printf("say %s: %d times\n", prefix, helloTimes)
} func num_say_hello() {
wg.Add(5)
go cal_hello_num("w")
go cal_hello_num("o")
go cal_hello_num("r")
go cal_hello_num("l")
go cal_hello_num("d")
} func main() {
runtime.GOMAXPROCS(1)
num_say_hello()
wg.Wait()
}

为方便说明这里将逻辑处理器的数量设为 1,同时引入 runtime 包的 Gosched 函数,该函数会将当前 goroutine 从线程退出,并放回到逻辑处理器的队列中。程序执行结果如下:

say d: 1 times
say w: 1 times
say o: 1 times
say r: 1 times
say l: 1 times

多次执行,每个 goroutine 打印结果均为 1,为什么呢?

分析上述代码,每个 goroutine 都会覆盖另一个 goroutine 的工作(竞争状态因此存在)。每个 goroutine 均创造了变量 helloTimes 的副本 value,当 goroutine 切换时,每个 goroutine 会将自己维护的 value 赋值给 helloTimes,导致 helloTimes 的值一直是 1。

那么,如果每个 goroutine 都不创造变量的副本是否这种竞争状态就消失了呢?

进一步改写程序如下:

改写版本1

func cal_hello_num(prefix string) {
defer wg.Done() helloTimes++
runtime.Gosched()
fmt.Printf("say %s: %d times\n", prefix, helloTimes)
} // 运行结果
say d: 5 times
say w: 5 times
say o: 5 times
say r: 5 times
say l: 5 times

改写版本 2

func cal_hello_num(prefix string) {
defer wg.Done() runtime.Gosched()
helloTimes++
fmt.Printf("say %s: %d times\n", prefix, helloTimes)
} // 运行结果
say d: 1 times
say w: 2 times
say o: 3 times
say r: 4 times
say l: 5 times

版本 1 和版本 2 移动了 helloTimes++ 相对于 GoSched 的位置,却得到了完全不同的结果。

其实不难理解,因为 helloTimes 是全局变量,每个 goroutine 都维护这个变量。所以,在版本一中每个 goroutine 切换之前都会对全局变量 helloTimes 加 1,加 1 完成后,程序依次打印“最终值” 5。而版本二 goroutine 在切换之后对全局变量加 1,其效果相当于每个 goroutine 按顺序依次执行全局变量的自增操作。

多个 goroutine 访问共享资源极易出现“幺蛾子”,在程序中可以通过锁住共享资源的方式来避免竞争状态的出现。

3. 锁住共享资源

通过原子函数,互斥锁锁住共享资源,实现 goroutine 对共享资源的顺序访问。

3.1 原子函数

import (
"fmt"
"runtime"
"sync"
"sync/atomic"
) func cal_hello_num(prefix string) {
defer wg.Done() atomic.AddInt32(&helloTimes, 1)
fmt.Printf("say %s: %d times\n", prefix, helloTimes)
}

使用 atomic 包导入原子函数 AddInt32 实现变量 helloTimes 的自增操作。执行结果如下:

say d: 1 times
say w: 2 times
say o: 3 times
say r: 4 times
say l: 5 times

3.2 互斥锁

使用互斥锁防止竞争状态的发生。互斥锁会在代码上创建临界区,保证同一时间只有一个 goroutine 可以访问执行临界区代码。代码如下:

var (
wg sync.WaitGroup
mutex sync.Mutex
) func cal_hello_num(prefix string) {
defer wg.Done()
mutex.Lock() value := helloTimes
runtime.Gosched()
value++
helloTimes = value
fmt.Printf("say %s: %d times\n", prefix, helloTimes) mutex.Unlock()
}

有一点要注意的是: 使用 Gosched 强制 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运行临界区代码。程序执行结果如下:

say d: 1 times
say w: 2 times
say o: 3 times
say r: 4 times
say l: 5 times

再次强调 value 的位置是很关键的,如果对 value := helloTimes 不加锁,每个 goroutine 还是会保留各自的副本,起不到防止竞争状态的作用。代码及执行结果如下所示:

func cal_hello_num(prefix string) {
defer wg.Done() value := helloTimes
mutex.Lock()
runtime.Gosched()
value++
helloTimes = value
fmt.Printf("say %s: %d times\n", prefix, helloTimes)
mutex.Unlock()
} // 执行结果
say d: 1 times
say w: 1 times
say o: 1 times
say r: 1 times
say l: 1 times

当然,除了原子函数和互斥锁防止竞争状态外,还可以使用 channel 通道,channel 通过发送和接收需要共享的资源,实现共享资源在 goroutine 之间的同步。下节将介绍 Go 的 channel 类型以及如何避免掉入 channel 的坑。

hello world 的并发实现的更多相关文章

  1. .Net多线程编程—并发集合

    并发集合 1 为什么使用并发集合? 原因主要有以下几点: System.Collections和System.Collections.Generic名称空间中所提供的经典列表.集合和数组都不是线程安全 ...

  2. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

  3. [高并发]Java高并发编程系列开山篇--线程实现

    Java是最早开始有并发的语言之一,再过去传统多任务的模式下,人们发现很难解决一些更为复杂的问题,这个时候我们就有了并发. 引用 多线程比多任务更加有挑战.多线程是在同一个程序内部并行执行,因此会对相 ...

  4. 关于如何提高Web服务端并发效率的异步编程技术

    最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...

  5. 如何在高并发环境下设计出无锁的数据库操作(Java版本)

    一个在线2k的游戏,每秒钟并发都吓死人.传统的hibernate直接插库基本上是不可行的.我就一步步推导出一个无锁的数据库操作. 1. 并发中如何无锁. 一个很简单的思路,把并发转化成为单线程.Jav ...

  6. Java多线程基础——对象及变量并发访问

    在开发多线程程序时,如果每个多线程处理的事情都不一样,每个线程都互不相关,这样开发的过程就非常轻松.但是很多时候,多线程程序是需要同时访问同一个对象,或者变量的.这样,一个对象同时被多个线程访问,会出 ...

  7. 多线程的通信和同步(Java并发编程的艺术--笔记)

    1. 线程间的通信机制 线程之间通信机制有两种: 共享内存.消息传递.   2. Java并发 Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式执行,通信的过程对于程序员来说是完全透 ...

  8. 伪共享(false sharing),并发编程无声的性能杀手

    在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...

  9. 编写高质量代码:改善Java程序的151个建议(第8章:多线程和并发___建议126~128)

    建议126:适时选择不同的线程池来实现 Java的线程池实现从根本上来说只有两个:ThreadPoolExecutor类和ScheduledThreadPoolExecutor类,这两个类还是父子关系 ...

  10. 理解Storm并发

    作者:Jack47 PS:如果喜欢我写的文章,欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 注:本文主要内容翻译自understanding-the-parall ...

随机推荐

  1. hszxoj 矿场搭建 [tarjan]

    hszxoj 矿场搭建 题目描述 原题来自:HNOI 2012 煤矿工地可以看成是由隧道连接挖煤点组成的无向图.为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处.于是矿主决 ...

  2. CSS语法检查利器之csslint

    本文于2015年底完成,发布在个人博客网站上. 考虑个人博客因某种原因无法修复,于是在博客园安家,之前发布的文章逐步搬迁过来. 背景 前段时间研究使用YUI Compressor压缩项目里的js和cs ...

  3. DNS解析中CNAME和MX记录冲突

    转载:DNS中CNAME和MX记录的冲突 在DNS解析中,CNAME记录与其他记录往往是互斥的.最常见的是CNAME记录和MX记录的互斥.例如我们在http://example.com部署官网,通过C ...

  4. JavaFx Maven配置推荐(七)

    JavaFx Maven配置推荐(七) JavaFX 从入门到入土系列 开发Java Fx,推荐使用Maven管理项目,下面是常用到的配置基于jdk11+ <!-- 打成 jar 包 --> ...

  5. java常用正则表达式工具

    public class RegexUtils { /** * 是否是无效手机格式 * @param phone 要校验的手机号 * @return true:符合,false:不符合 */ publ ...

  6. js文字转语音播放SpeechSynthesisUtterance

    文字转语音 SpeechSynthesisUtterance是HTML5中新增的API,用于将指定文字合成为对应的语音 function sayTTS(content) { const synth = ...

  7. 知识+AI融合创新探索,华为云论文被AI顶级学术期刊IEEE TPAMI接受

    摘要:通过利用物体类别之间存在的层级关系约束,自动学习从数据中抽取识别不同类别的规则,一方面对模型的预测过程进行解释,另一方面也提供了一条引入人工先验知识的可行途径. 前言 受益于深度学习技术的突破, ...

  8. 开抢| 华为开发者大会2021(Cloud)早鸟票来了!

    他们,是技术的引领者 他们,是科学的追随者 他们用代码守护蓝色星球的生态 破解宇宙深处的密码 化解城市生活的不畅 捉摸自然气候的脾性 ...... 这些改变世界的成果背后 每一个开发者都了不起 --那 ...

  9. ISO/IEC 5055:软件代码质量的标尺

    摘要:ISO 5055是首个直接从软件内部结构方面衡量软件质量(如安全性和可靠性)的ISO标准.该标准基于统计安全性.可靠性.可维护性和性能效率方面的软件缺陷来衡量软件的结构质量. 本文分享自华为云社 ...

  10. 如何精准预测天气?火山引擎ByteHouse与大地量子这么做

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群   伴随着气象技术的发展以及城市气象设施的完善,气象监测服务能力在不断提高,实现短期甚至中长期的气象预测成为可能. ...