07协程、通道

基本概念

并发和并行

并发和并行都是指处理机处理多个任务的情况。对于多核心CPU的情况而言,并发指的是多个程序交替占用其中一个核心或多个核心去执行任务,并不一定非要占用多核心去执行任务。但是对于并行而言,程序一定是在CPU多个核心上同时运行。

所以,并行是并发的一个子概念。

协程goroutines

协程是比操作系统线程更加轻量的一个概念。写成可以运行在一个或多个线程之内。go语言会自动管理用户创建的的协程,自动释放资源等。

go中,通过go关键字加上一个函数名*(可以是匿名函数)*去创建一个协程。

管道channel

管道是协程之间交换数据的一个类型化的消息队列。在管道上的写操作和读操作已提前实现了互斥,并不需要显示的去给管道加锁*(在访问共享数据时,也不应该去用加锁的方式。这会严重降低性能。)*

可以创建任意类型的管道,包括空接口类型。方法是ch:=make(chan dataType)

协程

创建协程

使用go关键字可以去创建一个协程。但是需要注意的是,创建的协程会随着main函数的结束而结束,所以假如创建携程后main函数就执行结束了,可能所有协程都得不到运行。

为了解决这个问题,在学习时可以让main函数sleep一段时间再结束运行。例如:

func main(){
	for i:=0;i<5;i++{//创建五个并发的协程
		go print(i)//创建协程
	}
	time.Sleep(1*time.Second)//让main函数睡一秒
}
func print(n int){
    for i:=0;i<20;i++{//每个协程会输出20个(编号,数字0-19)的组合
		fmt.Print("(",n,i,")")
	}
	fmt.Println()//这只能代表有一次换行,并不能说是输出20个数据后换行。
}
/*
输出:
(0 0)(0 1)(1 0)(1 1)(1 2)(2 0)(2 1)(2 2)(2 3)(2 4)(2 5)(2 6)(2 7)(2 8)(2 9)(2 10)(2 11)(2 12)(2 13)(2 14)(2 15)(2 16)(2 17)(2 18)(2 19)
(0 2)(0 3)(0 4)(0 5)(0 6)(0 7)(0 8)(0 9)(0 10)(0 11)(0 12)(0 13)(0 14)(0 15)(0 16)(0 17)(0 18)(0 19)
(3 0)(3 1)(3 2)(3 3)(3 4)(3 5)(3 6)(3 7)(3 8)(3 9)(3 10)(1 3)(1 4)(1 5)(1 6)(1 7)(1 8)(1 9)(1 10)(1 11)(1 12)(1 13)(1 14)(1 15)(1 16)(1 17)(1 18)(1 19)
(4 0)(3 11)(3 12)(3 13)(3 14)(3 15)(3 16)(3 17)(3 18)(3 19)
(4 1)(4 2)(4 3)(4 4)(4 5)(4 6)(4 7)(4 8)(4 9)(4 10)(4 11)(4 12)(4 13)(4 14)(4 15)(4 16)(4 17)(4 18)(4 19)
*/

可以看出,协程执行的顺序和创建的顺序并不一定相同。同时,一个携程也并不是从头到尾一次性执行完毕,他可能在函数体内部任意一条语句后就转去执行别的协程。因此,创建的五个协程内的5个fmt.Println()并不是每20个输出后换行,在最极端的情况下,可能出现输出连续输出100个元素后换行5次的情况。

但在实际开发中,肯定不会使用sleep的方法让协程完成运行。应该使用sync包下的WaitGroup结构体中的Add() Done() Wait()三个函数去解决问题。

通道

创建

和切片、map一样,通道也需要使用make创建。所以通道有下面两种创建方法:

var ch1 chan int//先声明
ch1=make(chan int)//再分配空间

ch2:=make(chan int)//声明时分配

发送和接收数据

在向通道发送和接收数据时,都要用到<-操作符,该操作符的箭头指向为数据的流向。

当发送数据时,使用msg:=<-ch来将通道中的数据发送到msg中。数据一旦从通道中取出,那么通道中就不会再存在这个数据了。向通道发送msg数据的方法为 ch<-msg。例子:

func main(){
	ch1:=make(chan int)
	go print(ch1)//创建输出数据的协程,将通道作为参数传入
	go send(ch1)//创建产生数据的协程
	time.Sleep(1*time.Second)
}
func send(ch chan int){
	for i:=0;i<10;i++{
		ch<-i//向通道中传入1-9
	}
}
func print(ch chan int){
	for i:=0;i<10;i++{
		fmt.Print(<-ch," ")//从通道中将数据取出
	}
}
/*
输出:
0 1 2 3 4 5 6 7 8 9 
*/

但是需要注意的是,上面创建的通道都是缓冲为0的通道。这个意思是,如果有一个协程发送数据到通道时没有被其他协程立即接收,那么这个数据将会丢失。所以做法是make通道时增加他的缓冲。如:make(chan int,1)即创建一个缓冲为1的通道。下面这个程序将会出现死锁:

func main(){
	ch1:=make(chan int)
	ch1<-999//缓冲为0,发送到ch1之后,这个数据就会丢失。
	go print(ch1)//这个协程等不到数据传入,所以会发生死锁。
	time.Sleep(1*time.Second)
}
func print(ch chan int){
	fmt.Println(<-ch)
}
/*
报错:
all goroutines are asleep - deadlock!
*/

如果将上述程序第二行的ch1:=make(chan int)改为ch1:=make(chan int,1)那么程序就会正常执行,输出999。

迭代和关闭通道

关闭通道

go中,使用close(ch)来关闭一个通道,释放资源。但即使是对一个已关闭的通道进行接收操作,也不会报错,这时就需要用到<-ch的第二个参数了。msg,ok:=<-ch才是一个通道正确的读取方法,如果通道已经被关闭,那么ok将会是false,否则为true。例子:

func main(){
	ch1:=make(chan int)
	go print(ch1)
	go send(ch1)
	time.Sleep(1*time.Second)
}
func send(ch chan int){
	for i:=0;i<5;i++{
		ch<-i
	}
	close(ch)//传入五个数据后就关闭通道
}
func print(ch chan int){
	for i:=0;i<20;i++{//连续读取二十个数据
		v,ok:=<-ch
		fmt.Print("(",v,ok,")")
	}
}
/*
输出:
(0 true)(1 true)(2 true)(3 true)(4 true)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)(0 false)
*/

可以看出只有前五个数据是正确的。

迭代读取通道

如果使用for-range模式读取通道,则会自动检测通道是否被关闭。例如将上述例子的print函数替换为下面,即可自动检测通道是否被关闭了:

func print(ch chan int){
	for i:=range ch{
		fmt.Print(i," ")
	}
}
/*
输出:
0 1 2 3 4 
*/

select

select的结构类似于switch-case结构,只不过他的case是通道。select语句选择一个已经准备好了的通道去执行,或是一直处于阻塞状态。例子:

func main(){
	ch1:=make(chan string)
	ch2:=make(chan string)
	go choose(ch1,ch2)
	rand.Seed(time.Now().UnixNano())//设置随机种子
	for i:=0;i<5;i++{
		c:=rand.Intn(2)//随机执行func1或func2
		if c==0{
			go func1(ch1)
		}else{
			go func2(ch2)
		}
	}
	time.Sleep(1*time.Second)
}
func func1(ch chan string){
	ch<-"11111111"
}
func func2(ch chan string){
	ch<-"22222222"
}
func choose(ch1,ch2 chan string){
	for{
		select { //哪个通道准备好了,就执行哪个通道。
		case v:=<-ch1:
			fmt.Println(v)
		case v:=<-ch2:
			fmt.Println(v)
		}
	}
}
/*
输出:
22222222
11111111
22222222
11111111
22222299
*/

上述程序一定会输出五条记录,因为不管是哪个通道准备好了数据,都会进行输出操作。如果choose函数写成下面这种,最极端的情况下一条数据都不会输出:

func choose(ch1,ch2 chan string){
	for{
			fmt.Println(<-ch1)
			fmt.Println(<-ch2)
	}
}

222
11111111
22222299
*/


上述程序一定会输出五条记录,因为不管是哪个通道准备好了数据,都会进行输出操作。如果`choose`函数写成下面这种,最极端的情况下一条数据都不会输出:

~~~go
func choose(ch1,ch2 chan string){
	for{
			fmt.Println(<-ch1)
			fmt.Println(<-ch2)
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值