【Golang第12章:goroutine协程与channel管道】GO语言goroutine协程和channel管道的基本介绍、goroutine协程和channel管道的应用案例
介绍
这个是在B站上看边看视频边做的笔记,这一章是Glang的goroutine协程与channel管道
内容有GO语言goroutine协程和channel管道的基本介绍、goroutine协程和channel管道的应用案例、GO协程和GO主线程、goroutine调度模型、channel的遍历、channel管道阻塞机制
配套视频自己去B站里面搜【go语言】,最高的播放量就是
里面的注释我写的可能不太对,欢迎大佬们指出╰(°▽°)╯
(十二)、goroutine和channel
一、goroutine协程
看一个需求
-
需求:要求统计1-9000000000 的数字中,哪些是素数?
-
分析思路:
- 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
- 使用并发或者并行的方式,将统计素数的任务分配给多个goroutine 去完成,这时就会使用到goroutine.【速度提高4 倍】
1.基本介绍
1)进程和线程介绍
- 进程就是程序程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
- 一个进程可以创建核销毁多个线程,同一个进程中的多个线程可以并发执行。
- 一个程序至少有一个进程,一个进程至少有一个线程
2)程序、进程和线程
3)并发和并行
-
多线程程序在单核上运行,就是并发
-
多线程程序在多核上运行,就是并行
-
示意图:
-
小结
并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
并行: 因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
4)Go 协程和Go 主线程
-
Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。
-
Go 协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由程序员控制
- 协程是轻量级的线程
-
示意图
2.快速入门
1)案例说明
-
请编写一个程序,完成如下功能:
- 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1 秒输出"hello,world"
- 在主线程中也每隔一秒输出"hello,golang", 输出10 次后,退出程序
- 要求主线程和goroutine 同时执行.
- 画出主线程和协程执行流程图
-
代码
package main import ( "fmt" "strconv" "time" ) // 1) 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1 秒输出"hello,world" // 2) 在主线程中也每隔一秒输出"hello,golang", 输出10 次后,退出程序 // 3) 要求主线程和goroutine 同时执行. // 编写一个函数,每隔1秒输出“he1lo,world” func test() { for i := 0; i < 10; i++ { fmt.Println("test函数 hello,world" + strconv.Itoa(i)) //strconv转换为字符串 time.Sleep(time.Second) //休眠1秒 } } func main() { go test() //开启了1个协程 for i := 0; i < 10; i++ { fmt.Println("主函数 hello,world" + strconv.Itoa(i)) //strconv转换为字符串 time.Sleep(time.Second) //休眠1秒 } }
输出的效果说明, main 这个主线程和test 协程同时执行
-
主线程和协程执行流程图
2)快速入门小结
- 主线程是一个物理线程,直接作用在cpu 上的。是重量级的,非常耗费cpu 资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
- Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang 在并发上的优势了
3.goroutine 的调度模型
1)MPG 模式基本介绍
- M: 操作系统的主线程(是物理线程)(Machine)
- P:协程执行需要的上下文(Processor)
- G:协程(Goroutine)
2)MPG 模式运行的状态1
3)MPG 模式运行的状态2
4.设置Golang运行的cpu数
-
介绍:为了充分了利用多cpu 的优势,在Golang 程序中,设置运行的cpu 数目
package main import ( "fmt" "runtime" ) func main() { CpuNum := runtime.NumCPU() //查询电脑有几个CPU fmt.Println("电脑有", CpuNum, "个CPU") //可以自己设置多个CPU runtime.GOMAXPROCS(CpuNum - 1) //减一个CPU运行 }
- go1.8后,默认让程序运行在多个核上,可以不用设置了
- go1.8前,还是要设置一下,可以更高效的利用cpu
二、channel管道
1.引出channel
**需求:**现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map 中。最后显示出来。要求使用goroutine 完成
-
分析思路:
-
使用goroutine 来完成(看看使用gorotine 并发完成会出现什么问题? 然后我们会去解决)
-
在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单,在编译该程序时,增加一个参数
-race
即可 -
代码:
package main import ( "fmt" "time" ) //需求:现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map 中。 //最后显示出来。要求使用goroutine 完成 //思路 //1.编写一个函数,来计算各个数的阶乘,并放到map中 //2.我们启动多个协程,将统计的结果放到map中 //3.map应该做出一个全局的。 var myMap = make(map[int]int, 10) //声明一个全局的map 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 <= 20; i++ { go test(i) } //休眠10秒 time.Sleep(time.Second) //打印map for i, v := range myMap { fmt.Printf("map[%d]=%d\n", i, v) } }
增加一个参数
-race
查看资源竞争数量PS D:\code\Go\src\demo2\09demo\01> go build -race .\main.go PS D:\code\Go\src\demo2\09demo\01> .\main.exe
================== WARNING: DATA RACE Write at 0x00c0000704b0 by goroutine 8: runtime.mapassign_fast64() D:/apps/Go/src/runtime/map_fast64.go:93 +0x0 main.test() D:/code/Go/src/demo2/09demo/01/main.go:26 +0x70 main.main.func1() D:/code/Go/src/demo2/09demo/01/main.go:32 +0x39 Previous write at 0x00c0000704b0 by goroutine 7: runtime.mapassign_fast64() D:/apps/Go/src/runtime/map_fast64.go:93 +0x0 main.test() D:/code/Go/src/demo2/09demo/01/main.go:26 +0x70 main.main.func1() D:/code/Go/src/demo2/09demo/01/main.go:32 +0x39 Goroutine 8 (running) created at: main.main() D:/code/Go/src/demo2/09demo/01/main.go:32 +0x84 Goroutine 7 (finished) created at: main.main() D:/code/Go/src/demo2/09demo/01/main.go:32 +0x84 ================== ================== WARNING: DATA RACE Read at 0x00c000086218 by main goroutine: main.main() D:/code/Go/src/demo2/09demo/01/main.go:39 +0x116 Previous write at 0x00c000086218 by goroutine 25: main.test() D:/code/Go/src/demo2/09demo/01/main.go:26 +0x7c main.main.func1() D:/code/Go/src/demo2/09demo/01/main.go:32 +0x39 Goroutine 25 (finished) created at: main.main() D:/code/Go/src/demo2/09demo/01/main.go:32 +0x84 ================== map[19]=121645100408832000 map[4]=24 map[10]=3628800 map[14]=87178291200 map[16]=20922789888000 map[12]=479001600 map[18]=6402373705728000 map[20]=2432902008176640000 map[3]=6 map[2]=2 map[5]=120 map[9]=362880 map[17]=355687428096000 map[7]=5040 map[8]=40320 map[13]=6227020800 map[15]=1307674368000 map[1]=1 map[6]=720 map[11]=39916800 Found 2 data race(s) # 显示2个资源竞争
-
示意图:
-
1)不同goroutine如何通讯
- 全局变量的互斥锁
- 使用管道channel 来解决
2)使用全局变量加锁
-
因为没有对全局变量m 加锁,因此会出现资源争夺问题,代码会出现错误,提示
concurrent map writes
-
解决方案:加入互斥锁,排队进行写入数据
-
我们的数的阶乘很大,结果会越界,可以将求阶乘改成
sum += uint64(i)
-
代码改进
package main import ( "fmt" "sync" "time" ) //需求:现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map 中。 //最后显示出来。要求使用goroutine 完成 //思路 //1.编写一个函数,来计算各个数的阶乘,并放到map中 //2.我们启动多个协程,将统计的结果放到map中 //3.map应该做出一个全局的。 var ( myMap = make(map[int64]int64, 10) //声明一个全局的map //声明一个全局的互斥锁 //lock 是一个全局的互斥锁 //sync 是包:synchornized lock sync.Mutex ) func test(n int64) { var res int64 = 1 for i := 1; int64(i) <= n; i++ { res *= int64(i) //乘等于 } lock.Lock() //加锁 ,等待一个test(i)函数运行完毕 myMap[n] = res //将res放入到myMap中 lock.Unlock() //解锁 ,函数运行完毕后解锁 } func main() { for i := 1; i <= 20; i++ { //开启多个协程完成 go test(int64(i)) } time.Sleep(time.Second * 5) //休眠5秒,防止主程序过快关闭,导致test()函数没有执行完毕 // lock.Lock() for i, v := range myMap { //打印map fmt.Printf("map[%d]=%d\n", i, v) } // lock.Unlock() }
3)为什么需要channel
- 前面使用全局变量加锁同步来解决
goroutine
的通讯,但不完美 - 主线程在等待所有
goroutine
全部完成的时间很难确定,我们这里设置10 秒,仅仅是估算。 - 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有
goroutine
处于工作状态,这时也会随主线程的退出而销毁 - 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
- 上面种种分析都在呼唤一个新的通讯机制-
channel
2.基本介绍
-
channle 本质就是一个数据结构-队列【示意图】
-
数据是先进先出【FIFO : first in first out】
-
线程安全,多goroutine 访问时,不需要加锁,就是说channel 本身就是线程安全的
-
channel 有类型的,一个string 的channel 只能存放string 类型数据。
-
示意图:
3.定义/声明channel
-
var 变量名 chan 数据类型
-
举例:
var intChan chan int //(intChan 用于存放int 数据) var mapChan chan map[int]string //(mapChan 用于存放map[int]string 类型) var perChan chan Person var perChan2 chan *Person ...
-
说明
channel 是引用类型
channel 必须初始化才能写入数据, 即make 后才能使用
管道是有类型的,intChan 只能写入整数int
-
管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
package main import "fmt" func main() { //演示管道的使用 //1.创建一个可以存放3个int类型的管道 // var intChan chan int // intChan = make(chan int, 3) var intChan chan int = make(chan int, 3) //2.intChan是引用类型,指向了一个地址 fmt.Printf("intChan的值是=%v 地址是=%p\n", intChan, &intChan) //0xc00007c080 //3.向管道写入数据,当我们向管道写入数据时,不能超出管道的容量 intChan <- 10 num := 211 intChan <- num //4.查看管道的长度和cap(容量) fmt.Printf("intChan的长度是=%v 容量是=%v\n", len(intChan), cap(intChan)) //5.从管理读取数据 var num2 int = <-intChan fmt.Println(num2) //取出一个数据 fmt.Printf("intChan的长度是=%v 容量是=%v\n", len(intChan), cap(intChan)) //长度减小,容量不变 //6.在没有使用协程的情况下,如果管道的数据已经全部去取出,再取就会报告 deadlock(死锁) num3 := <-intChan fmt.Println(num3) fmt.Printf("intChan的长度是=%v 容量是=%v\n", len(intChan), cap(intChan)) //长度减小,容量不变 num4 := <-intChan //管道已经空了,继续取会报错 fmt.Println(num4) //fatal error: all goroutines are asleep - deadlock! }
4.使用的注意事项
channel
中只能存放指定的数据类型channel
的数据放满后,就不能再放入了- 如果从
channel
取出数据后,可以继续放入 - 在没有使用协程的情况下,如果
channel
数据取完了,再取,就会报dead lock
- 不要数据可以使用
<-intChan
扔掉数据
5.案例演示
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func main() {
allChan := make(chan interface{}, 3)
allChan <- 10
allChan <- "tom"
cat := Cat{"黑猫警长", 7}
allChan <- cat
//我们希望获得到管道中的第三个元素,则先将前2个推出
<-allChan
<-allChan
newCat := <-allChan //从管道取出的是
fmt.Printf("newCat=%T newCat=%v\n", newCat, newCat) //查看类型和值
//下面的写法是错误的!编译不通过
// fmt.Printf("newCat .Name=%v", newCat.Name)
//因为interface{}空接口可以使用任意类型,导致编译器无法确认数据类型
//需要使用类型断言
a := newCat.(Cat)
fmt.Println(a.Name)
}
6.练习
说明: 请完成如下案例
- 创建一个 Person 结构体[Name,Age,Addressl
- 使用rand方法配合随机创建10个Person 实例,并放入到channel中
- 遍历channel ,将各个Person实例的信息显示在终端…
7.channel遍历和关闭
1)channel 的关闭
使用内置函数close
可以关闭channel
, 当channel
关闭后,就不能再向channel
写数据了,但是仍然可以从该channel
读取数据
案例:
package main
import "fmt"
func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
close(intChan) //关闭管道
//关闭后无法写入数据,运行会报错
//intChan <- 300 //panic: send on closed channel
n1 := <-intChan //关闭后可以正常读取
n2 := <-intChan
fmt.Println(n1, n2)
}
2)channel 的遍历
channel 支持for–range 的方式进行遍历,请注意两个细节
- 在遍历时,如果
channel
管道没有关闭,则回出现deadlock
死锁的错误 - 在遍历时,如果
channel
管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
代码:
package main
import "fmt"
func main() {
//给管道生成数据
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i * 2 //放入10个数据到管道
}
intChan1 := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan1 <- i * 2 //放入10个数据到管道
}
//关闭管道,不然会出现死锁错误
close(intChan)
close(intChan1)
// 使用range遍历
for v := range intChan { //管道没有下标,直接取值
fmt.Println(v)
}
for {
v, ok := <-intChan1
if !ok {
break
}
fmt.Println(v)
}
}
8.应用案例1
请完成goroutine
和channel
协同工作的案例,具体要求:
- 开启一个
writeData
协程,向管道intChan
中写入50个整数 - 开启一个
readData
协程,从管道intChan
中读取writeData
写入的数据 - 注意:
writeData
和readDate
操作的是同一个管道 - 主线程需要等待
writeData
和readDate
协程都完成工作才能退出【管道】
思路
代码
package main
import "fmt"
//写入数据到管道
func writeData(intChan chan int) {
for i := 1; i <= 100; i++ {
fmt.Println("写入数据:", i)
intChan <- i
}
close(intChan) //写入完后关闭管道
}
//读取数据函数
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Printf("readData数据为=%v\n", v)
}
//readData读取完成后
//exitChan <- true
close(exitChan) //写入完后关闭管道
}
func main() {
//创建2个管道
intChan := make(chan int, 50) //创建一个数据管道
extiChan := make(chan bool, 1) //创建一个退出管道
go writeData(intChan)
go readData(intChan, extiChan)
for { //检测
_, ok := <-extiChan //在管道关闭后,ok会被修改为false
if !ok { //判断exitChan管道无法读数据后退出
break
}
}
}
1)练习1
2)练习二
9.应用案例2
-
需求:
要求统计1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有
goroutine
和channel
的知识后,就可以完成了[测试数据: 80000] -
分析思路:
传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。
使用并发/并行的方式,将统计素数的任务分配给多个(4个)
goroutine
去完成,完成任务时间短。 -
示意图
-
代码
package main import "fmt" //向 intchan放入 1-8000个数 func putNum(intChan chan int) { for i := 1; i <= 8000; i++ { intChan <- i } //写入完成后关闭管道 close(intChan) fmt.Println("数据写入完成!!!!!!!!!!!!!!") } //从intChan管道取数据,并计算出素数,放入primeChan管道 func PrimeNum(intChan chan int, primeChan chan int, exitChan chan bool) { for { num, ok := <-intChan if !ok { //判断intChan是否有值 break } judged := true //定义一个判断变量 for i := 2; i < num; i++ { if num%i == 0 { //num % 2到<num = 0 能除没有余数的 表示不是素数 judged = false break //退出for循环 } } if judged { //判断 judged为true,将num放入primeChan primeChan <- num } } fmt.Println("有一个协程找不到数据,退出") exitChan <- true //完成后向退出管道写入数据 } //素数只能被1和自己整除 func main() { intChan := make(chan int, 2000) //存入1-8000所有值 primeChan := make(chan int, 2000) //存入计算后的素数 exitChan := make(chan bool, 4) //开启的线程数,然后判断退出 //开启一个协程,向intChan放入1-8000 go putNum(intChan) //开启4个协程,从intChan取数据,并判断是否为素数,如果事,就放入primeChan for i := 0; i < 4; i++ { go PrimeNum(intChan, primeChan, exitChan) } go func() { //使用匿名函数,开启协程,完成primeChan管道关闭 //主线程处理,现在primeChan管道都没有关闭 for i := 0; i < 4; i++ { //由于管道没有关闭,for循环会一直等待 取到4个数后关闭管道 <-exitChan } close(primeChan) //关闭管道 }() for { // 因为前面管道关闭了,现在可以使用ok检测里面的数据 res, ok := <-primeChan //取出primeChan管道数据 if !ok { //当没有数据后,退出打印 break } fmt.Println("素数结果为:", res) } }
可以使用runtime.NumCPU()
查看cpu逻辑处理器核心数
10.管道阻塞机制
11.channel使用细节和注意事项
-
channel 可以声明为只读,或者只写性质【案例演示】
package main import "fmt" func main() { //管道可以声明为只读或者只写 //1.在默认情况下,管道事双向的 // var chan1 chan int //可读可写 chan1 := make(chan int, 3) chan1 <- 10 num1 := <-chan1 fmt.Println(num1) //2.声明为只写 // var chan2 chan<- int //声明为只写 chan2 := make(chan<- int, 3) chan2 <- 20 // num2 := <-chan2 //错误 fmt.Println(chan2) //3.声明为只读 // var chan2 <-chan int //声明为只读 chan3 := make(<-chan int, 3) // chan3 <- 30 //错误 num3 := <-chan3 fmt.Println(num3) }
-
channel 只读和只写的最佳实践案例
-
使用
select
可以解决从管道取数据的阻塞问题package main import ( "fmt" ) func main() { //使用select可以解决从管道取数据的阻塞问 //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) } //传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock //问题,在实际开发中,可能我们不好确定什么关闭该管道 //可以使用select方式解决 for { judged := true select { case v := <-intChan: //注意,如果intChan一直没有关闭,不会一直阻塞deadlock fmt.Println("从intChan读取了数据", v) case v := <-stringChan: fmt.Println("从stringChan读取了数据", v) default: fmt.Println("都取不到数据了...") judged = false } if !judged { //退出循环 break } } }
-
goroutine
中使用recover
,解决协程中出现panic
,导致程序崩溃问题package main import ( "fmt" "time" ) func sayHello() { for i := 0; i < 10; i++ { time.Sleep(time.Second) fmt.Println("hello,world", i) } } func test() { //定义一个错误的map //使用defer + recover捕获错误,让程序继续执行 defer func() { //使用匿名函数 //捕获test抛出的panic if err := recover(); err != nil { //声明并判断 fmt.Println("test()发生错误", err) } }() //使用匿名函数 //错误的定义 var myMap map[int]string myMap[0] = "golang" } func main() { go sayHello() go test() for i := 0; i < 10; i++ { time.Sleep(time.Second) fmt.Println("mian()等待", i) } }
章节目录
【Golang第1~3章:基础】如何安装golang、第一个GO程序、golang的基础
【Golang第4章:函数】Golang包的引用,return语句、指针、匿名函数、闭包、go函数参数传递方式,golang获取当前时间
【Golang第5章:数组与切片】golang如何使用数组、数组的遍历和、使用细节和内存中的布局;golang如何使用切片,切片在内存中的布局
【Golang第6章:排序和查找】golang怎么排序,golang的顺序查找和二分查找,go语言中顺序查找二分查找介绍和案例
【Golang第7章:map】go语言中map的基本介绍,golang中map的使用案例,go语言中map的增删改查操作,go语言对map的值进行排序
【Golang第8章:面向对象编程】Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态
【Golang第9章:项目练习】go项目练习家庭收支记账软件项目、go项目练习客户管理系统项目
【Golang第10章:文件操作】GO语言的文件管理,go语言读文件和写文件、GO语言拷贝文件、GO语言判断文件是否存在、GO语言Json文件格式和解析
【Golang第12章:goroutine协程与channel管道】GO语言goroutine协程和channel管道的基本介绍、goroutine协