Golang协程介绍

130 阅读2分钟

一、概述

协程(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毫秒
}

从两种方式对比来看,开启多个协程统计方式大大提升了性能。