• 十四、goroutine(协程)和channel(管道)


    16.1 goroutine(协程)

    16.1.1 基本介绍

    进程和线程说明

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

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

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

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

    16.1.2程序、进程和线程的关系示意图

    fdafaf.PNG

    16.1.3 并发和并行

    1、多线程程序在单核上运行,就是并发

    2、多线程程序在多核上运行,就是并行

    fdafaf.PNG

    并发:因为是在一个CPU上,比如有10个线程,每个线程执行10毫秒(进行轮训操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发

    并行:因为是在一个CPU上(比如有10个CPU),比如有10个线程,每个线程执行10毫秒(各自在不同CPU上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也其实只有10个线程在执行,这就是并行

    16.1.4 Go协程 和 Go主线程

    1、Go主线程(有程序员直接称为线程/也可以理解为进程):一个Go线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程(编译器做优化)

    2、Go协程的特点

    • 有独立的栈空间
    • 共享程序堆空间
    • 调度由用户控制
    • 协程是轻量级的线程

    fdafaf.PNG

    16.1.5 入门案例

    1、在主线程(可以理解成进程)中,开启一个gorotine,该协程每隔1秒输出“hello world”

    2、在主线程中也每隔1秒输出“hello world”,输出10次后,退出程序

    3、要求主线程和gorotine同时执行

    4、画出主线程和协程执行流程图

    代码实现

    package main
    import (
    	"fmt"
    	"strconv"
    	"time"
    )
    // 编写一个函数,每隔1秒输出"hello world"
    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)
    	}
    }
    

    fdafaf.PNG

    输出的效果说明,main这个主线程和test协程同时执行

    主线程和携程执行流程图

    fdafaf.PNG

    入门小结

    1、主线程是一个物理线程,直接作用在CPU上,是重量级的,非常耗费CPU资源

    2、协程是从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小

    3、Golang的协程机制是重要的特点,可以轻松点的开启上万个协程,其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显了Golang在并发上的优势了

    16.1.6 gorotine的调度模型

    MPG模型基本介绍

    M:操作系统的主线程(是物理线程)

    P:协程执行需要的上下文

    G:协程

    16.1.6.1 MPG模式运行—状态1

    fdafaf.PNG

    1、当前程序有三个M,如果三个M都在一个CPU运行,就是并发,如果在不同的CPU运行,就是并行

    2、M1,M2,M3正在执行一个G,M1的协程队列有3个,M2的协程队列有3个,M3协程队列有2个

    3、从上图可以看到:Go的协程是轻量级的协程,是逻辑态的,Go可以容易的起上万个协程

    4、其他程序c/java,往往谁内核态的,比较重量级,几千个县城可能耗光CPU

    16.1.6.2 MPG模式运行—状态2

    fdafaf.PNG

    1、分成两个部分来看

    2、原来的情况是M0主线程正在执行G0协程,另外有三个协程在队列等待

    3、如果G0协程阻塞,比如读取文件或者数据库等

    4、这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写

    5、这样的MPG调度模式,可以即让G0执行。同时也不会让队列的其他协程一直阻塞,仍然可以并发/并行执行

    6、等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒

    16.2 设置Golang运行的cpu数

    ​ 为了充分利用多cpu的优势,在Golang程序中,设置运行的cpu数目

    package main
    import (
    	"fmt"
    	"runtime"
    )
    
    func main() {
    	cpuNum := runtime.NumCPU()
    	fmt.Println("cpuNum=", cpuNum)
    	// 可以自己设置使用几个CPU
    	runtime.GOMAXPROCS(cpuNum - 1)
    	fmt.Println("ok")
    }
    

    16.3 channel(管道)

    16.3.1 入门案例

    需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中,最后显示出来,要求使用gorotine完成

    在运行某个程序时,如果直到是否存在资源竞争问题,方法很简单,在编译该程序时增加一个参数 -race即可

    代码

    package main
    import (
    	"fmt"
    	"time"
    )
    
    // 1、编写一个函数,,来计算各个数的阶乘,并放入到map中
    // 2、我们启动的协程是多个,统计的将结果放入到map中
    // 3、map应该做成全局的
    
    var (
    	myMap = make(map[int]int, 10)
    )
    
    // test函数就是计算n!,将这个结果放入到 myMap
    func test(n int) {
    	
    	res := 1
    	for i := 1; i <= n; i++ {
    		res *= i
    	}
    	// 这里我们将res放入到myMap
    	myMap[n] = res
    }
    
    func main() {
    	// 我们这里开启多个协程完成这个任务[200个]
    	for i := 1; i <= 200; i++ {
    		go test(i)
    	}
    	// 休眠10秒钟
    	time.Sleep(time.Second * 10)
    
    	// 输出结果,遍历这个结果
    	for i, v := range myMap {
    		fmt.Printf("map[%d]=%d
    ",i, v)
    	}
    }
    

    结果

    NCzpWV.png

    示意图

    NCzKSK.png

    16.3.2 不同gorotine之间如何通讯

    • 全局变量加锁同步

    • channel

    16.3.3 使用全局变量加锁同步改进程序

    • 因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map wrotes
    • 解决方案:加入互斥锁
    • 我们的数的阶乘很大,结果会越界,可以将求阶乘改成sum += unit64(i)

    代码改进

    NPC7DO.png

    NPilOf.png

    NPidlq.png

    NPFYDK.png

    16.3.4 channel的介绍

    • channel本质就是一个数据结构-队列
    • 数据是先进先出
    • 线程安全,多gototine访问时,不需要加锁,就是说channel本身就是线程安全的
    • channel 是有类型的,一个string的channel只能存放string类型数据

    NPEk3d.png

    16.3.5 管道基本使用

    声明/定义 channel

    var 变量名 chan  数据类型
    举例:
    var intChan  chan int  (intChan用于存放int数据)
    var mapChan  chan map[int]string  (mapChan 用于存放map[int]string类型)
    
    说明:
    - channel 是引用类型
    - channel必须初始化才能写入数据,即make后才能使用
    - 管道是有类型的, intChan只能写入整数int
    

    16.3.6 管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项

    channel初始化

    说明:使用make 进行初始化

    var  intChan chan int
    intChan = make(chan int, 10)
    

    向channel写入(存入)数据

    var intChan chan int
    intChan = make(chan int, 10)
    num := 999
    intChan <- 10
    intChan <- num
    
    package main
    import (
    	"fmt"
    )
    
    func main() {
    	// 1、创建一个可以存放3个int类型
    	var intChan chan int 
    	//这里的 3 决定了这个 intChan管道 的容量为3,不能超过 3 这个值
    	intChan = make(chan int, 3) 
    
    	// 2、看看intChan是什么
    	fmt.Printf("intChan 的值=%v intChan本身的地址=%p
    ", intChan, &intChan)
    
    	// 3、向管道写入数据
    	intChan<- 10
    	num := 211
    	intChan<- num
    	// 注意!!!:当我们给管道写入数据时,不能超过其容量
    
    	// 4、看看管道的长度和cap(容量)
    	fmt.Printf("channel len=%v cap=%v
    ", len(intChan), cap(intChan))
    
    	// 5、从管道读取数据
    
    	var num2 int
    	num2 = <-intChan
    	fmt.Println("num2=", num2)
    	fmt.Printf("channel len=%v cap=%v
    ", len(intChan), cap(intChan))
    
    	// 6、在没有使用协程的情况下,如果我们的管道数据已经被全部取出
    	// 再取就会报告 deadlock
    	num3 := <-intChan
    	num4 := <-intChan
    	fmt.Println("num3=", num3, "num4=", num4)
    }
    

    注意事项

    1、channel中只能存放指定的数据类型

    2、channel的数据放满后,就不能再放入了

    3、如果从channel取出数据后,可以继续放入

    4、在没有协程的情况下,如果channel数据取完了,再去就会报dead lock

    案例

    package main
    import (
    	"fmt"
    )
    type Cat struct {
    	Name string
    	Age int
    }
    func main() {
    	// 定义一个可以存放任意数据类型的管道 interface
    	allChan := make(chan interface{}, 3)
    	allChan <- 10
    	allChan <- "tom jack"
    	cat := Cat{"小花猫", 4}
    	allChan <- cat
    	// 我们希望获得到管道中的第三个元素,则先将前2个推出
    	<- allChan
    	<- allChan
    	newCat := <-allChan  //从管道中取出的猫是什么?
    	fmt.Printf("newCat=%T, newCat=%v", newCat, newCat)
    	// 下面的写法是错误的,编译不通过。
    	// fmt.Printf("newCat=%v", newCat.Name)
    	// 解决上面的问题:类型断言
    	a := newCat.(Cat) 
    	fmt.Printf("newCat名字=%v", a.Name)
    }
    

    16.4 channel的遍历和关闭

    channel的关闭

    使用内置函数close可以关闭channel,当channel关闭后,就不能向channel写数据了,但是仍然可以从该channel读取数据

    package main
    import (
    	"fmt"
    )
    func main(){
    	intChan := make(chan int, 3)
    	intChan <- 100
    	intChan <- 200
    	close(intChan)  //close
    	// 这时不能够再写入数据到管道
    	// intChan <- 300
    	fmt.Println("okokok~")
    	// 当管道关闭后,读取数据是可以的
    	n1 := <- intChan
    	fmt.Println("n1=", n1)
    }
    

    channel的遍历

    channel支持for--range的方方式进行遍历,请注意2个细节

    • 在遍历时,如果channel没有关闭,则会出现dead lock的错误
    • 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
    package main
    import (
    	"fmt"
    )
    
    func main(){
    	intChan2 := make(chan int, 100)
    	for i :=0; i < 100; i++ {
    		intChan2<- i * 2  //放入100个数据到管道
    	}
    	// 遍历管道不能使用普通的for 循环
    	// 在变量时,如果channel咩有关闭,则会出现dead lock的错误
    	// 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
    	close(intChan2)
    	for v := range intChan2 {
    		fmt.Println("v=", v)
    	}
    }
    

    16.5 应用实例

    案例1

    请完成gorotine和channel协同工作的案例,要求如下:

    1、开启一个writeData协程,向管道intChan中写入50个整数

    2、开启一个readData协程,从管道intChan中读取writeData写入的数据

    3、注意:writeData和readDaata操作的是同一管道

    4、主线程需要等待writeData和readData协程都完成工作才能退出管道。

    思路分析图

    fdafaf.PNG

    package main
    import (
    	"fmt"
    	// "time"
    )
    
    // write Data
    
    func writeData(intChan chan int) {
    	for i :=1; 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
    		}
    		// time.Sleep(time.Second)
    		fmt.Printf("readData 读到数据=%v
    ", v)
    	}
    	// readData读取完数据后,即任务完成
    	exitChan <- true
    	close(exitChan)
    }
    
    func main(){
    	// 创建2个管道
    	intChan := make(chan int, 50)
    	exitChan := make(chan bool, 1)
    
    	go writeData(intChan)
    	go readData(intChan, exitChan)
    
    	// time.Sleep(time.Second * 10)
    	for {
    		_, ok := <-exitChan
    		if !ok {
    			break
    		}
    	}
    }
    

    案例2 -阻塞

    package main
    import (
    	"fmt"
    	"time"
    )
    
    // write Data
    
    func writeData(intChan chan int) {
    	for i :=1; 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
    		}
    		time.Sleep(time.Second)
    		fmt.Printf("readData 读到数据=%v
    ", v)
    	}
    	// readData读取完数据后,即任务完成
    	exitChan <- true
    	close(exitChan)
    }
    
    func main(){
    	// 创建2个管道
    	intChan := make(chan int, 10)
    	exitChan := make(chan bool, 1)
    
    	go writeData(intChan)
    	go readData(intChan, exitChan)
    	// time.Sleep(time.Second * 10)
    	for {
    		_, ok := <-exitChan
    		if !ok {
    			break
    		}
    	}
    }
    

    问题:

    如果注销掉 go readData(intChan, exitChan),程序会怎样?

    答:如果只是向管道写入数据,而没有读取,就会出现阻塞而dead lock,原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在writeData的 intChan <- i

    如果编译器(运行),发一个管道,只有写,而没有读,则该管道会阻塞,写管道和读管道的频率不一致,无所谓。

    案例3

    Ni1uR0.png

    思路图:

    Ni3WN9.png

    代码

    package main
    import (
    	"fmt"
    	"time"
    )
    
    // 开启一个协程,向 intChan 放入1-8000个数
    func putNum(intChan chan int) {
    	for i := 0; i < 8000; i++ {
    		intChan <- i
    	}
    	// 关闭intChan
    	close(intChan)
    }
    
    // 开启4个协程,从 intChan 取出数据,并判断是否为素数,
    // 如果是就放入到primeChan
    func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
    	// 使用for循环
    	var flag bool 
    	for {
    		num, ok := <-intChan
    		time.Sleep(time.Millisecond * 10)
    		if !ok {
    			break //intChan管道取不出东西了
    		}
    		flag = true //假设是素数
    		// 判断num是否是素数
    		for i := 2; i < num; i++ {
    			if num % i == 0 { //说明该num不是素数
    				flag = false
    				break
    			}
    		}
    		if flag {
    			// 将这个数就放入到primeChan
    			primeChan<- num
    		}
    	}
    	fmt.Println("有一个primerNum 协程因为取不到数据,退出")
    	// 这里还不能关闭primerChan
    	// 向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 putNum(intChan)
    	// 开启4个协程,从 intChan 取出数据,并判断是否为素数,
    	// 如果是就放入到primeChan
    	for i := 0; i < 4; i++ {
    		go primeNum(intChan, primeChan, exitChan)
    	}
    	// 这里主线程开始进行处理
    	go func(){
    	for i := 0; i < 4; i++ {
    		<-exitChan
    	}
    	// 当从exitChan取出了4个结果,就可以关闭primeChan管道
    	close(primeChan)
    	}()
    
    	// 遍历primeChan,把结果取出
    	for {
    		res, ok := <- primeChan
    		if !ok {
    			break
    		}
    		// 将结果输出
    		fmt.Printf("素数=%d
    ", res)
    	}
    	fmt.Println("main主线程退出")
    }
    

    普通的方法解决案例3

    package main
    import (
    	"time"
    	"fmt"
    )
    func main() {
    	start := time.Now().Unix()
    	for num := 1; num <= 80000; num++ {
    		flag := true //假设是素数
    		// 判断num是否是素数
    		for i := 2; i < num; i++ {
    			if num % i == 0 { //说明该num不是素数
    				flag = false
    				break
    			}
    		}
    		if flag {
    			// 将这个数就放入到primeChan
    			// primeChan<- num
    		}
    	}
    	end := time.Now().Unix()
    	fmt.Println("普通耗时=", end - start)
    }
    

    16.6 channel使用细节和注意事项

    • channel可以声明为只读,或者只写性质

    NidOUK.png

    • channel只读和只写的最佳实践案例

    NidvCD.png

    • 使用select可以解决从管道取数据的阻塞的问题
    package main
    import (
    	"fmt"
    	"time"
    )
    func main() {
    	// 1、定义一个管道10个数据int
    	intChan := make(chan int, 10)
    	for i := 0; i < 10; i++ {
    		intChan<- i
    	}
    	// 2、定义一个管道 5个数据string
    	stringChan := make(chan string, 5)
    	for i := 0; i < 5; i++ {
    		stringChan <- "hello" + fmt.Sprintf("%d", i)
    	}
    
    	// 传统的方法在遍历管道时,如果不关闭会阻塞导致dead lock
    
    	// 在实际开发中,我们不好确定什么时候关闭该管道
    	// 可以使用select方式可以解决
    	for {
    		select {
    //注意,这里如果intChan 一直没有关闭不会一直阻塞而dead lock
    // 会自动到下一个case匹配
    			case v := <-intChan :
    				fmt.Printf("从intChan读取的数据%d
    ", v)
    				time.Sleep(time.Second)
    			case v := <-stringChan :
    				fmt.Printf("从stringChan读取的数据%s
    ", v)
    				time.Sleep(time.Second)
    			default :
    				fmt.Printf("都取不到了")
    			time.Sleep(time.Second)
    			return
    		}
    	}
    }
    
    • gorotine中使用recover,解决协程中出现panic,导致程序崩溃问题
    package main
    import (
    	"fmt"
    	"time"
    )
    // 函数
    func saayhello() {
    	for i := 0; i < 10; i++ {
    		time.Sleep(time.Second)
    		fmt.Println("hello,world")
    	}
    }
    // 函数
    func test() {
    	// 这里我们可以使用defer + recover
    	defer func() {
    		// 捕获test抛出的panic
    		if err := recover(); err != nil {
    			fmt.Println("test协程发生了错误")
    		}
    	}()
    	// 定义一个map
    	var myMap map[int]string
    	myMap[0] = "golang"  //err
    }
    
    func main() {
    	go saayhello()
    	go test()
    	for i :=0; i < 10; i++ {
    		fmt.Println("main() ok=", i)
    		time.Sleep(time.Second)
    	}
    }
    

    说明:如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序的崩溃,这时我们可以在gorotine中使用recover来捕获panic进行处理,这样即使这个协程发生问题,但是主线程仍然不受影响,可以继续执行

  • 相关阅读:
    Sublime Text 命令大全(积累所得)
    端口号
    帝之意志 看透世间
    Session and Cookie的基础用法
    厌胜术
    微信支付
    微信登录
    navicat常用快捷键与SQL基本使用
    idea Spring项目一直报错,但是点击进去就恢复正常了,解决办法,通过mvn idea:module命令生成iml文件
    解决报错WARNING: IPv4 forwarding is disabled. Networking will not work.
  • 原文地址:https://www.cnblogs.com/jiaxiaozia/p/13129305.html
Copyright © 2020-2023  润新知