这是我参与「第五届青训营 」笔记创作活动的第8天
goroutine和channel是Golang中十分鲜明的特点,为Golang在高性能高并发编程方面打下深厚基础,之前的课程中关于这方面的了解只是浅尝辄止,今天来进一步学习。
goroutine和channel
goroutine基本介绍
进程
进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
线程
进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
进程和线程
一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
一个程序至少有一个进程,一个进程至少有一个线程
并发和并行
1)多线程程序在单核上运行,叫做并发
- 多个任务作用在一个CPU上
- 从微观角度上看,在一个时间点只有一个任务在执行
2)多线程程序在多核上运行,叫做并行
- 多个任务在多个CPU上运行
- 从微观上看,某一个时间点有多个任务在执行
Go协程(goroutine)和Go主线程
为了解决:能否在底层上对相对笨重的线程做优化?
- 在Go上一个主线程可以有多个协程(协程可以理解为轻量级的线程【编译器优化】)
- 协程的特点:有独立的栈控件,共享程序的堆控件,调度由用户控制,协程是轻量级的线程
案例说明
//在主线程中开启一个goroutine,该协程每隔一秒输出“hello,world”
//主线程中野每隔一秒输出“helloworld”,输出10次后退出程序
//要求主线程和goroutine同时执行
// 写一个每隔一秒输出helloworld的函数
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() hello,world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() //这样就开启了一个协程
for i := 1; i < 10; i++ {//主线程结束的话,无论协程是否结束都会结束程序
fmt.Println("main() hello,golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
快速入门小结
MPG模式基本介绍
1)M:是一个操作性的主线程(是物理线程
2)P:协程执行需要的上下文
3)G:协程
Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程
其他语言的多线程往往是内核态的,比较重量级,几千个线程可能耗光CPU
goroutine的调度模型
Golang中设置运行的CPU数目
- 需要用到的包:runtime.NumCPU()
- 需要用到的函数 runtime.GOMAXPROCS
channel管道
看个需求-由需求引入学习
要计算1-200的各个数的阶乘,并且把各个数的阶乘放到map中。最后显示出来,要求使用goroutine完成
在用goroutine完成代码中出现的错误
fatal error: concurrent map writes这是由于go中的map是不安全的,用多个协程同时写入map时就是报这个错误,直译就是并发mapx写入
2.同时,还伴随一种错误,那就是再协程跑完之前,主线程就跑完了!所以在得出结果之前一切就都结束了!粗暴一点的解决方案就是直接主线程结束前另其休眠,等待协程跑完
分析思路
使用goroutine,效率高,但是会出现并发/安全问题
代码(存在问题)
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//这里我将res的值让如到mymap中
myMap[n] = res
}
func main() {
//在这里开启多个协程完成任务
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠十秒钟
time.Sleep(time.Second * 10)
//这里输出结果,遍历这个结果
for i, v := range myMap {
fmt.Printf("map%v = %v\n", i, v)
}
}
fatal error: concurrent map write
解决方案1:加入锁
逻辑是这样的:当协程1第一次来到这里,就会拿到锁(lock),就可以继续往下执行,这个时候协程2也来到了这里!,发现了锁(lock),协程2进入队列,协程3来到这里,发现了lock和正在排队的协程2,继续排在协程2后面,以此类推....
加入锁
使用全局变量并用互斥锁
在包 sync中,提供基本的同步基元,如互斥锁,除了once和waitgroup,大部分都适用于低水平程序线程,高水平的同步使用channel通信更好一些
var lock sync.Mutex声明
lock.Lock() lock.Unlock()加锁与解锁
解决方案2:channel管道
这才是go推荐的方式,既可以解决goroutine运行结束时间很难确定的问题,又可以解决资源竞争的问题
channel是线程安全,多个协程操作同一个管道时,不会发生资源竞争
- 本质就是一个队列!
- 数据时先进先出的(FIFO)
- 线程安全,多goroutine访问时,不需要枷锁,channel本身就是线程安全的
管道本身也是有数据类型的,什么类型的管道放什么类型的数据
基本用法
var 变量名 chan 数据类型
var intChan chan int (intCahn存int类型数据)<-------这是例子
//演示一下管道的使用
//1.创建一个可以存放三个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2.看看intChan是什么
fmt.Printf("intChan的值 = %v 以及intChan本身的地址= %p\n", intChan, &intChan)
//3.向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- 50
//看看管道的长度和容量
fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan))
//长2 容量 3----->因为塞入了两个数,make的空间数量是3
//长度不得超过容量
//从管道中读取数据
var num2 int
num2 = <-intChan//管道里面的数据可以取出但是不接收哦!
fmt.Println("num2=", num2)
//在没有协程的情况下如果管道的数据已经全部取出,再取就会报告deadlock
细节与练习
- 如果是要把map推入管道内的话,则需要在推入之前先把map的空间make好
- var allChan chan interface{} 数据类型用空接口,但是仍然不能实现混合数据传输,因为当传入第一个变量进管道的时候,就会导致interface的类型变成了该变量的类型
-
allChan := make(chan interface{}, 3) allChan <- 10 allChan <- "Tom jack" cat := Cat{"小花猫", 4} allChan <- cat //我们希望得到管道中的第三个元素,则需要将前两个推出 <-allChan <-allChan newcat := <-allChan fmt.Printf("new cat=%T,newcat=%v\n", newcat, newcat) //fmt.Printf("newcat,Name=%v\n", newcat.Name) 会报错的,不存在这种方法因为数据类型默认已经不是猫猫了 a := newcat.(Cat) //使用类型断言即可正常使用
管道的关闭和遍历
管道关闭后,读取数据还是可以的,但是不能再往其中写入数据了
用内置函数 close(管道)即可关闭
需要遍历管道里面的数据时,也需要关闭管道,如果没有关闭会出现deadlock的错误,已经关闭就会正常进行
遍历也不能用普通的for循环,要用range
for v := range intChan2{
fmt.println("%v=",v)
}
协程配合管道的经典案例
/ writeData
func writeData(intChan chan int) {
for i := 0; i < 50; i++ {
//放入数据
intChan <- i
fmt.Println("WriteData=", i)
//time.Sleep(time.Second)
}
close(intChan) //关闭
}
// read data
func readData(intChan chan int, exitchan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Printf("readData 读到数据%v\n", v)
//time.Sleep(time.Second)
}
//raedData 读取完数据后,即任务完成
exitchan <- true
close(exitchan)
}
func main() {
//创建两个管道
//创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
//time.Sleep(time.Second * 10)
}
管道阻塞的机制
管道如果存储只有写入但是没有读取会阻塞
但是如果仅仅只是读写评率不一样,仅仅只会降低读取/写入的速度罢了
应用实例3,协程求素数
思路分析
- 先设计一个管道intChan
- 启动一个协程putNum(容量大小1000),用来不断放入数据
- 再启动余下协程primeNum协程去从管道中不停的去取出num,并计算是否为素数
- 再设计一个管道primeChan(2000),容量必须真实体现出数据量
- 怎么才能知道管道何时取完呢?
- 当intChan读取完毕之后close
- 再写一个管道,exitChan(4)
- 当一个协程完成任务后,就往exit管道中放入一个数据
- 只要保证能从exitchan管道取出4个T,就能结束主线程
//代码
func InputNum(intChan chan int) {
for i := 2; i <= 8000; i++ {
intChan <- i
}
close(intChan)
}
func PrimeNum(intChan chan int, primeChan chan int, exitchan chan bool) {
//使用for循环
var flag bool
for {
num, ok := <-intChan
if !ok { //intChan中已经取不到数据了
break
}
flag = true
for i := 2; i < num; i++ {
if num%i == 0 { //说明该number不是素数
flag = false
break
}
}
if flag {
//将数据放入 primeChan中
primeChan <- num
}
}
//有一个prime协程完成工作退出了
fmt.Println("有一个prime协程完成工作退出了")
//这里害不能关闭primeChan
//向exitChan 写入true
exitchan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
//标识退出的管道
exitChan := make(chan bool, 4) //4个
//开启一个协程,向intChan放入1-8000个数据
go InputNum(intChan)
//开启四个协程,从intchan中取出数据,并判断是否为素数,如果是,就放入到primeChan中
for i := 0; i < 4; i++ {
go PrimeNum(intChan, primeChan, exitChan)
}
//主线程,进行处理
//直接
go func() {
for i := 0; i < 4; i++ {
<-exitChan //取不到4就等待,我们本质上不是真的想获取exitchan管道上的数据,就是把exitchan管道当做某种阻塞来用,防止主线程结束导致其他线程也结束
}
close(primeChan)
}()
var res int
var ok bool
for {
res, ok = <-primeChan
if !ok {
break
}
fmt.Printf("素数= %d\n", res)
}
fmt.Println("main线程退出")
}
管道的注意事项和细节
只读与只写
- 管道可以声明为制度或者只写
- 在默认情况下,管道时双向的
var chan1 chan int //可读可写 - 只写
var chan2 chan<- int chan2 = make(chan int,3) - 只读
var chan3 <-chan int
- 在默认情况下,管道时双向的
关于select
使用select可以解决从管道取数据的阻塞问题
传统方法中,遍历管道时如果不关闭会阻塞而导致 deadlock,可是实际开发中我们不好确定什么时候该关闭管道
为此,引出了select!
select {
case v := intChan://如果这里intChan一直没有关闭,不会一直阻塞和deadlock
fmt.Printf("从intChan读取的数据%d\n",v)
case v := <-stringchan:
fmt.Printf("从stringChan读取的数据%d\n",v)
default :=
fmt.printf("已经都取不到了,程序员可以加入逻辑\n")
}
recover
goroutine中使用recover + defer 可以解决协程中出现panic,导致程序崩溃
用recover捕获panic,进行处理,这样及时这个协程发生了问题,但是主线程仍然不受影响,可以继续执行
defer fun() {
//用来捕获test抛出的panic
if err := recover(); err != nil {
fmt.println("test发生错误,"err)
}
}