协程(goroutine)
基本介绍
- 进程和线程的关系;
- 进程是系统进行资源分配和调度的基本单位
- 一个进程可以有多个线程
- 同一个进程的多个线程并发的执行
- 并发和并行
- 多线程程序在单核上进行,就是并发
- 多线程程序在多核上运行,就是并行
Go协程和Go主线程
- Go主线程(有的程序员直接称为 线程/也可理解成进程):一个Go线程上,可以起多个协程,协程是轻量级的线程【编译器做了优化】
- Go协程的特点:
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
协程快速入门
- 案例:请编写一个程序,完成如下功能
- 在主线程中,开启一个goroutine,该协程每隔1秒输出“hello,world”
- 在主线程中也每隔一秒输出“hello,golang”,输出10次后,退出程序
- 要求主线程和goroutine同时执行
- 画出主线程和协程执行流程图
- 代码和运行结果
- 执行流程图
- 小结:
- 主线程是一个物理线程,直接作用在CPU上的,是重量级的,非常耗费CPU资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小
- Golang的协程机制是重要的特点,可以轻松开启上万个协程。其他编程语言的并发机制一般是基于线程的,开启过多线程,资源耗费大
goroutine 调度模型(MPG)
- M:操作系统的主线程(是物理线程)
- P:协程执行需要的上下文环境,是实现从N:1到N:M映射的关键
- G:协程
runtime包
设置Golang运行的CPU数
- 运用到的runtime包中的方法
- func NumCPU() int:返回本地机器的逻辑CPU个数
- func GOMAXPROCS(n int) int:设置可同时执行的最大CPU数,并返回先前的设置。
- go1.8后,默认让程序运行在多个核上,可以不用设置;之前的版本,可以设置一下,可让CPU利用率更高效
package main
import (
"fmt"
"runtime"
)
func main(){
//获取当前系统CPU的数量
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=", cpuNum)
//可以自己设置使用多少个CPU
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Println("ok")
}
管道(channel)
引入
- 需求:现要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成
- 代码实现与问题发现:
- 上述代码,需求中,存在多个协程向同一块map空间中写数据的操作
- 资源竞争命令:1)go build -race main.go;2)main.exe
解决方法
不同goroutine之间如何通信:
1)全局变量的互斥锁
使用了sync包:sync包提供了基本的同步基元,如互斥锁。大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。
代码改进:
缺点:
- 主线程在等待所有goroutine全部完成时间是很难确定的
- 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
-2) 管道(channel)
- channel的本质就是数据结构—队列
- 线程安全,多goroutine访问时,不需要加锁;
- channel是有类型的,一个string的channel只能存放string类型数据
channel的定义/声明
1. 定义/声明 channel
- 代码示例:
package main
import (
"fmt"
)
func main(){
//演示管道的使用
//创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//看看channel是什么
fmt.Printf("intChan的值=%v intChan本身的地址=%p\n", intChan, &intChan)
//向管道写入数据
intChan<- 10
num := 211
intChan<- num
//看看管道的长度和容量;管道的容量分配内存时已经确定,不会动态增长
fmt.Printf("intChan的长度=%v 容量=%v \n", len(intChan), cap(intChan))
//从管道中取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
//观察管道的长度,容量
fmt.Printf("intChan的长度=%v 容量=%v \n", len(intChan), cap(intChan))
//在没有使用协程的情况下,若管道中的数据已经全部取出,再取就会报告 deadlock
}
- 运行结果:
2. 案例演示
注意
- 上图划红线的地方,编译不通过;
- 纠正代码如下:
channel的遍历与关闭
1. channel关闭:使用内置函数close
package main
import (
"fmt"
)
func main(){
intChan := make(chan int, 3)
intChan<- 10
intChan<- 20
//关闭channel
close(intChan)
//此时不能在写入数据,但是可以读取
num := <- intChan
fmt.Println("num", num)
}
2. channel遍历:使用for-range方式
- 在遍历时,如果channel没有关闭,则会出现deadlock的错误
- 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
package main
import (
"fmt"
)
func main(){
intChan := make(chan int, 3)
intChan<- 10
intChan<- 20
//关闭channel
close(intChan)
//此时不能在写入数据,但是可以读取
num := <- intChan
fmt.Println("num", num)
//遍历管道
intChan2 := make(chan int, 20)
for i := 0; i < 20; i++{
intChan2<- i*2 //放入20个数据到channel中
}
//遍历管道不能使用普通for循环
/*在遍历时,没有关闭管道,会遍历输出,
但会出现fatal error: all goroutines are asleep - deadlock!*/
close(intChan2)
for v := range intChan2{
fmt.Println("v=", v)
}
}
goroutine 和channel结合
思路分析:
代码实现:
package main
import (
"fmt"
)
func WriteData(intChan chan int){
for i := 1; i <= 50; i++{
intChan<- i
fmt.Println("写入数据", i)
}
close(intChan)
}
func ReadData(intChan chan int, exitChan chan bool){
for{
v, ok := <-intChan
if !ok{
break
}
fmt.Println("读取的数据", v)
}
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
}
}
}
代码实现:
package main
import (
"fmt"
)
func putNum(intChan chan int){
for i := 1; i <= 8000; i++{
intChan<-i
}
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool){
for{
num, ok := <-intChan
if !ok{
break
}
var flag = true //假定为素数
//判断num是否为素数
for i := 2; i < num; i++{
if num % i == 0{
flag = false
break
}
}
if flag{
primeChan<- num
}
}
exitChan<- true
fmt.Println("有一个协程已经走完了")
}
//统计1-8000中的素数个数
func main(){
//声明管道
intChan := make(chan int, 1000)
primeChan := make(chan int, 4000) //放素数结果
exitChan := make(chan bool, 4) //标识退出的管道
//开启一个协程putNum,向 intChan中放1-8000个数
start := time.Now().Unix()
go putNum(intChan)
//开启4个协程primeNum,从intChan中取出数据,并判断是否为素数
//如果是,就将其放入primeChan管道中
for i := 0; i < 4; i++{
go primeNum(intChan, primeChan, exitChan)
}
//另开起一个协程,用于判断是否上述4个primeNum协程已经运行完
go func(){
for i := 0; i < 4; i++{
<-exitChan
}
end := time.Now().Unix()
fmt.Println("使用协程耗时=", end - start)
//当从exitChan中取出4个值后,说明primeNum协程已经全部结束
//此时可以关闭primeChan管道
close(primeChan)
}()
//遍历取出结果
for{
res, ok := <-primeChan
if !ok{
break
}
//将结果输出
fmt.Println("是素数的有:", res)
}
}
channel使用注意事项
- channel可以声明为只读,或者只写性质
package main
imoprt (
"fmt"
)
func main(){
//管道可以只声明为只读 或者 只写
var intChan chan int //双向管道,可读可写
var intChan2 chan<- int //只能写
var intChan3 <-chan int //只能读
intChan2 = make(chan int, 2)
intChan3 = make(chan int, 5)
}
- 使用select可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
)
func main(){
//使用select可以解决从管道取数据的阻塞问题
intChan := make(chan int, 10)
for i := 0; i < 10; i++{
intChan<-i
}
stringChan := make(chan string, 5)
for i := 0; i< 5; i++{
stringChan<- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭会阻塞导致 deadlock
//而在实际开发中,存在不好确定什么时候关闭管道
//使用select方式解决上述问题
//label:
for{
select{
case v := <-intChan:
fmt.Printf("从intChan读取的数据%d\n", v)
case v := <-stringChan:
fmt.Printf("从stringChan读取的数据%s\n", v)
default:
fmt.Printf("都取不到数据\n")
return
//break label
}
}
}
- goroutine中使用recover,解决协程中出现panic,导致程序崩溃的问题
- 说明:若起了一个协程,但是协程出现了panic;若没有捕获这个panic,就会造成整个程序崩溃。这时我们可以在goroutine中使用recover来捕获panic进行处理。这样即使这个协程发生问题,但是主线程仍然不受影响,可以继续执行。
package main
import (
"fmt"
"time"
)
func sayHello(){
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()发生错误", err)
}
}()
//定义一个map
var myMap map[int]string
myMap[0] = "golang" //此处发生错误
}
func main(){
go sayHello()
go test()
time.Sleep(time.Second * 20)
}