协程、延迟函数调用、以及恐慌和恢复

本文介绍了Go语言中的协程(goroutine)、并发同步以及延迟函数调用(deferred function call)和恐慌恢复(panic/recover)机制。详细阐述了协程的创建与状态、调度原理,以及延迟调用的功能、实参估值时刻。同时,讨论了恐慌和恢复在并发编程中的作用,强调了一些不可恢复的致命性错误。

协程(goroutine)

Go不支持创建系统线程,所以协程是一个Go程序内部唯一的并发实现方式。每个Go程序启动的时候只有一个对用户可见的协程,我们称之为主协程。一个协程可以开启更多其它新的协程。

在Go中,开启一个新的协程是非常简单的,我们只需要在一个函数调用之前使用一个go关键字,即可让此函数调用运行在一个新的协程之中。当此函数调用退出后,这个新的协程也随之结束。(不管其本身是否执行结束)我们可以称此函数调用为一个协程调用(或者为此协程的启动调用)。一个协程调用的所有返回值(如果存在的话)必须被全部舍弃。

并发同步(concurrency synchronization)

不同的并发计算可能会共享一些资源,其中共享内存资源最为常见。在一个并发程序中,常常会发生下面的情形:

  • 在一个协程向一段内存写数据的时候,另一个协程从此内存段读数据,结果导致读出的数据完整性得不到保证
  • 在一个协程向一段内存写数据的时候,另一个计算也向此段内存写数据,结果导致被写入的数据的完整性得不到保证

这些情形被称为数据竞争(data race)。并发编程的一大任务就是要调度不同协程,控制它们对资源的访问时段,以使数据竞争的情况不会发生。此任务常称为并发同步(或者数据同步)。Go支持几种并发同步技术。并发编程中的其它任务包括:

  • 决定需要开启多少协程
  • 决定何时开启、阻塞、解除阻塞和结束哪些协程
  • 决定如何在不同的协程中分担工作负载

协程的状态

一个活动中的协程可以处于两个状态:运行状态阻塞状态。一个协程可以在这两个状态之间切换。注意,一个处于睡眠中的(通过调用time.Sleep)或者在等待系统调用返回的协程被认为是处于运行状态,而不是阻塞状态。

当一个新协程被创建的时候,它将自动进入运行状态,一个协程只能从运行状态而不能从阻塞状态退出。一个处于阻塞状态的协程不会自发结束阻塞状态,它必须被另外一个协程通过某种并发同步方法来被动的结束阻塞状态。

如果一个运行中的程序当前所有的协程都处于阻塞状态,则这些协程将永远阻塞下去,程序将被视为死锁了。当一个程序死锁后,官方标准编译器的处理是让这个程序崩溃。

协程的调度

并非所有处于运行状态的协程都在执行。在任一时刻,只能最多有和逻辑CPU数目一样多的协程在同时执行。我们可以调用runtime.NumCPU函数来查询当前程序可利用的逻辑CPU数目

标准编译器采纳了一种被称为M-P-G模型的算法来实现协程调度。其中,M表示系统线程,P表示逻辑处理器(并非上述的逻辑CPU),G表示协程。大多数的调度工作是通过逻辑处理器(P)来完成的。逻辑处理器像一个监工一样通过将不同的处于运行状态的协程(G)交给不同的系统线程(M)来执行。一个协程在同一时刻只能在一个系统线程中执行。一个执行中的协程运行片刻后将自发的脱离让出一个系统线程,从而使得其它处于等待子状态的协程得到执行机会

在运行时刻,我们可以调用runtime.GOMAXPROCS(n)函数来获取和设置逻辑处理器的数量。当n<1时,则不会更改当前设置,自从Go1.5之后,默认初始逻辑处理器的数量和逻辑CPU的数量一致。默认设置在大多数情况下是最佳选择。但是对于某些文件操作十分频繁的程序,设置一个大于runtime.NumCPU()GOMAXPROCS值可能是有好处的。

延迟函数调用(defferred function call)

在Go中,一个函数调用可以跟在一个defer关键字后面,形成一个延迟函数调用。和协程调用类似,被延迟的函数调用的所有返回值必须全部被舍弃。

当一个函数调用被延迟后,它不会立即被执行。它将被推入由当前协程维护的一个延迟调用堆栈。当一个函数调用(可能是也可能不是一个延迟调用)返回并进入它的退出阶段后,所有在此函数调用中已经被推入的延迟调用将被按照它们被推入堆栈的顺序逆序执行。当所有这些延迟调用执行完毕后,此函数调用也就真正退出了。

下面这个例子展示了如何使用延迟调用函数

package main

import "fmt"

func main() {
	defer fmt.Println("The third line")
	defer fmt.Println("The second line")
	fmt.Println("The first line")
}

输出结果:

The first line
The second line
The third line

事实上,每个协程维护着两个调用堆栈

  • 一个是正常的函数调用堆栈。在此堆栈中,相邻的两个调用存在着调用关系。晚进入堆栈的调用被早进入堆栈的调用所调用。此堆栈中最早被推入的调用是对应协程的启动调用
  • 另一个堆栈是上面提到的延迟调用堆栈。处于延迟调用堆栈中的任意两个调用之间不存在调用关系
一个延迟调用可以修改包含此延迟调用的最内层函数的返回值
package mian

import "fmt"

func Triple(n int) (r int) {
	defer func() {
		r += n // 修改返回值
	}
	
	return n + n
}
延迟函数调用的必要性和好处

延迟调用对于恐慌/恢复特性是必要的。另外延迟函数调用可以帮助我们写出更整洁和更棒的代码。

协程和延迟调用的实参的估值时刻

一个协程调用或者延迟调用的实参是在此调用发生时被估值的。更具体的说:

  • 对于一个延迟函数调用,它的实参是在此调用被推入延迟调用堆栈的时候被估值的
  • 对于一个协程调用,它的实参是在此协程被创建的时候估值的

一个匿名函数体内的表达式是在此函数被执行的时候才会被逐个估值的,不管此函数是被普通调用还是延迟/协程调用

package mian

import "fmt"

func main() {
	func() {
		for i:=0;i<3;i++ {
			defer fmt.Println("a:",i)
		}
	}()
	fmt.Println()
	func() {
		for i:=0;i<3;i++ {
			defer func() {
				fmt.Println("b:",i)
			}()
		}
	}()
}

打印结果:

a:2
a:1
a:0

b:=3
b:=3
b:=3

恐慌(panic)和恢复(recover)

Go不支持异常抛出和捕获,而是推荐使用返回值显示返回错误。不过,Go支持一套和异常抛出/捕获类似的机制。此机制称为恐慌/恢复(panic/recover)机制

我们可以调用内置函数panic来产生一个恐慌以使当前协程进入恐慌状态。一个恐慌不会蔓延到其它协程。

进入恐慌状态是另一种使当前函数调用开始返回的途径。一旦一个函数调用产生一个恐慌,此函数调用将立即进入它的退出阶段,在此函数调用中被推入堆栈的延迟调用将按照它们被推入的顺序逆序执行。

通过在一个延迟函数调用之中调用内置函数recover,当前协程中的一个恐慌可以被消除,从而使得当前协程重新进入正常状态。

一些致命性错误不属于恐慌

对于官方标准编译器来说,很多致命性错误(比如堆栈溢出和内存不足)不能被恢复。它们一旦产生,程序将崩溃。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值