使用Golang的协程竟然变慢了|100万个协程的归并排序耗时分析
前言
这篇文章将用三个版本的归并排序,为大家分析使用协程排序的时间开销(被排序的切片长度由128到1000w)
本期demo地址:https://github.com/BaiZe1998/go-learning
往期视频讲解 :B站:白泽talk,公众号:白泽talk

认为并发总是更快
- 线程:OS 调度的基本单位,用于调度到 CPU 上执行,线程的切换是一个高昂的操作,因为要求将当前 CPU 中运行态的线程上下文保存,切换到可执行态,同时调度一个可执行态的线程到 CPU 中执行。
- 协程:线程由 OS 上下文切换 CPU 内核,而 Goroutine 则由 Go 运行时上下文切换协程。Go 协程占用内存比线程少(2KB/2MB),协程的上下文切换比线程快80~90%。
GMP 模型:
- G:Goroutine
- 执行态:被调度到 M 上执行
- 可执行态:等待被调度
- 等待态:因为一些原因被阻塞
- M:OS thread
- P:CPU core
- 每个 P 有一个本地 G 队列(任务队列)
- 所有 P 有一个公共 G 队列(任务队列)
协程调度规则:每一个 OS 线程(M)被调度到 P 上执行,然后每一个 G 运行在 M 上。

上图中展示了一个4核 CPU 的机器调度 Go 协程的场景:
此时 P2 正在闲置因为 M3 执行完毕释放了对 P2 的占用,虽然 P2 的 Local queue 中已经空了,没有 G 可以调度执行,但是每隔一定时间,Go runtime 会去 Global queue 和其他 P 的 local queue 偷取一些 G 用于调度执行(当前存在6个可执行的G)。
特别的,在 Go1.14 之前,Go 协程的调度是合作形式的,因此 Go 协程发生切换的只会因为阻塞等待(IO/channel/mutex等),但 Go1.14 之后,运行时间超过 10ms 的协程会被标记为可抢占,可以被其他协程抢占 P 的执行。
归并案例
为了印证有时候多协程并不一定会提高性能,这里以归并排序为例举三个例子:

主函数:
func main() {
size := []int{128, 512, 1024, 2048, 100000, 1000000, 10000000}
sortVersion := []struct {
name string
sort func([]int)
}{
{"Mergesort V1", SequentialMergesortV1},
{"Mergesort V2", SequentialMergesortV2},
{"Mergesort V3", SequentialMergesortV3},
}
for _, s := range size {
fmt.Printf("Testing Size: %d\n", s)
o := GenerateSlice(s)
for _, v := range sortVersion {
s := make([]int, len(o))
copy(s, o)
start := time.Now()
v.sort(s)
elapsed := time.Since(start)
fmt.Printf("%s: %s\n", v.name, elapsed)
}
fmt.Println()
}
}
v1的实现:
func sequentialMergesort(s []int) {
if len(s) <= 1 {
return
}
middle := len(s) / 2
sequentialMergesort(s[:middle])
sequentialMergesort(s[middle:])
merge(s, middle)
}
func merge(s []int, middle int) {
// ...
}
v2的实现:
func SequentialMergesortV2(s []int) {
if len(s) <= 1 {
return
}
middle := len(s) / 2
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
SequentialMergesortV2(s[:middle])
}()
go func() {
defer wg.Done()
SequentialMergesortV2(s[middle:])
}()
wg.Wait()
Merge(s, middle)
}
v3的实现:
const max = 2048
func SequentialMergesortV3(s []int) {
if len(s) <= 1 {
return
}
if len(s) < max {
SequentialMergesortV1(s)
} else {
middle := len(s) / 2
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
SequentialMergesortV3(s[:middle])
}()
go func() {
defer wg.Done()
SequentialMergesortV3(s[middle:])
}()
wg.Wait()
Merge(s, middle)
}
}
耗时对比:
Testing Size: 128
Mergesort V1: 10.583µs
Mergesort V2: 211.25µs
Mergesort V3: 7.5µs
Testing Size: 512
Mergesort V1: 34.208µs
Mergesort V2: 500.666µs
Mergesort V3: 60.291µs
Testing Size: 1024
Mergesort V1: 48.666µs
Mergesort V2: 643.583µs
Mergesort V3: 47.084µs
Testing Size: 2048
Mergesort V1: 94.666µs
Mergesort V2: 786.125µs
Mergesort V3: 77.667µs
Testing Size: 100000
Mergesort V1: 6.810917ms
Mergesort V2: 58.148291ms
Mergesort V3: 4.219375ms
Testing Size: 1000000
Mergesort V1: 62.053875ms
Mergesort V2: 558.586458ms
Mergesort V3: 37.99375ms
Testing Size: 10000000
Mergesort V1: 632.3875ms
Mergesort V2: 5.764997041s
Mergesort V3: 388.343584ms
由于创建协程和调度协程本身也有开销,第二种情况无论多少个元素都使用协程去进行并行排序,导致归并很少的元素也需要创建协程和调度,开销比排序更多,导致性能还比不上第一种顺序归并。
特别在切片长度为1000w的时候,基于v2创建的协程数量共计百万。
而在本台电脑上,经过调试第三种方式可以获得比第一种方式更优的性能,因为它在元素大于2048个的时候,选择并行排序,而少于则使用顺序排序。但是2048是一个魔法数,不同电脑上可能不同。这里这是为了证明,完全依赖并发/并行的机制,并不一定会提高性能,需要注意协程本身的开销。
小结
可以克隆项目之后,使用协程池写一个归并排序,进一步比较内存消耗。
使用Golang的协程竟然变慢了|100万个协程的归并排序耗时分析的更多相关文章
- Stackful 协程库 libgo(单机100万协程)
libgo 是一个使用 C++ 编写的协作式调度的stackful协程库, 同时也是一个强大的并行编程库. 设计之初是为高并发分布式Linux服务端程序开发提供底层框架支持,可以让链接进程序的同步的第 ...
- 在C++中使用golang的协程
开源项目cpp_features提供了一个仿golang协程的stackful协程库. 可以在c++中使用golang的协程,大概语法是这样的: #include <iostream> v ...
- asyncio异步模块的21个协程编写实例
启动一个无返回值协程 通过async关键字定义一个协程 import sys import asyncio async def coroutine(): print('运行协程') if sys.ve ...
- Golang 之协程详解
转自:https://www.cnblogs.com/liang1101/p/7285955.html 一.Golang 线程和协程的区别 备注:需要区分进程.线程(内核级线程).协程(用户级线程)三 ...
- GoLang之协程
GoLang之协程 目前,WebServer几种主流的并发模型: 多线程,每个线程一次处理一个请求,在当前请求处理完成之前不会接收其它请求:但在高并发环境下,多线程的开销比较大: 基于回调的异步IO, ...
- Golang 的 协程调度机制 与 GOMAXPROCS 性能调优
作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:http://www.cnblogs.com/linguan ...
- 『GoLang』协程与通道
作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算)和程序的并发.程序可以在不同的处理器和计算机上同时执行不同的代码段.Go 语言为构建并发程序的基本代码块是 ...
- GoLang 的协程调度和 GMP 模型
GoLang 的协程调度和 GMP 模型 GoLang 是怎么启动的 关于 GoLang 的汇编语言,请查阅 参考文献[1] 和 参考文献[2] 编写一个简单的 GoLang 程序 main.go, ...
- Goroutine并发调度模型深度解析之手撸一个协程池
golanggoroutine协程池Groutine Pool高并发 并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题:Go语言作为一个出道以来就自带 『高并发』光环 ...
- 实现一个协程版mysql连接池
实现一个协程版的mysql连接池,该连接池支持自动创建最小连接数,自动检测mysql健康:基于swoole的chanel. 最近事情忙,心态也有点不积极.技术倒是没有落下,只是越来越不想写博客了.想到 ...
随机推荐
- Coding:小写一个debugfs
Coding:小写一个debugfs 上一次整活还是在上一个月,写了一个简单的module并且熟悉了module的挂载查看和卸载.这一次我们自然玩一个大的,就是利用linux的debugfs AP ...
- C语言gcc编译环境搭建
第一步,根据以下链接下载gcc工具包: gcc工具包下载地址: 链接:https://pan.baidu.com/s/1JqEjakTcWLPv7p6zkah6sA提取码:k4d2 第二步,将下载好的 ...
- vulnhub - ASSERTION1.0.1
vulnhub - ASSERTION 1.0.1 信息收集 nmap -sT --min-rate 10000 -p- 192.168.157.171 sudo nmap -sT -sV -sC - ...
- Java 面向对象编程之继承的super关键词
java 继承里面的super关键词 super关键词 一个引用变量,用于引用父类对象 父类和子类都具有相同的命名属性,要调用父类中的属性时使用 super也是父类的构造函数,格式super(参数) ...
- C#-WPF初学
1.新建一个WPF的应用: 2.拖拽控件并布局好: [小技巧]选中控件,点击"回形针"即可让该控件跟随窗口自动调整大小: 3.编写代码: 主程序代码如下: namespace Wp ...
- Python win11 安装lxml 失败
如果你有一个项目执行了requirements后,一直提示lxml失败,解决步骤如下 1.尝试升级pip python.exe -m pip install --upgrade pip 2.尝试下载包 ...
- [oeasy]python0082_[趣味拓展]控制序列_清屏_控制输出位置_2J
光标位置 回忆上次内容 上次了解了键盘演化的过程 ESC 从 组合键 到 独立按键 添加图片注释,不超过 140 字(可选) ESC的作用 是 进入 控制序列 配置 控制信息 控制信息 ...
- TIER 2: Oopsie
TIER 2: Oopsie Web 渗透 此次靶机结合前面知识,非常简单: nmap 扫描,发现 22 和 80 端口开放 服务 80 的 HTTP 服务 之后使用继续 Web 渗透: 使用 Wap ...
- python中pip安装包出现Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None))…………或无法pip install packname安装依赖包
问题: 安装包出现Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connec ...
- 浅谈 golang 代码规范, 性能优化和需要注意的坑
浅谈 golang 代码规范, 性能优化和需要注意的坑 编码规范 [强制] 声明slice 申明 slice 最好使用 var t []int 而不是使用 t := make([]int, 0) 因为 ...