Contents
8.1. 轻量级线程-goroutine¶
8.1.1. 1.进程/线程¶
进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。
8.1.2. 2.并发/并行¶
多线程程序在单核上运行,就是并发
多线程程序在多核上运行,就是并行
并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。
并发和并行之间的区别。
并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
举生活中的一个例子:
并发: 吃饭时,电话来了,需要停止吃饭去接电话,电话完后继续来吃饭,这个过程就是并发执行。
并行: 吃饭时,电话来了,边接电话边吃饭,这个过程是并行执行。
GO在GOMAXPROCS数量与任务数量相等时,可以做到并行执行,但一般情况下都是并发执行。
并发不是并行。并行是让不同的代码片段同时在不同的物理处理器上执行。 并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。
在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。 这种“使用较少的资源做更多的事情”的哲学,也是指导 Go语言设计的哲学。
8.1.3. 3.协程/线程¶
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。使用Go语言开发服务器程序时,就需要对它的并发机制有深入的了解。
goroutine的概念类似于线程,但goroutine由Go程序运行时的调度和管理。Go程序会智能地将goroutine中的任务合理
地分配给每个CPU。
Go程序从main包的main()函数开始,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。
8.1.4. 4.使用普通函数创建goroutine¶
Go程序中使用go关键字为一个函数创建一个goroutine。一个函数可以被创建多个goroutine,一个goroutine必须对应一个函数。
格式
goroutine语法:
go 函数名(参数列表)
· 函数名:要调用的函数名
· 参数列表:调用函数需要传入的参数。
使用go关键字创建goroutine时,被调用函数的返回值会被忽略。
4.1 例子¶
使用go关键字,将running()函数并发执行,每隔一秒打印一次计数器, 而 main 的 goroutine 则等待用户输入,两个行为可以同时进行。
package main
import (
"fmt"
"time"
)
func running() {
var times int
// 使用for形成一个无限循环。
for {
// times变量在循环中不断自增。
times++
// 输出times变量的值。
fmt.Println("tick:", times)
// 使用time.Sleep暂停1秒后继续循环。
time.Sleep(time.Second)
}
}
func main() {
// 使用go关键字让running()函数并发运行。
go running()
// 接受用户输入,直到按Enter键时将输入的内容写入input变量中并返回,整个程序终止。
var input string
fmt.Scanln(&input)
}
命令行输出如下:
tick 1
tick 2
tick 3
tick 4
tick 5
8.1.5. 5.使用匿名函数创建goroutine¶
go关键字后也可以为匿名函数或闭包启动goroutine。
5.1 使用匿名函数创建goroutine的格式¶
使用匿名函数或闭包创建 goroutine 时,除了将函数定义部分写在 go 的后面之外,还需要加上匿名函数的调用参数,格式如下:
go func( 参数列表 ){
函数体
}( 调用参数列表 )
其中:
·参数列表:函数体内的参数变量列表。
·函数体:匿名函数的代码。
·调用参数列表:启动goroutine时,需要向匿名函数传递的调用参数。
5.2 匿名函数创建goroutine的例子1¶
在main()函数中创建一个匿名函数并为匿名函数启动goroutine。 匿名函数没有参数。代码将并行执行定时打印计数的效果
package main
import (
"fmt"
"time"
)
func main() {
// go后面接匿名函数启动goroutine。
go func() {
var times int
for {
times++
fmt.Println("tick", times)
time.Sleep(time.Second)
}
}()
var input string
fmt.Scanln(&input)
}
/*
tick 1
tick 2
tick 3
tick 4
tick 5
.....
*/
提示:所有goroutine在main()函数结束时会一同结束。
goroutine虽然类似于线程概念,但是从调度性能上没有线程细致,而细致程度取决于Go程序的goroutine调度器的实现和运行环境。
终止goroutine的最好方法就是自然返回goroutine对应的函数。 虽然可以用golang.org/x/net/context包进行goroutine生命期深度控制,但这种方法仍然处于内部试验阶段,并不是官方推荐的特性。
截止Go 1.9版本,暂时没有标准接口获取goroutine的ID。
5.3 匿名函数创建goroutine的例子2¶
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
//// 分配一个逻辑处理器给调度器使用
runtime.GOMAXPROCS(1)
// wg用来等待程序完成
// 计数加2,表示要等待两个goroutine
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("Start Goroutines")
// 启动第1个goroutine
go func() {
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done()
//// 显示字母表3次
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ",char)
}
}
}()
// 启动第2个goroutine
go func() {
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done()
//// 显示字母表3次
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ",char)
}
}
}()
//等待goroutine结束
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating Program")
}
8.1.6. 6.调整并发的运行性能(GOMAXPROCS)¶
在Go程序运行时(runtime)实现了一个小型的任务调度器。
这套调度器的工作原理类似于操作系统调度线程,
Go程序调度器可以高效地将CPU资源分配给每一个任务。
传统逻辑中,开发者需要维护线程池中线程与CPU核心数量的对应关系。
同样的,Go地中也可以通过runtime.GOMAXPROCS()函数做到,格式为:
runtime.GOMAXPROCS(逻辑CPU数量)
这里的逻辑CPU数量可以有如下几种数值:
<1:不修改任何数值。
=1:单核心执行。
>1:多核并发执行。
一般情况下,可以使用runtime.NumCPU()查询CPU数量,并使用runtime.GOMAXPROCS()函数进行设置,例如:
runtime.GOMAXPROCS(runtime.NumCPU())
Go 1.5版本之前,默认使用的是单核心执行。 从Go 1.5版本开始,默认执行上面语句以便让代码并发执行,最大效率地利用CPU。
GOMAXPROCS同时也是一个环境变量,在应用程序启动前设置环境变量也可以起到相同的作用。
package main
import (
"fmt"
"runtime"
)
func main() {
// 获取当前系统cpu的数量
num := runtime.NumCPU()
// 设置num的cpu运行go程序
runtime.GOMAXPROCS(num)
fmt.Println("num = ", num) //num = 4
}
6.1 示例代码¶
package main
import (
"fmt"
"runtime"
"time"
)
func A() {
for i := 1; i < 10; i++ {
fmt.Println("A", i)
}
}
func B() {
for i := 1; i < 10; i++ {
fmt.Println("B", i)
}
}
func main() {
// 设置CPU的核心数量
// runtime.GOMAXPROCS(1)
num := runtime.NumCPU()
runtime.GOMAXPROCS(num)
go A()
go B()
time.Sleep(time.Second)
}
Go语言中的操作系统线程和goroutine的关系:
一个操作系统线程对应用户态多个goroutine。
go程序可以同时使用多个操作系统线程。
goroutine和OS线程是多对多的关系,即m:n。