Go 并发编程原理
Go 语言的协程实现被称之为 goroutine,由 Go 运行时管理,在 Go 语言中通过协程实现并发编程非常简单:我们可以在一个处理进程中通过关键字 go
启用多个协程,然后在不同的协程中完成不同的子任务,这些用户在代码中创建和维护的协程本质上是用户级线程,Go 语言运行时会在底层通过调度器将用户级线程交给操作系统的系统级线程去处理,如果在运行过程中遇到某个 IO 操作而暂停运行,调度器会将用户级线程和系统级线程分离,以便让系统级线程去处理其他用户级线程,而当 IO 操作完成,需要恢复运行,调度器又会调度空闲的系统级线程来处理这个用户级线程,从而达到并发处理多个协程的目的。此外,调度器还会在系统级线程不够用时向操作系统申请创建新的系统级线程,而在系统级线程过多的情况下销毁一些空闲的线程,这个过程和 PHP-FPM 的工作机制有点类似,实际上这也是很多进程/线程池管理器的工作机制,这样一来,可以保证对系统资源的高效利用,避免系统资源的浪费。
以上,就是 Go 语言并发编程的独特实现模型。
协程简单示例
下面通过一个简单的示例来演示如何在 Go 语言中通过协程进行并发编程,我们在 add.go
中编写一个加法函数 add
并通过协程的方式来调用它:
package main import "fmt" func add(a, b int) { var c = a + b fmt.Printf("%d + %d = %d", a, b, c) } func main() { go add(1, 2) }
嗯,就是这么简单,在这段代码中包含了两个协程,一个是显式的,通过 go
关键字声明的这条语句,表示启用一个新的协程来处理加法运算,另一个是隐式的,即 main
函数本身也是运行在一个主协程中,该协程和调用 add
函数的子协程是并发运行的两个协程,就好比从 go
关键字开始,从主协程中叉出一条新路。
并发执行示例
目前为止,我们仅仅演示了 Go 语言协程的启动和简单使用,但是通过上述代码还不足以验证协程是并发执行的,接下来,我们通过下面这段代码来验证协程的并发执行
package main import ( "fmt" "time" ) func add(a, b int) { var c = a + b fmt.Printf("%d + %d = %d\n", a, b, c) } func main() { for i := 0; i < 10; i++ { go add(1, i) } time.Sleep(1e9) }
通过 channel 进行消息传递
前面我们说到通道是一种数据类型,和数组/切片类型类似,一个通道只能传递一种类型的值,这个类型需要在声明 通道时指定。在使用通道时,需要通过 make
进行声明,通道对应的类型关键字是 chan
:
ch := make(chan int)
这里我们初始化了一个通道类型 ch
,其中只能传递 int
类型的值。
我们可以把通道看作是一个先进先出(FIFO)的队列,通道中的元素会严格按照发送顺序排列,继而按照排列顺序被接收,通道元素的发送和接收都可以通过 <-
操作符来实现,发送时元素值在右,通道变量在左:
ch <- 1 // 表示把元素 1 发送到通道 ch
接收时通道变量在右,可以通过指定变量接收元素值:
element := <-ch
也可以留空表示忽略:
<-ch
package main import ( "fmt" "time" ) func add(a, b int, ch chan int) { c := a + b fmt.Printf("%d + %d = %d\n", a, b, c) ch <- 1 } func main() { start := time.Now() chs := make([]chan int, 10) for i := 0; i < 10; i++ { chs[i] = make(chan int) go add(1, i, chs[i]) } for _, ch := range chs { <- ch } end := time.Now() consume := end.Sub(start).Seconds() fmt.Println("程序执行耗时(s):", consume) }