go Goroutine和Channel的使用总结
Goroutine
Goroutine可以被看作是Go语言特有的应用程序线程,
传统的线程通讯:将数据存放在共享内存中,供多个线程中的程序访问。虽然在思路省操作非常简单,但却使并发控制变得相对麻烦。只有做到了各种约束和限制,才可以使这种方法实施。
go语言处理方法:不推荐使用共享内存区的方法传递数据,作为代替,因该优先使用Channel,Channel主要被用来在多个Goroutione之间传递数据,并且还会保证其过程的同步。作为一种可选方法,Go语言依然提供了一些传统的并发访问控制方法。
Go的线程实现模型,有三个必知的核心元素
M:machine的缩写。一个M代表一个内核线程,或者“工作线程”。
P:processor的缩写。一个P代表执行一个Go代码片段所必需的资源(或称“上下文环境”)。
G:goroutine的缩写。一个G代表一个Go代码片段。前者是对后者的一种封装。
简单来说,一个G的执行需要P和M的支持。一个M在与一个P关联之后,就形成了一个有效的G运行环境(内核线程+上下文环境)。
每个P都会包含一个可运行的G的队列(runq)。该队列中的G会被依次传递给与本地P关联的M,并获得运行时机。
在这里,我把运行当前G的那个M称为“当前M”,并把与当前M关联的那个P称为“本地P”
Goroution的运行流程(主线程)
1. 主线程Goroution是在runtime.m0(一个内核线程)上被运行的,
引导程序runtime.go就是在runtime.m0(一个内核线程)上运行,运行完runtime才会接着运行主线程。
2. 主线程Goroution设定每一个Goroutine所能申请的栈空间的最大尺寸。32位为250M,64位为1GB。如果某一个Goroutine超出这个界限就会报出"stack overflow"的运行时恐慌,程序的运行就会终止。
3. 主线程Goroution会启动系统检测器(对调度器的工作进行查漏补缺)。
4. 进行一系列初始化工作。(这些操作比较特殊,这时会将主线程Goroution与runtime.m0进行锁定,)
a.创建一个特殊的defer语句,以执行主Goroution退出时所做的善后处理(与当前的m进行解锁操作,防止非正常结束造成死锁)
b.检查当前M是否为runtime.m0。如果不是,说明此时程序出了问题。这时就会立即抛出异常,主程序停止。
c.创建定时垃圾回收器(scavenger)。主程序会创建一个专门的Goroution来封装这个定时垃圾回收器,并将它放入当前的M可运行的P队列中。定时垃圾回收会定时(当前是两分钟执行一次)的执行垃圾回收任务,并在必要的时候促使一些M协助他进行一些垃圾回收
d.执行main的init函数。
e.对之前的那个特殊defer语句进行最后的检查和设置,并必要时抛出异常。
5. 执行main函数。
6. 检查是否有Goroution发生了运行时恐慌,并进行必要处理。
7. 主线程Goroution结束自己及当前进程的运行。
runtime包和Groutine
看了主流程就会发现runtime包对Groutine有好多设置的操作。
runtime.GOMAXPROCS函数
通过runtime.GOMAXPROCS函数,应用程序何以在运行期间设置运行时系统中得P最大数量。但这会引起“Stop the Word”。所以,应在应用程序最早的调用。并且最好的设置P最大值的方法是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS,而不是在程序中调用runtime.GOMAXPROCS函数。
最后记住,无论我们传递给函数的整数值是什么值,运行时系统的P最大值总会在1~256之间。
runtime.Goexit函数
runtime.Goexit函数被调用后,会立即使调用他的Groution的运行被终止,但其他Goroutine并不会受到影响。runtime.Goexit函数在终止调用它的Goroutine的运行之前会先执行该Groution中还没有执行的defer语句。
runtime.Gosched函数
runtime.Gosched函数的作用是暂停调用他的Goroutine的运行,调用他的Goroutine会被重新置于Gorunnable状态,并被放入调度器可运行G队列中。
runtime.NumGoroutine函数
runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的Goroutine的数量。这里的特指是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。
注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。
runtime.LockOSThread和runtime.UnlockOSThread函数
前者调用会使调用他的Goroutine与当前运行它的M锁定到一起,后者调用会解除这样的锁定。
注意:
- 多次调用前者不会出现任何问题,但最后一次调用的记录会被保留,
- 即时之前没有调用前者,对后者的调用也不会产生任何副作用
debug.SetMaxStack函数
debug.SetMaxStack函数的功能是约束单个Groutine所能申请的栈空间的最大尺寸。
debug.SetMaxThreads函数
debug.SetMaxThreads函数的功能是对go语言运行时系统所使用的内核线程的数量(确切的说是M的数量)进行设置
runtime.GC函数
会让运行时系统进行一次强制性的垃圾收集,
- 强制的垃圾回收:不管怎样,都要进行的垃圾回收。
- 非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。
debug.SetGCPercent函数
用于设置一个比率(垃圾收集比率),前面所说的单元增量与前一次垃圾收集时的岁内存的单元数量和此垃圾手机比率有关。
<触发垃圾收集的堆内存单元增量>=<上一次垃圾收集完的堆内存单元数量>*(<垃圾收集比率>/100)
Channel
go语言所提倡的“应该以通讯手段来共享内”的最直接和最重要的体现。即通道类型,是go语言预定义的数据类型之一。
在同一时刻,仅有一个Goroutine能向一个通道发送元素值,同时也仅有一个Groutine能从他那里接收元素值。在通道中,各个元素值都是严格按照发送至此的先后顺序排列的。最早发送至通道的元素都会被最先接收。因此相当于(先进先出的)一个消息队列。
初始化
make(chan int,10)
第一个参数表明此值的具体类型是元素类型为int的通道类型,第二个参数则指出该值在同一时刻最多可以容纳10个元素值。也就是说,如果我们发送给该通道的值尚未背取走,那么该通道做多可以暂存10个元素值。
make(chan int)
我们对初始化一个字典类型的时候时候也可以类似这样做。不过这样做的效果却截然不同。
- 字典类型:
是否传递第二个值,只会影响到该值的初始长度。其初始长度与第二个长度相同,或者为零。由于字典类型的值是可以自动增长的,所以通常不会出现什么问题,
- 通道类型:
一个通道类型的值的缓冲容量是固定不变的,它可同时容纳的元素值的最大值永远等于他被初始化是给定的第二个参数值。如果第二个值被省略了,那么就表示被初始化的这个通道无法缓冲任何元素值,发送给他的元素值必须立即取走。
实际上,我们把初始化时给定了第二个参数的通道,称为缓冲通道,未给定第二个参数的通道称为非缓冲通道。
Happens before
如果一个通道是带缓冲的:
- 针对此通道的发送操作会被阻塞,直到被发送的元素完全被复制到通道的缓冲区中。(通道缓冲元素的这个动作一定发生在相应的发送动作之前。)
- 针对此通道的接收操作会阻塞,直到当前Goroutine真正从中获取到一个元素值。(当前Groutine接收某个元素的这个结果一定会在形成在相应的接收操作完成之前)
- 对于同一个元素值来说,把他发送给某个通道的操作,一定会在从该通道接收它的操作完成之前完成。
接收元素
strChan:=make(chan string,3)
elem:=<-strChan
如果通道中没有任何值,当前的Groutine会被阻塞到这里,进入Gwaiting状态,直到Groutine中有新值出现。如果在接收操作之前或者操作中该通道关闭了,那么该操作会立即结束,并且变量elem会被赋予该通道的元素类型的零值。
elem,ok:=<-strChan
如果通道中没有任何值,当前的Groutine会被阻塞到这里,进入Gwaiting状态,直到Groutine中有新值出现。如果在接收操作之前或者操作中该通道关闭了,那么该操作会立即结束,并且变量elem会被赋予该通道的元素类型的零值。
如果之前通道关闭了,则ok为flase,否则为true.
注意
试图从一个未初始化的通道类型值中哪里接元素值会造成当前的Groutine的永久阻塞!
发送元素
strChan<"a"
一个通道的缓冲容量是固定的。因此,当某个Groutine中向通道发送值的操作的时候,该Groutine会被阻塞在哪里。直到该通道中有足够的空间容纳该元素为止。
如果有多个Groutine因向一个通道发送元素值而被阻塞,那么当给通道中有多余的空间时,最早被阻塞的那个Groutine会最先被唤醒。也就是说,唤醒顺序和发送操作顺序相同。
无论发送操作还是接收操作,每次只会唤醒一个Groutine
注意
- 当我们像一个值为空的通道发送元素值,挡圈Groutine会被永久阻塞。
- 我们向一个通道发送一个值后,该通道只会得到该值的一个副本。
关闭通道
close(strChan)
不合时宜的关闭通道会给针对它的发送和接收操作带来问题。这样可能会对他们所在的Groutine的正常流程的执行产生影响。因此,我们要在保证安全的情况下进行关闭通道的操作。
更确切的讲,调用close函数的作用是告诉系统不应该再允许对被关闭的通道进行发送操作,该通道即将被关闭。只能让相应的通道进入关闭状态,而不是立即阻止对它的一切操作。
for与Channel
在需要循环接收通道值的元素情境下,我们总是应该优先使用for语句来实现。
当通道中没有任何元素的时候,当前Groutine依然会陷入阻塞。阻塞的具体位置会在其中的range子语句中。
语句for会不断的尝试从通道中接收元素值,直到该通道被关闭。
相应的通道关闭后,若通道中已无元素值或当前的Groutine正阻塞在这里,则for语句就会立即结束。而当此时通道中还有遗留的元素时,运行时体统会等for语句将他们全部接收后再结束该语句的执行。
栗子:
func batch(origs <-chan Person)<-chan Person{
dests:=make(chan Person,100)
go func(){
for p:=range origs {
hander.Handle(p)
dests<-p
}
fmt.Println("All the information has been handled")
close(deste)
}()
return deste
}
这个方法中,for语句不断尝试接收通道origs中的元素值。每接收一个元素值,for语句中的代码就会对他进行处理,并通过dests通道将处理的结果传递给下一道工序
select语句与Channal
一个select语句在被执行的时候,会选择执行其中的一个分支。在表现形式上,select语句与switch语句非常相似,但他们选择分支的方法完全是不同的。
组成和编写方法
在select语句中,每个分支依然以关键字case开始。但与switch不同的是,跟在每个case后面的只能是针对某个通道的发送语句或接收语句。
select语句也可以包含default case。如果select语句中的所有case都不满足选择条件且存在default case,那么default case就会被执行。
分支选择规则
- 在开始执行select语句的时候,所有跟在case关键字右边的发送语句或接受语句中的通道表达式和元素表达式都会先被求值。求值顺序是:自上而下、从左到右的。
- 其中的通道是可以为nil的,不管他是表达式还是标识符。但是这样的话,他所属的case就会永远被无视,就像case语句中不包含它一样。
- 在执行select语句的时候,运行系统自上而下地判断每个case中的发送或者接收操作是否可以被立即执行。这里的立即执行的意思是当前的Groutine不会因此操作而阻塞。
- 当发现第一个满足条件的case时,运行系统就会执行该case所包含的语句。这也意味着其他case被忽略。如果多个case满足条件,那么运行时系统就会通过一个伪随机的算法决定那个case将会被执行。
- 另一方面,如果被执行的select语句中的所有case都不满足选择的条件并且没有default case的话,那么当前Groutine就会一直被阻塞于此,直到某一个case 中的发送或接收操作可以被立即进行为止。(若这样的select语句中的所有case右边的通道都是nil,那么当前Groutine就会被永久地阻塞在这条语句上)
更多使用习惯
普通避免死锁
go func() {
for {
select{
case e,ok:=<-ch:
if !ok {
//End
break
}else{
//continue
}
}
}
}()
当通道关闭就停止
go func() {
var e int
ok:=true
for {
select{
case e,ok=<-ch:
if !ok {
//End
break
}else{
//continue
}
}
}
if !ok{
break
}
}()
流程执行超时
1
go func() {
for {
select {
case <- time.NewTimer(time.Millisecond).C:
fmt.Println("time out")
break
}
}
}()
2
go func() {
var e int
ok:=true
for {
select {
case e,ok=<-chaint://执行语句
case ok=<-func() chan bool{//发出超时语句
timeout:=make(chan bool,1)
go func() {
time.Sleep(time.Millisecond)
timeout<-false
}()
return timeout
}():
fmt.Println("timeOut=====")
break
}
if !ok {
break
}
}
}()