一、概述
协程(Goroutines) 是与其他函数或方法同时运行的函数或方法。 Goroutines可以被认为是轻量级线程。与线程相比,创建 Goroutine 的成本很小。因此,Go 应用程序通常会同时运行数千个 Goroutine。
协程的优势:
-
与线程相比,
Goroutines非常小。它们的堆栈大小只有几 kb,堆栈可以根据应用程序的需要增长和缩小,而在线程的情况下,堆栈大小必须指定并固定。
-
Goroutines被多路复用到较少数量的 OS 线程。一个包含数千个
Goroutine的程序中可能只有一个线程。如果该线程块中的任何Goroutine要等待用户输入,则创建另一个 OS 线程,并将剩余的Goroutine移动到新的 OS 线程。所有这些都由运行时处理,开发者可以从这些复杂的细节中抽象出来,并获得了一个干净的 API 来处理并发。
-
Goroutine使用通道(channel)进行通信。通道的设计可以防止在使用
Goroutine访问共享内存时发生竞争条件。通道可以被认为是一个管道,Goroutines使用它进行通信。
二、goroutine快速入门
2.1 并发执行
编写一个函数,该函数每隔1秒输出"Hello GO"
import (
"fmt"
"strconv"
"time"
)
func test(){
for i := 1;i<=5;i++{
fmt.Println("test() Hello GO",strconv.Itoa(i)) //strconv.Itoa 数字转字符串
time.Sleep(time.Second)
}
}
func main() {
test()
for i:= 1;i<=5;i++{
fmt.Println("main() Hello Golang",strconv.Itoa(i)) //strconv.Itoa 数字转字符串
time.Sleep(time.Second)
}
}
运行结果
test() Hello GO 1
test() Hello GO 2
test() Hello GO 3
test() Hello GO 4
test() Hello GO 5
main() Hello Golang 1
main() Hello Golang 2
main() Hello Golang 3
main() Hello Golang 4
main() Hello Golang 5
分析
上面并发的场景下,程序会先执行test()函数下的代码进行输出,而主线程等待
当test()执行完毕后,才会继续执行主线程,这样就是并发,将所有任务放在一个cpu上。
2.2 并行执行
在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出"test() Hello GO"
在主线程中也每隔 1 秒输出"main() Hello Golang", 输出 5 次后,退出程序,要求主线程和goroutine 同时执行。
import (
"fmt"
"strconv"
"time"
)
func test02(){
for i := 1;i<=10;i++{
fmt.Println("test() Hello GO",strconv.Itoa(i)) //strconv.Itoa 数字转字符串
//睡眠1s
time.Sleep(time.Second)
}
}
func main() {
//在函数前面加上一个关键字go 表明开启一个协程来运行这个函数
go test02()
for i:= 1;i<=5;i++{
fmt.Println("main() Hello Golang",strconv.Itoa(i)) //strconv.Itoa 数字转字符串
//睡眠1s
time.Sleep(time.Second)
}
}
运行结果
main() Hello Golang 1
test() Hello GO 1
main() Hello Golang 2
test() Hello GO 2
test() Hello GO 3
main() Hello Golang 3
main() Hello Golang 4
test() Hello GO 4
test() Hello GO 5
main() Hello Golang 5
说明
- 从运行结果可以看出,主线程执行完毕后即使协程没有执行完毕,程序也会退出;
- 协程可以在主线程没有执行完毕前提前退出,协程是否执行完毕不会影响主线程的执行。
为了保证程序可以顺利执行,让协程执行完毕后在执行主线程退出,可以使用sync.WaitGroup等待协程执行完毕。
三、sync.WaitGroup介绍
sync.WaitGroup用来实现启动一组goroutine,并等待任务做完再结束goroutine。
sync.WaitGroup方法
| 方法 | 说明 |
|---|---|
wg.Add() | main协程通过调用 wg.Add(delta int) 设置worker协程的个数,然后创建worker协程 |
wg.Done() | worker协程执行结束以后,都要调用 wg.Done(),表示做完任务,goroutine减1 |
wg.Wait() | main协程调用 wg.Wait() 且被block,直到所有worker协程全部执行结束后返回 |
针对可能panic的goroutine,可以使用defer wg.Done()来结束goroutine。
示例
import (
"fmt"
"sync"
"time"
)
//主协程退出后所有协程无论有没有执行完毕都会退出,所以我们在主进程中可以通过WaitGroup等待协程执行完毕
var wg sync.WaitGroup
func test03(){
for i := 0; i < 3; i++ {
fmt.Println("test03 - Hello GO",i)
time.Sleep(time.Millisecond * 100)
}
wg.Done() //协程计数器-1
}
func test04(){
for i := 0; i < 3; i++ {
fmt.Println("test04 - Hello Golang",i)
time.Sleep(time.Millisecond * 100)
}
wg.Done() //协程计数器-1
}
func main() {
wg.Add(1) //协程计数器+1
go test03() //开启一个协程
wg.Add(1) //协程计数器+1
go test04() //开启一个协程
wg.Wait() //等待协程执行完毕
fmt.Println("主线程退出...")
}
运行结果
test04 - Hello Golang 0
test03 - Hello GO 0
test04 - Hello Golang 1
test03 - Hello GO 1
test03 - Hello GO 2
test04 - Hello Golang 2
主线程退出...
四、启动多个 Goroutine
通过在主线程中使用for循环,可以启动多个
goroutine同时使用
sync.WaitGroup来实现等待goroutine执行完毕
import (
"fmt"
"sync"
"time"
)
var wg2 sync.WaitGroup
func test05(num int){
defer wg2.Done()
for i := 1;i <= 2;i++{
fmt.Printf("协程(%v)输出的第\t%v\t条数据\n", num, i)
time.Sleep(time.Millisecond * 100)
}
}
func main() {
for i := 1; i <= 2; i++ {
wg2.Add(1)
go test05(i)
}
wg2.Wait()
fmt.Println("主线程关闭...")
}
运行结果
协程(2)输出的第 1 条数据
协程(1)输出的第 1 条数据
协程(1)输出的第 2 条数据
协程(2)输出的第 2 条数据
主线程关闭...
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为goroutine之间是并发执行的,而 goroutine 的调度是随机的。
五、设置 Golang 并行运行的时候占用的cup数量
-
Go 运行时的调度器使用
GOMAXPROCS参数来确定需要使用多少个OS 线程来同时执行Go代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,调度器会把Go 代码同时调度到 8 个 OS 线程上。
-
Go 语言中可以通过
runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU 逻辑核心数。
Go1.5 版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU逻辑核心数。
示例
import (
"fmt"
"runtime"
)
func main() {
//获取当前计算机上面的CPU个数
numCPU := runtime.NumCPU()
fmt.Println("numCPU=",numCPU)
//可以自己设置使用多个CPU
runtime.GOMAXPROCS(numCPU - 1)
fmt.Println("OK")
}
六、Goroutine 统计素数
6.1 传统for循环实现
需求:要统计1-120000的数字中哪些是素数
import (
"fmt"
"time"
)
func main() {
start := time.Now().Unix()
for num := 2;num < 120000;num++{
var flag = true
for i := 2; i < num; i++ {
if num % i == 0 {
flag = false
break
}
}
if flag {
//fmt.Println(num,"是素数")
}
}
end := time.Now().Unix()
fmt.Println(end - start) //15毫秒
}
6.2 goroutine 开启多个协程统计
import (
"fmt"
"sync"
"time"
)
/*
1 协程 统计 1-30000
2 协程 统计 30001-60000
3 协程 统计 60001-90000
4 协程 统计 90001-120000
// start:(n-1)*30000+1 end:n*30000
*/
var wg3 sync.WaitGroup
func test06(n int){
for num := (n-1)*30000 + 1;num < n*30000; num++ {
if num > 1 {
var flag = true
for i := 2; i < num; i++{
if num % i == 0 {
flag = false
break
}
}
if flag {
//fmt.Println(num,"是素数")
}
}
}
wg3.Done()
}
func main() {
start := time.Now().Unix()
for i := 1; i <= 4; i++ {
wg3.Add(1)
go test06(i)
}
wg3.Wait()
fmt.Println("执行完毕")
end := time.Now().Unix()
fmt.Println(end - start) //3毫秒
}
从两种方式对比来看,开启多个协程统计方式大大提升了性能。