0%

Go语言学习笔记8-Go语言并发

Go 语言通过编译器运行时(runtime),从语言上支持了并发的特性。Go 语言的并发通过goroutine 特性完成。
goroutine 类似于线程,但可以根据需要创建多个goroutine并发工作
goroutine 是由Go 语言的运行时调度完成,而线程是由操作系统调度完成

Go 语言还提供channel 在多个goroutine 间进行通信。goroutine 和 channel是Go 语言秉承CSP并发模式的重要实现基础。

Go 语言goroutine

在编写 Socket 网络程序时,需要提前准备一个线程池为每一个 Socket 的收发包分配一个线程。
开发人员需要在线程数量和 CPU 数量间建立一个对应关系,以保证每个任务能及时地被分配到 CPU 上进行处理,同时避免多个任务频繁地在线程间切换执行而损失效率。

如果面对随时随地可能发生的并发和线程处理需求,线程池就不是非常直观和方便了。能否有一种机制:使用者分配足够多的任务,系统能自动帮助使用者把任务分配到 CPU 上,让这些任务尽量并发运作。这种机制在 Go 语言中被称为 goroutine

goroutine 的概念类似于线程,但 goroutine 是由Go 程序运行时进行调度和管理。Go 程序会智能地将goroutine 中的任务合理地分配给每个CPU

Go 程序从main 包的main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的goroutine

使用普通函数创建 goroutine

Go 语言程序中使用go关键字为一个函数创建一个goroutine。一个函数可以被创建多个goroutine,一个goroutine必定对应一个函数

格式

为一个普通函数创建goroutine的写法如下:

1
go 函数名(参数列表)

使用 go关键字创建goroutine时,被调用函数的返回值会被忽略

如果需要在goroutine中返回数据,需要使用channel特性,通过通道(channel)把数据从goroutine中作为返回值传出

例子

使用 go 关键字,将 running() 函数并发执行,每隔一秒打印一次计数器,而 main 的 goroutine 则等待用户输入,两个行为可以同时进行。请参考下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
go running()
var input string
fmt.Scanln(&input) // 接受用户输入,直到按 Enter 键时将输入的内容写入 input 变量中并返回,整个程序终止。
}

func running(){
var times int
for {
times++
fmt.Println("tick" , times)

time.Sleep(time.Second)
}
}

命令行输出如下:
tick 1
tick 2
tick 3
tick 4
tick 5

代码执行后,命令行会不断地输出 tick,同时可以使用 fmt.Scanln() 接受用户输入。两个环节可以同时进行。

这个例子中,Go 程序在启动时,运行时(runtime)会默认为 main() 函数创建一个 goroutine。在 main() 函数的 goroutine 中执行到 go running 语句时,归属于 running() 函数的 goroutine 被创建,running() 函数开始在自己的 goroutine 中执行。此时,main() 继续执行,两个 goroutine 通过 Go 程序的调度机制同时运作。

使用匿名函数创建goroutine

go 关键字后也可以为匿名函数或者闭包启动goroutine

使用匿名函数创建goroutine

使用匿名函数或者闭包创建goroutine时,除了将函数定义部分卸载go 的后面之外,还需要加上匿名函数的调用参数,格式如下:

1
2
3
go func(参数列表){
函数体
}(调用参数列表)

使用匿名函数创建goroutine例子

在main() 函数中创建一个匿名函数并未匿名函数启动goroutine。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
go func() {
var times int
for {
times ++
fmt.Println("tick" , times)
time.Sleep(time.Second)
}
}()

var input string
fmt.Scanln(&input)
}

goroutine 虽然类似于线程概念,但是从调度性能上没有线程细致,而细致程度取决于 Go 程序的 goroutine 调度器的实现和运行环境。

终止 goroutine 的最好方法就是自然返回 goroutine 对应的函数

Go语言GOMAXPROCS(调整并发的运行性能)

在 Go 程序运行时(runtime)实现了一个小型的任务调度器。这套调度器的工作原理类似于操作系统调度线程,Go 程序调度器可以高效地将 CPU 资源分配给每一个任务。传统逻辑中,开发者需要维护线程池中线程与 CPU 核心数量的对应关系。同样的,Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到,格式为:

1
runtime.GOMAXPROCS(逻辑CPU数量)

这里的逻辑CPU数量可以有以下几种数值:

  • <1不修改任何数值
  • =1单核心执行
  • >1多喝并发执行

一般情况下,可以使用 runtime.NumCPU() 查询 CPU 数量,并使用 runtime.GOMAXPROCS() 函数进行设置,例如:

1
runtime.GOMAXPROCS(runtime.NumCPU())

GOMAXPROCS 同时也是一个环境变量,在应用程序启动前设置环境变量也可以起到相同的作用。

并发和并行的区别

下面让我们来了解并发和并行之间的区别:

  • 并发:把任务在不同的时间点交给处理器进行处理,在同一时间点,任务并不会同时运行
  • 并行:把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行

两个概念的区别是:任务是否同时执行。举一个生活中的例子:打电话和吃饭。

吃饭时,电话来了,需要停止吃饭去接电话。电话接完后回来继续吃饭,这个过程是并发执行。

吃饭时,电话来了,边吃饭边接电话。这个过程是并行执行。

GO 语言在 GOMAXPROCS 数量与任务数量相等时,可以做到并行执行,但一般情况下都是并发执行。

goroutine 和 coroutine的区别

coroutine 与 goroutine 在名字上类似,都可以将函数或者语句在独立的环境中运行,但是它们之间有两点不同:

  • goroutine 可能发生并行执行;
  • 但 coroutine 始终顺序执行。

狭义的说,

goroutine 可能发生在多线程环境下,goroutine无法控制自己获取高优先度支持

coroutine始终发生在单线程,coroutine 程序需要主动交出控制器,宿主才能获得控制权并将控制权交给其他coroutine

goroutine 间使用 channel 通信,coroutine 使用 yield 和 resume 操作。

coroutine 的运行机制属于协作式任务处理,早期的操作系统要求每一个应用必须遵守操作系统的任务处理规则,应用程序在不需要使用 CPU 时,会主动交出 CPU 使用权。如果开发者无意间或者故意让应用程序长时间占用 CPU,操作系统也无能为力,表现出来的效果就是计算机很容易失去响应或者死机。

goroutine 属于抢占式任务处理,已经和现有的多线程和多进程任务处理非常类似。应用程序对 CPU 的控制最终还需要由操作系统来管理,操作系统如果发现一个应用程序长时间大量地占用 CPU,那么用户有权终止这个任务。

Go语言通道(chan)