关于goroutine和channel的学习| 青训营笔记

171 阅读6分钟

这是我参与「第五届青训营 」笔记创作活动的第8天

goroutine和channel是Golang中十分鲜明的特点,为Golang在高性能高并发编程方面打下深厚基础,之前的课程中关于这方面的了解只是浅尝辄止,今天来进一步学习。

goroutine和channel

goroutine基本介绍

进程

进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位

线程

进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位

进程和线程

一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行

一个程序至少有一个进程,一个进程至少有一个线程

并发和并行

1)多线程程序在单核上运行,叫做并发

  • 多个任务作用在一个CPU上
  • 从微观角度上看,在一个时间点只有一个任务在执行

2)多线程程序在多核上运行,叫做并行

  • 多个任务在多个CPU上运行
  • 从微观上看,某一个时间点有多个任务在执行

Go协程(goroutine)和Go主线程

为了解决:能否在底层上对相对笨重的线程做优化?

  1. 在Go上一个主线程可以有多个协程(协程可以理解为轻量级的线程【编译器优化】)
  2. 协程的特点:有独立的栈控件,共享程序的堆控件,调度由用户控制,协程是轻量级的线程

案例说明

//在主线程中开启一个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完成代码中出现的错误

  1. 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)
	}
}