16.1 goroutine(协程)
16.1.1 基本介绍
进程和线程说明
1、进程就是程序在操作系统中的一次执行结果,是系统进行资源分配和调度的基本单位
2、线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
3、一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。
4、一个程序至少有一个进程,一个进程至少有一个线程
16.1.2程序、进程和线程的关系示意图
16.1.3 并发和并行
1、多线程程序在单核上运行,就是并发
2、多线程程序在多核上运行,就是并行
并发:因为是在一个CPU上,比如有10个线程,每个线程执行10毫秒(进行轮训操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
并行:因为是在一个CPU上(比如有10个CPU),比如有10个线程,每个线程执行10毫秒(各自在不同CPU上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也其实只有10个线程在执行,这就是并行
16.1.4 Go协程 和 Go主线程
1、Go主线程(有程序员直接称为线程/也可以理解为进程):一个Go线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程(编译器做优化)。
2、Go协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
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)
}
}
输出的效果说明,main这个主线程和test协程同时执行
主线程和携程执行流程图
入门小结
1、主线程是一个物理线程,直接作用在CPU上,是重量级的,非常耗费CPU资源
2、协程是从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小
3、Golang的协程机制是重要的特点,可以轻松点的开启上万个协程,其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显了Golang在并发上的优势了
16.1.6 gorotine的调度模型
MPG模型基本介绍
M:操作系统的主线程(是物理线程)
P:协程执行需要的上下文
G:协程
16.1.6.1 MPG模式运行—状态1
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
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)
}
}
结果
示意图
16.3.2 不同gorotine之间如何通讯
-
全局变量加锁同步
-
channel
16.3.3 使用全局变量加锁同步改进程序
- 因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map wrotes
- 解决方案:加入互斥锁
- 我们的数的阶乘很大,结果会越界,可以将求阶乘改成sum += unit64(i)
代码改进
16.3.4 channel的介绍
- channel本质就是一个数据结构-队列
- 数据是先进先出
- 线程安全,多gototine访问时,不需要加锁,就是说channel本身就是线程安全的
- channel 是有类型的,一个string的channel只能存放string类型数据
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协程都完成工作才能退出管道。
思路分析图
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
思路图:
代码
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可以声明为只读,或者只写性质
- channel只读和只写的最佳实践案例
- 使用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进行处理,这样即使这个协程发生问题,但是主线程仍然不受影响,可以继续执行