Go语言基础之并发(channel通信)

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel是一种类型,一种引用类型。

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。

向管道发送数据

ch <- 10 // 把10发送到ch中

从管道接受数据

x := <- ch // 从ch中接收值并赋值给变量x
//<-ch       // 从ch中接收值,忽略结果

关闭管道

close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

妈妈知道我学习累了,允许我玩半小时游戏。半小时之后她通过channel告诉我,你该睡觉了。
女朋友在和我进行了十个来回的对话之后,发现了可能是机器人,她不再回复我了。

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

//定义一个计数器对goroutine进行计数,main函数结束前,保证其它goroutine都已经执行结束
var wg sync.WaitGroup

func chat(who string) {
	defer wg.Done()
	for i := 0; i < 10; i++ { //女朋友聊十句话,发现了可能是机器人,睡觉了
		n := rand.Int31n(5) //生成[0,5)的随机数
		switch n {
		case 0:
			fmt.Println(who, ",我想你了,你在干嘛?")
		case 1:
			fmt.Println(who, ",我想你了,我在和小可爱聊天呀")
		case 2:
			fmt.Println(who, ",你今天有什么有趣的跟我分享吗?")
		case 3:
			fmt.Println(who, ",哇啊,还是我的小可爱机智过人,比心")
		case 4:
			fmt.Println(who, ",亲爱的,你真漂亮")
		default:
			fmt.Println(who, ",I love you!!!")
		}
		time.Sleep(time.Second)
	}
}

func countdown(chWithMa chan bool) {
	defer wg.Done() //告诉计数器,我的任务完成了,我要退出了
	time.Sleep(time.Minute)
	chWithMa <- false
	close(chWithMa)
}

func main() {
	who := "girlFriend"
	chWithMa := make(chan bool)
	wg.Add(2)
	go countdown(chWithMa)
	go chat(who) //聊天机器人去聊天
	//我现在正在玩游戏,我时刻听着母亲的呼喊
	<-chWithMa //这里会阻塞一分钟,这一分钟是我的自由时间
	close(chWithMa)
	wg.Wait()  //等待计数器为0,才终止程序
}

上述启动了两个goroutine,main函数如果不对自己执行流启动的goroutine进行统计,它盲目退出,有可能导致正在工作的goroutine被迫因为进程的退出的停止,为了让所有goroutine在完成工作之前,main函数都不退出,需要使用sync.WaitGroup来实现goroutine的同步。

有缓冲通道和无缓冲通道

无缓冲的通道又称为阻塞的通道。ch := make(chan int)
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。

package main

import (
	"fmt"
)

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // 启用goroutine从通道接收值
	ch <- 10
	fmt.Println("发送成功")
}

有缓冲通道ch := make(chan int,3)指定了通道容量。

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
	ch <- 10
	fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

从通道取值

从通道取值,还可以判断通道是否已经关闭,x := <- ch 可以接受两个值val,ok:=<-ch第二个值是bool类型。

package main

import (
	"fmt"
	"strconv"
)

// channel 练习
func main(){
	chat:=make(chan string)
	go func(){
		//往chat中写入数据
		for i:=0;i<100;i++{
			chat<-strconv.Itoa(i)
		}
		close(chat)
	}()
	for {
		val,ok:=<-chat
		if !ok{
			break
		}else{
			fmt.Println(val)
		}
	}
}

实际上还可以通过for range方式去取值,而不用判断通道是否关闭。

for val:=range chat{
		fmt.Println(val)
	}

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况。

package main

import (
	"fmt"
	"strconv"
)

func send(chat chan<- string){//send函数对于通道chat只写权限,不能从chat取值
	//往chat中写入数据
	for i:=0;i<100;i++{
		chat<-strconv.Itoa(i)
	}
	close(chat)
}
func recv(chat <-chan string){//recv函数对于chat只读权限,不能向chat发送值
	for val:=range chat{
		fmt.Println(val)
	}
}
func main(){
	chat:=make(chan string)
	go send(chat)
	go recv(chat)
}

通道总结
channel常见的异常总结,如下图:
在这里插入图片描述

select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

select{
    case <-ch1:
        ...
    case data := <-ch2:
        ...
    case ch3<-data:
        ...
    default:
        //默认操作
}
package main

//作为新时代的好男人,既要听老婆的话也要孝顺听父母的
import (
	"fmt"
	"strconv"
	"sync"
	"time"
)

var wg sync.WaitGroup

func parent(mather chan<- string) {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		msg1 := "父母都是为你好"
		msg2 := strconv.Itoa(i)
		msg := fmt.Sprintf("%s%s", msg2, msg1)
		mather <- msg
	}
	close(mather)
}

func woman(wife chan<- string) {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		msg1 := "老婆才是陪着你终老的人"
		msg2 := strconv.Itoa(i)
		msg := fmt.Sprintf("%s%s", msg2, msg1)
		wife <- msg
	}
	close(wife)
}
func man(wife, mather <-chan string) {
	defer wg.Done()
	wifeOver := true
	matherOver := true
	for wifeOver || matherOver {
		select {
		case x, ok := <-wife:
			if ok {
				fmt.Println(x)
			} else {
				wifeOver = false
			}
		case y, ok := <-mather:
			if ok {
				fmt.Println(y)
			} else {
				matherOver = false
			}
		default:
			fmt.Println("今天星期天")
		}
		time.Sleep(time.Second)
	}
}

func main() {
	wife := make(chan string)
	mather := make(chan string, 3)
	wg.Add(3)
	go parent(mather)
	go woman(wife)
	go man(wife, mather)
	wg.Wait()
}

使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有case的select{}会一直等待,可用于阻塞main函数。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值