本篇文章将介绍 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. 安装华企盾DSC加密软件,USB、银行key等驱动加载不了常见处理方法

    1.首先打开高级客户端查看客户端的权限是否正确 2.将USB设置成放行 3.修改USB管控的注册表 4.安装6.6.0高版本,并添加便捷式设备的注册表改为1见下图 5.添加flag000如正常则把相关 ...

  2. Salesforce LWC学习(四十七) 标准页面更新以后自定义页面如何捕捉?

    本篇参考: https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platfor ...

  3. 初识BigDecimal

    BigDecimal所创建的是对象,我们不能使用传统的+.-.*./等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法. 方法中的参数也必须是BigDecimal的对象. BigDecim ...

  4. CentOS 7 NTP服务端和客户端详细配置

    参考: https://blog.csdn.net/ankang654321/article/details/103542015 ntp同步时间实验 服务端IP  192.168.1.101      ...

  5. Angular 集成 Material UI 后组件显示不正常 踩坑日记

    在使用了 npm 下载 Material 后, 项目不能正常使用 Material 组件, 随后又使用官方命令使用 Material 组件, 仍然不能正常使用 Material 组件. npm 命令 ...

  6. 数据湖探索DLI新功能:基于openLooKeng的交互式分析

    摘要:基于华为开源openLooKeng引擎的交互式分析功能,将重磅发布便于用户构建轻量级流.批.交互式全场景数据湖. 在这个"信息爆炸"的时代,大数据已经成为这个时代的关键词之一 ...

  7. ModelArts的雪中送炭,让我拿下CCF BDCI华为Severless工作负载预测亚军

    摘要: 中国计算机学会大数据与计算智能大赛(CCF BDCI)华为Severless工作负载预测亚军方案和ModelArts使用体验分享 本文分享自华为云社区<免费薅ModelArts算力资源- ...

  8. QA团队基于DataLeap开放平台能力的数据测试实践

    背景 &痛点 随着生态体系扩展和业务发展,数据在业务中承担的决策场景越来越多样化,一部分数据已应用在资损.高客诉等高风险场景,因此对数据质量的要求,尤其是高风险场景的质量要求非常之高.但在保障 ...

  9. LAS Spark 在 TPC-DS 的优化揭秘

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 文章主要介绍了火山引擎湖仓一体分析服务 LAS Spark(下文以 LAS Spark 指代)在 TPC-DS 上 ...

  10. PPT 画册风格

    图片嵌入 图片填充 图片裁剪 字体 PPT 关掉再打开. 排列对齐 图片下载 https://www.pexels.com http://www.500px.com http://www.bing.c ...