Go语言goroutine、channel(管道)及反射

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的管理
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值