go基础之并发编程,goroutine

goroutine
在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?

Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

goroutine的使用
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

goroutine的使用
启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。

package main

import (
	"fmt"
	"time"
)

func testGoroutine() {
    
    
	for i := 0; i <= 9; i++ {
    
    
		fmt.Println("这是一个goroutine")
	}
}

func main() {
    
    
	go testGoroutine()
	for i := 0; i <= 9; i++ {
    
    
		fmt.Println("这是main函数")
	}
	// 在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。
	// 当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,
	// main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。
	time.Sleep(time.Second)
}

开启多个goroutine
使用sync.WaitGroup,等待goroutine结束

package main

import (
	"fmt"
	"sync"
)

var wy sync.WaitGroup

func testGoroutine() {
    
    
	//goroutine运行结束-1
	defer wy.Done()
	for i := 0; i <= 9; i++ {
    
    
		fmt.Println("这是testGoroutine函数")
	}
}

func goroutine() {
    
    
	//goroutine运行结束-1
	defer wy.Done()
	for i := 0; i <= 9; i++ {
    
    
		fmt.Println("这是goroutine函数")
	}
}

func main() {
    
    
	//开启了两个goroutine
	wy.Add(2)
	go testGoroutine()
	for i := 0; i <= 9; i++ {
    
    
		fmt.Println("这是main函数")
	}
	go goroutine()
	//当wy为0时就结束等待
	wy.Wait()
}

goroutine与线程

可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始是只是很小的栈(经典情况下2KB),goroutine的栈不是固定的,他可以按需要增大或缩小。goroutine的栈大小限制可达1GB,虽然很少会用到那么大。所以在go语言中一次创建十万左右个goroutine也是可以的

goroutine的调度
GPM是Go语言运行时runtime层实现的,是Go语言自己实现的一套调度系统,区别于操作系统调度OS线程
G很好理解,就是goroutine,里面除了存放goroutine的信息外,还有与所在P绑定等信息
P管理着一组goroutine,P里面存放当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界)P会对自己管理的goroutine队列做出一些调度,(比如把占用CPU时间较长的goroutine暂停,运行后续的goroutine等等)当自己的队列消费完会到全局队列中取goroutine,如果全局队列中的goroutine消费完了,就到其他的P中抢任务
M(machine)过运行时(runtime)对操作系统内核的虚拟,M与内核线程一般是一一映射的关系,一个goroutine最终要放在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调度方面的性能。

GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

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

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

package main

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

var wy sync.WaitGroup

func testGoroutine() {
    
    
	//goroutine运行结束-1
	defer wy.Done()
	for i := 0; i <= 9; i++ {
    
    
		fmt.Println("这是testGoroutine函数")
	}
}

func goroutine() {
    
    
	//goroutine运行结束-1
	defer wy.Done()
	for i := 0; i <= 9; i++ {
    
    
		fmt.Println("这是goroutine函数")
	}
}

func main() {
    
    
	wy.Add(2)
	runtime.GOMAXPROCS(2)
	go testGoroutine()
	go goroutine()
	wy.Wait()
}

猜你喜欢

转载自blog.csdn.net/weixin_44865158/article/details/114998623