有关Groutine无限创建的分析
有关Groutine无限创建的分析
文章目录
- 有关Groutine无限创建的分析
- 从操作系统分析进程、线程、协程的区别
- 进程内存
- 线程内存
- 执行单元
- cpu切换成本
- 协程切换成本
- 线程切换成本
- 内存占用
- Go程是否可以无限创建
- 不控制go程创建引发的问题
- 简单方式控制go程创建
- channel有buffer
- sync.WaitGroup
- channel有buffer + sync.WaitGroup
- channel无buffer + (初始准备一定数量的协程)任务发送 和 任务执行 分离
- 总结
从操作系统分析进程、线程、协程的区别
在操作系统中,进程是就是一个程序运行。他是独占一个内存空间的个体。操作系统分配资源时,就是按照进程的极限内存大小分配的。因此,进程是操作系统资源分配的最小单元
。线程是进程的一部分,多个线程构成一个进程。在一个进程中,有一块全局的内存空间(堆空间)堆空间是对所有线程共享的,每个线程都有一个独立的栈空间。因此,线程可以独立运行。cpu分配时间片就是按照进程中线程的数量分的。所以,线程是cpu调度的最小单元。
由于线程的开辟受限于硬件资源,为了适应高并发的场景,增加并发量,减少cpu上下文的切换开销。我们提出了协程,协程是工作于用户态的。每个协程需要绑定一个线程才可以正常运行。协程的大小是线程的1/400000。因此可以极大的减少内存开销,同时协程上下文的切换是在用户态的,不需要用户态->内核态的转变,极大的减少了cpu的时间资源。
进程内存
首先,进程内存是一个独立的内存块,向os申请的一块虚拟内存。进程的内存由几部分组成:
内存区域 | 作用 | 特点 |
---|---|---|
代码段(Text) | 存储进程的可执行代码(机器指令) | 只读,多个进程可共享同一份代码(如共享库) |
数据段(Data) | 存储已初始化的全局变量和静态变量(如 static int a = 10; ) | 读写权限,程序运行期间持续存在 |
BSS 段 | 存储未初始化的全局变量和静态变量(初始化为 0 或 NULL) | 不占用可执行文件磁盘空间,运行时由操作系统自动清零 |
堆(Heap) | 动态内存分配区域(如 C 语言的 malloc 、Go 的 new ) | 由程序主动申请 / 释放,大小不固定,可能产生内存碎片 |
栈(Stack) | 存储函数调用的局部变量、参数、返回地址等 | 由操作系统自动管理,后进先出(LIFO),大小通常有限制(如 Linux 默认 8MB) |
共享库 / 动态链接 | 存储程序运行时加载的动态链接库(如 Linux 的 .so 文件) | 多个进程可共享同一库的内存实例,节省内存资源 |
内核空间 | 进程访问内核服务时使用的内存区域(如系统调用、文件描述符表) | 每个进程共享内核空间,用户态程序不能直接访问 |
直接由操作系统调度,操作系统以进程为单位分配资源,因此进程是操作系统资源分配的最小单元。
线程内存
线程在进程的栈区有自己的栈区,其余部分,进程内的所有线程共享。共享使得线程之间的通信更容易,内存关联性很大,一个出现问题,导致其他线程出问题,进而影响进程。所以线程是cpu调度的最小单元。
执行单元
对于Linux来讲,对于进程和线程是相同的,都被视作一个单独的执行单位。所以,进程中的线程数量影响cpu时间片的分配。通过提高进程中线程的数量来提高进程在cpu中分配到的时间片的比例。那是不是开辟越多线程越好?当然不是:cpu切换一个执行单元会花费时间和性能开销。(原因:页表查询缓慢,切换使得cache失效使得缓存命中率低,宏观表现运行速度慢)
cpu切换成本
由于cpu切换需要花费大量成本,所以不能无限制的开辟线程,我们可以考虑在用户态进行切换执行流程,让cpu内核态不再执行切换流程。因此,我们就在用户态创建一个伪执行单元——协程。
协程切换成本
用户态:当前cpu寄存器状态保存,将需要切换进来的协程cpu寄存器状态加载到cpu寄存器上。
线程切换成本
- 用户态和内核态的切换
- 内核态:操作系统调度
- 上下文大小:除了和协程一样的上下文,还有线程特有的栈、寄存器等,上下文比协程的大一点
内存占用
可以使用命令查看内存:
ulimit -s
Go程是否可以无限创建
go程虽然每个也就2.5KB左右,但是也不能无限创建。从我们做实验就可以得到。最终会耗尽cpu资源,导致panic
不控制go程创建引发的问题
for i:=0;i<math.Maxint64;i++{go func(i int){do ...fmt.Println("go func ",i,"go程 count = ",runtime.NumGoroutine())}(i)
}
短时间占据操作系统资源:cpu、内存、文件描述符等。使得:cpu使用率浮动上涨、内存占用上涨、主进程崩溃(被杀掉)。
简单方式控制go程创建
channel有buffer
eg: make(chan bool,3)
可以,但是需要最后阻塞,保证所有任务都结束
sync.WaitGroup
eg:
var wg = sync.WaitGroup{}
wg.Add(1)
wg.Done()
wg.Wait()
不可以,执行的速度远小于创建的速度。因为创建和执行都没有限制速度,只是保证开启的协程全部可以完成
channel有buffer + sync.WaitGroup
可以,在2的基础上可以控制生成速度。平衡 生成和消费的速度
channel无buffer + (初始准备一定数量的协程)任务发送 和 任务执行 分离
无buffer,那么生成的速度取决于消费的速度,当有协程接收任务时才有任务被生产到chan中。消费的速度取决于我们一开始生产的协程数目
总结
主要从操作系统的层面介绍了进程、线程、协程,并以cpu切换成本的角度对比了线程和协程的区别。后面,又介绍了go是否可以无限创建的问题。并提出如何有效控制go的数量。