内容简介:相信大家在学习Go的过程中,都会看到类似这样一句话:”与传统的系统级线程和进程相比,协程的最大优势在于其‘轻量级’,可以轻松创建上百万个而不会导致系统资源衰竭”。那是不是意味着我们在开发过程中,可以随心所欲的调用协程,而不关心它的数量呢?答案当然是否定的。我们在开发过程中,如果不对Goroutine加以控制而进行滥用的话,可能会导致服务程序整体崩溃。这里我先模拟一下协程数量太多的危害:
相信大家在学习 Go 的过程中,都会看到类似这样一句话:”与传统的系统级线程和进程相比,协程的最大优势在于其‘轻量级’,可以轻松创建上百万个而不会导致系统资源衰竭”。那是不是意味着我们在开发过程中,可以随心所欲的调用协程,而不关心它的数量呢?
答案当然是否定的。我们在开发过程中,如果不对Goroutine加以控制而进行滥用的话,可能会导致服务程序整体崩溃。
这里我先模拟一下协程数量太多的危害:
func main() { number := math.MaxInt64 for i := 0; i < number; i++ { go func(i int) { // 做一些业务逻辑处理 fmt.Printf("go func: %d\n", i) time.Sleep(time.Second) }(i) } }
如果number是用户输入的一个参数,没有做限制。有些开发人员会全部丢进去进行循环,认为全部都并发使用Goroutine去做一件事情,效率比较高。但这样的话,噩梦般的事情就开始了,服务器系统资源利用率不断上涨,到最后程序自动killed。
通过执行top命令查看到该程序占用的CPU、内存较高。
为了避免上图这种情况,下面会简单的介绍一下Goroutine以及在我们日常开发中如何控制Goroutine的数量。
一、基本介绍
工欲善其事必先利其器。先简单的介绍一下Goroutine,Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。
为了更好理解Goroutine,先讲一下进程、线程和协程的概念。
进程(process):用户下达运行程序的命令后,就会产生进程。同一程序可产生多个进程(一对多关系),以允许同时有多位用户运行同一程序,却不会相冲突。进程需要一些资源才能完成工作,如CPU使用时间、存储器、文件以及I/O设备,且为依序逐一进行,也就是每个CPU核心任何时间内仅能运行一项进程。进程的局限是创建、撤销和切换的开销比较大。
线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。
协程(coroutine):又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。和线程类似,共享堆,不共享栈,协程的切换一般由 程序员 在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。
Goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是Goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程,同时它可以运行在一个或多个线程上。
在Go中生成一个Goroutine的方式非常的简单:只要在函数前面加上go就生成了。
func number() { for i := 0; i < ; i++ { fmt.Printf("%d ", i) } } func main() { go number() // 启动一个goroutine number() }
二、协程池解决?
回到开头的问题,如何控制Goroutine的数量?相信有过开发经验的人,第一想法是生成协程池,通过协程池控制连接的数量,这样每次连接都从协程池里去拿。在Golang开发中需要协程池吗?这里分享下知乎有个相关点赞最高的回答:
显然不需要,goroutine的初衷就是轻量级的线程,为的就是让你随用随起,结果你又搞个池子来,这不是脱裤子放屁么?你需要的是限制并发,而协程池是一种违背了初衷的方法。池化要解决的问题一个是频繁创建的开销,另一个是在等待时占用的资源。goroutine 和普通线程相比,创建和调度都不需要进入内核,也就是创建的开销已经解决了。同时相比系统线程,内存占用也是轻量的。所以池化技术要解决的问题goroutine 都不存在,为什么要创建 goroutine pool 呢?如果因为 goroutine 持有资源而要去创建goroutine pool,那只能说明代码的耦合度较高,应该为这类资源创建一个goroutine-safe的对象池,而不是把goroutine本身池化。
在我们日常大部分场景下,不需要使用协程池。因为Goroutine非常轻量,默认2kb,使用go func()很难成为性能瓶颈。当然一些极端情况下需要追求性能,可以使用协程池实现资源的复用,例如FastHttp使用协程池性能提高许多。
当然现在我们如果需要使用Goroutine池也不需要重复造轮子了,目前github上已经有开源的项目ants来实现 Goroutine 池。ants已经实现了对大规模 Goroutine 的调度管理、Goroutine 复用,允许使用者在开发并发程序的时候限制 Goroutine 数量,复用资源,达到更高效执行任务的效果。
项目地址: https://github.com/panjf2000/ants
三、 通过channel和sync方式限制协程数量
3.1 Channel
Goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么Goroutine之间如何进行数据的通信呢?Go提供了一个很好的通信机制channel,channel可以与 Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make创建channel。
3.2 Sync
Go语言中有一个sync.WaitGroup,WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。下面示例代码中wg.Wati会阻塞代码的运行,直到计数器值为0。
通过Golang自带的channel和sync,可以实现需求,下面代码中通过channel控制Goroutine数量。
package main import ( "fmt" "sync" "time" ) type Glimit struct { n int c chan struct{} } // initialization Glimit struct func New(n int) *Glimit { return &Glimit{ n: n, c: make(chan struct{}, n), } } // Run f in a new goroutine but with limit. func (g *Glimit) Run(f func()) { g.c <- struct{}{} go func() { f() <-g.c }() } var wg = sync.WaitGroup{} func main() { number := 10 g := New(2) for i := 0; i < number; i++ { wg.Add(1) value :=i goFunc := func() { // 做一些业务逻辑处理 fmt.Printf("go func: %d\n", value) time.Sleep(time.Second) wg.Done() } g.Run(goFunc) } wg.Wait() }
四、总结
在文章的开头通过在服务器模拟Goroutine数量太多导致系统资源上升,提醒大家避免这类问题。当然每个人可根据自己所在的场景选择最合适的方案,有时候成熟的第三方库也是个很好的选择,可以避免重复造轮子。
下面有两个思考问题,大家可以尝试着去思考一下。
思考1:为什么我们要使用sync.WaitGroup?
这里如果我们不使用sync.WaitGroup控制的话,原因出在当主程序结束时,子协程也是会被终止掉的。因此剩余的 goroutine 没来及把值输出,程序就已经中断了
思考2:代码中channel数据结构为什么定义struct,而不定义成bool这种类型呢?
因为空结构体变量的内存占用大小为0,而bool类型内存占用大小为1,这样可以更加最大化利用我们服务器的内存空间。
func main(){ a :=struct{}{} b := true fmt.Println(unsafe.Sizeof(a)) # println 0 fmt.Println(unsafe.Sizeof(b)) # println 1 }
【作者简介】
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Java并发 -- 线程数量
- 来,控制一下 Goroutine 的并发数量
- 控制 Goroutine 的并发数量的方式
- Go 开发中,如何有效控制 Goroutine 的并发数量
- 字符串的长度,是字符数量,还是字节数量?
- kafka增加topic的备份数量
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beginning Google Maps API 3
Gabriel Svennerberg / Apress / 2010-07-27 / $39.99
This book is about the next generation of the Google Maps API. It will provide the reader with the skills and knowledge necessary to incorporate Google Maps v3 on web pages in both desktop and mobile ......一起来看看 《Beginning Google Maps API 3》 这本书的介绍吧!
XML、JSON 在线转换
在线XML、JSON转换工具
RGB CMYK 转换工具
RGB CMYK 互转工具