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

1.并发与并行

并发:同一时间段执行多个任务
并行:同一时间点执行多个任务

  1. Go语言中的并发通过goroutine实现。
  2. goroutine属于用户态的线程(协程),支持千万级的并发。
  3. goroutine是由Go语言的timerun调度完成的,线程是由操作系统调度完成的。
  4. Go语言中使用channel在多个goroutine间进行通讯。
  5. goroutinechannel是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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值