天天看点

16.1Go语言干货-并发编程之goroutine

1.并发与并行

并发:同一

时间段

执行多个任务

并行:同一

时间点

执行多个任务

  1. Go语言中的并发通过

    goroutine

    实现。
  2. goroutine

    属于用户态的线程(协程),支持千万级的并发。
  3. goroutine

    是由Go语言的

    timerun

    调度完成的,线程是由操作系统调度完成的。
  4. Go语言中使用

    channel

    在多个

    goroutine

    间进行通讯。
  5. goroutine

    channel

    是Go语言秉承了CSP并发模式的基础实现的。

2.goroutine

2.1 使用goroutine

在Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上

go

关键字,就可以为一个函数创建有个

goroutine

一个

goroutine

必须对应一个函数,可以创建多个

goroutine

去执行相同的函数。

2.2 启动单个

goroutine

启动

goroutine

只需要在调用的函数(匿名函数或者普通函数)前面加一个关键字

go

举个栗子:

package main

import (
	"fmt"
	"time")


func nowTime() {
	nowtime := time.Now()
	fmt.Println(nowtime.Format("2006-01-02 15:04:06"))
}
func main() {
	go nowTime()
	fmt.Println("this is main function")
	// time.Sleep(10)
}

           

当我们不执行

time.Sleep()

时,结果只打印了

this is main function

这是为什么?

  1. 在程序启动时,Go语言会为

    main()

    创建一个默认的

    goroutine

  2. main

    函数(主函数)执行完毕的时候该主函数的

    goroutine

    也就结束了。这时创建的分支函数,可能还没有执行完毕也被强制结束了。
  3. 创建新的

    goroutine

    需要一些时间的。
  4. 所以加上一个

    time.Sleep()

    就可以等待分支函数执行完毕。

2.3 启动多个

goroutine

启用多个

goroutine

举个栗子:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func f1(n int64) {
	fmt.Println(n)
	defer wg.Done()
}

func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go f1(int64(i))
	}

	fmt.Println("main")
	wg.Wait()
}

           

2.4 sync.WaitGroup

第一个栗子我们使用的

time.Sleep()

用来阻塞主函数,第二个栗子使用的是

sync.WaitGroup

,二者由什么区别呢?

使用time.Sleep()将这个阻塞给写死了,我们不知道子函数执行需要多少时间,然后定死了一个时间。这个有很大的弊端,阻塞时间给多个浪费工作效率,阻塞时间给少了可能子函数还没有执行完成。

Go语言提供了一个解决方案,

sync.WaitGroup

是一个结构体,在他的内部维护了一个计数器,使用

.Add(1)

时给计数器加1,使用

.Done()

给计数器减1。最后使用

.Wait()

来判断计数器是否归零。

方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0

3.

goroutine

与线程

3.1 可增长的栈

操作系统一般都有固定的栈内存,通常为2MB,一个

goroutine

的栈在其生命周期开始时只有很小的栈(2KB)。

goroutine

的栈不是固定的,可以增大也可以缩小。最大限制为1GB。

在Go语言中一次创建十万左右的

goroutine

是可以的。

3.2

goroutine

的调度

GPM

是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统,区别于操作系统调度OS线程。

1.G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。

2.P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。

3.M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

3.3 GOMAXPROCS

1.Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。

2.默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

3.Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

4.Go1.5版本之前,默认使用的是单核心执行。

5.Go1.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果

两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。

举个栗子

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func f1() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Println("f1", i)
	}
}

func f2() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Println("f2", i)
	}
}

func main() {
	wg.Add(2)
	runtime.GOMAXPROCS(1)
	go f1()
	go f2()
	fmt.Println("main")
	wg.Wait()
}
`
main
f2 0
f2 1
f2 2
f2 3
f2 4
f2 5
f2 6
f2 7
f2 8
f2 9
f1 0
f1 1
f1 2
f1 3
f1 4
f1 5
f1 6
f1 7
f1 8
f1 9
`
           

将逻辑核心数设为2,此时两个任务并行执行,代码如下。

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func f1() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Println("f1", i)
	}
}

func f2() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Println("f2", i)
	}
}

func main() {
	wg.Add(2)
	runtime.GOMAXPROCS(12)
	go f1()
	go f2()
	fmt.Println("main")
	wg.Wait()
}

           

1.一个操作系统线程对应多个用户态

goroutine

2.go程序可以同时使用多个操作系统线程

3.操作系统线程与

goroutine

是多对多关系,即

m:n

go