1. goroutine(协程)
- Go主线程(有程序直接称为线程):一个Go线程可以起多个协程,协程是轻量级的线程
- 协程特点:1)有独立的栈空间;2)共享程序堆空间;3)调度由用户控制;4)协程是轻量级的线程。
- 引入背景:1)主线程是一个物理线程,直接作用在CPU上的,是重量级的,非常消耗CPU资源;2)协程从主线程开启,是轻量级的线程,是逻辑态。对资源消耗相当小;3)golang可轻松开启上万个协程,其它语言的并发机制一般基于线程的,开启过多的线程,资源耗费大,凸显出golang在并发上的优势了。
2. goroutine的调度模型——MPG模式
- M:操作系统的主线程(是物理线程)
- P:协程执行需要的上下文
- G: 协程
3.使用goroutine进行并发出现的问题及解决方法
- 场景:计算1-200各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。
//计算1-200各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。
var myMap = make(map[int]int, 10)
//lock是一个全局的互斥锁
var lock sync.Mutex
//test函数用于计算n!,将这个结果放入到myMap
func test(n int){
res := 1
for i := 1; i <= n; i++{
res *= i
}
lock.Lock()
//将res放入到myMap
myMap[n] = res
lock.Unlock()
}
func main(){
//开启多个协程
for i := 1; i <= 20; i++{
go test(i)
}
time.Sleep(time.Second*10)
//这里我们输出结果,变量这个结果
lock.Lock()
for i, v := range myMap{
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()
}
- 问题:1) 启动的goroutine向map写入数据时,会存在资源竞争,报错:fatal error: concurrent map writes;2)主线程退出时间与协程完成时间不确定,出现协程未完成,但主线程结束从而终止了协程地工作
- 解决方法:1)全局变量加锁同步;2)使用管道channel
4.goroutine不适用的场景
- goroutine不适用于并发任务隔离数据比较高的场景,因为goroutine为逻辑态,共享栈空间,部分数据共享。这时应该使用进程,进程的数据在内核空间,不可见,数据隔离性较高,所以引出需要进程间通信
5. 为什么需要channel
全局变量加锁同步解决上述问题存在不完美之处如下:
- 主线程在等待所有goroutine全部完成的时间很难确定
- 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
- 通过全部变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
6. channel管道介绍
- channel本质就是一个数据结构-队列
- 数据是先进先出
- 线程安全,多goroutine访问时,不需要加锁,不存在资源竞争问题
- channel是有类型的,一个string的channel只能存放string类型数据
- channel是引用类型,必须初始化后才能写入数据,即make后才能使用
- 在操作系统(linux)中,管道的实现本质是一系列的文件描述符
// var 变量名 chan 数据类型
var intChan chan int
var mapChan can map[int]string
// 演示管道的使用
func main() {
//1.创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2.看看intChan是什么
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan) //intChan 的值=0xc000110000 intChan本身的地址=0xc000006028
//3.向管道写入数据
intChan <- 3
num := 211
intChan <- num
//注意向管道存入的数据长度不能超过3
intChan <- 6
//intChan <- 12 //fatal error: all goroutines are asleep - deadlock!
//4.看看管道的长度和cap(容量)
fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan))
//5.从管道中取数据
var num2 int
num2 = <- intChan
fmt.Println("num2=", num2)
//6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报deadlock
num3 := <- intChan
num4 := <- intChan
num5 := <- intChan //fatal error: all goroutines are asleep - deadlock!
fmt.Println(num3, num4, num5)
}
7.channel的注意事项
- channel中只能存放指定的数据类型
- channel的数据放满后,就不能再放入了
- 如果从channel取出数据后,可以继续放入
- 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
8.channel的关闭与遍历
- 使用内置函数close关闭channel,当channel关闭后,就不能再向channel写数据了,但仍然可以从该channel读取数据
- 遍历:支持for-range的方式进行遍历,注意两个细节:1)遍历时,如果channel没有关闭,则会出现deadlock的错误;2)遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
intChan <- 300
close(intChan) //这时不能够再写入数到channel
//intChan <- 400 //panic: send on closed channel
//遍历管道
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++{
intChan2 <- i*2
}
//遍历管道不能使用普通的for循环
//遍历时,一定要关闭管道,否则出现dead lock
close(intChan2)
for v := range intChan2{
fmt.Println("v=", v)
}
}
9.案例
完成goroutine和channel协同工作的案例,具体要求:
- 开启一个writeData协程,向管道intChan中写入50个整数
- 开启一个readData协程,从管道intChan中读取writeData写入数据
- writeData和readData操作的是同一个管道
- 主线程需要等待writeData和readData协程都完成工作才能退出
//write Data
func writeData(intChan chan int){
for i := 1; i <= 50; i++{
//放入数据
intChan <- i
fmt.Printf("writeData 写数据=%v\n", i)
}
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)
}
//readData 读取完数据后,即任务完成
exitChan <- true
close(exitChan)
}
func main(){
//创建两个管道
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
}
}
}
10. channel的阻塞机制
- 写快读慢不会引起阻塞,即读、写频率不一致对阻塞没影响
- 如果编译器(运行),发现一个管道只有写,而没有读,则该管道会阻塞
11.channel细节
- 管道可以声明为只读或只写,在默认情况下,管道是双向
- 使用select可以解决从管道取数据的阻塞问题
func main(){
//使用select可以解决从管道取数据的阻塞问题
//定义10个数据的管道
intChan := make(chan int, 10)
for i := 0; i < 10; i++{
intChan <- i
}
//定义5个数据的管道
stringChan := make(chan string, 5)
for i := 0; i < 5; i++{
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭会阻塞而导致deadlock
//问题,在实际开发中,不好确定什么时候关闭该管道
//使用select方式解决
//label:
for {
select {
//注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
//会自动到下一个case匹配
case v := <- intChan:
fmt.Printf("从intChan读取的数据%d\n", v)
case v := <- stringChan:
fmt.Printf("从stringChan读取的数据%s\n", v)
default:
fmt.Printf("都取不到了,程序员可以加入自己逻辑\n")
return
//break
}
}
}
- goroutine中使用recover,解决协程中出现panic导致程序崩溃问题
实例:统计1-200000的数字中,哪些为素数(开启4个协程)
func putData(intChan chan int) {
for i := 1; i <= 8000; i++ {
intChan <- i
}
close(intChan)
}
func judgePrime(primeChan chan int, exitChan chan bool, intChan chan int) {
for {
v, ok := <-intChan
if !ok {
exitChan <- true
break
}
flag := true
for i := 2; i < v; i++ {
if v%i == 0 {
//说明该number不是素数
flag = false
break
}
}
if flag {
//为素数,将数据放入管道中
primeChan <- v
}
}
fmt.Println("有一个协程因为取不到数据退出")
exitChan <- true
}
func main() {
intChan := make(chan int, 8000)
primeChan := make(chan int, 8000)
exitChan := make(chan bool, 4)
go putData(intChan)
for i := 0; i < 4; i++ {
go judgePrime(primeChan, exitChan, intChan)
}
go func(){
for i := 0; i < 4; i++ {
<-exitChan
}
close(primeChan)
}()
for v := range primeChan{
fmt.Printf("素数的结果=%v\n,", v)
}
}
12.反射基本介绍
- 反射可以在运行时获取变量的各种信息,比如变量的类型(type)、类别(kind)
- 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
- 通过反射,可以修改变量的值,可以调用关联的方法
13.反射注意事项
- type是类型,kind是类别,它们可能相同,也可能是不同的(在反射中type表示一个接口,kind表示不同的数据类型)
var num int = 10 //num的type和int都是int
var stu Student //stu的type是pkg1.Student,stu的kind是struct
*使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int,那么就应该使用reflect.Value(x).Int(),而不能使用其它的,否则报panic
- 通过反射来修改变量时,注意当使用SetXxx方法来设置需要通过对应的指针类型来完成,这样才能改变传入变量的值,同时需要使用到reflect.Value.Elem()方法
//通过反射,修改num int的值
//修改student的值
func reflect01(b interface{}){
//1.获取到reflect.Value
rVal := reflect.ValueOf(b)
//看看rVal的kind
//fmt.Printf("rVal kind=%v\n", rVal.Kind()) //rVal kind=ptr
//2. rVal
rVal.Elem().SetInt(20) //Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装
}
func main(){
var num int = 10
reflect01(&num)
fmt.Println("num=", num) //20
}
14.反射的应用场景
- 结构体序列化、反序列化中的tag标签
- 使用反射操作任意结构体类型
- 使用反射创建并操作结构体,或修改变量的值
15.go语言常用并发模型
- 使用无缓冲channel实现并发控制
- 通过sync包中的waitgroup实现并发控制,waitgroup会等待它所收集的所有goroutine任务全部完成
- Context上下文,go将一个程序的运行环境、现场和快照封装在一个Context里,再将它传给要执行的goroutine。context包用来处理多个goroutine之间共享数据,及多个goroutine的管理