【Go学习笔记】第十四章 Go 通道

前言:下面的内容都是边看【飞雪无情】大佬的博客,自己边整理的,其中部分内容有过删改,推荐大家去看原作者的博客进行学习本博客内容仅作为自己的学习笔记。在此之前,我跟着b站韩茹老师刷完了Go语言入门教程。

学习链接:https://www.flysnow.org/archives/
参考书籍:《Go语言实战》

十四、Go 通道

(补充:Go语言的并发同步模型来自一个叫做通信顺序进程(Communicating Sequential Processes, CSP)的范型(paradigm)。**CSP是一种消息传递模型,通过在goroutine之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。**用于在goroutine之间同步和传递数据的关键数据类型叫做通道(channel)。对于没有使用过通道写并发程序的程序员来说,通道会让他们感觉神奇而兴奋。使用通道可以使编写并发程序更容易,也能让并发程序出错更少。)

上一篇我们讲的原子函数和互斥锁,都可以保证共享数据的读写,但是呢,它们还是有点复杂,而且影响性能,对此,Go又为我们提供了一种工具,这就是通道。

所以在多个goroutine并发中,我们不仅可以通过原子函数和互斥锁保证对共享资源的安全访问,消除竞争的状态,还可以通过使用通道,在多个goroutine发送和接受共享的数据,达到数据同步的目的。

通道,他有点像在两个routine之间架设的管道,一个goroutine可以往这个管道里塞数据,另外一个可以从这个管道里取数据,有点类似于我们说的队列。

声明一个通道很简单,我们使用chan关键字即可,除此之外,还要指定通道中发送和接收数据的类型,这样我们才能知道,要发送什么类型的数据给通道,也知道从这个通道里可以接收到什么类型的数据。

ch := make(chan int)

通道类型和Map这些类型一样,可以使用内置的make函数声明初始化,这里我们初始化了一个chan int类型的通道,所以我们只能往这个通道里发送int类型的数据,当然接收也只能是int类型的数据。

我们知道,通道是用于在goroutine之间通信的,它具有发送和接收两个操作,而且这两个操作的运算符都是<-

ch <- 2 	//发送数值2给这个通道
x := <-ch 	//从通道里读取值,并把读取的值赋值给x变量
<-ch 		//从通道里读取值,然后忽略

看例子,慢慢理解发送和接收的用法。发送操作<-在通道的后面,看箭头方向,表示把数值2发送到通道ch里;接收操作<-在通道的前面,而且是一个一元操作符,看箭头方向,表示从通道ch里读取数据。读取的数据可以赋值给一个变量,也可以忽略。

通道我们还可以使用内置的close函数关闭。

close(ch)

如果一个通道被关闭了,我们就不能往这个通道里发送数据了,如果发送的话,会引起painc异常。但是,我们还可以接收通道里的数据,如果通道里没有数据的话,接收的数据是零值

刚刚我们使用make函数初始化的时候,只有一个参数,其实make还可以有第二个参数,用于指定通道的大小。默认没有第二个参数的时候,通道的大小为0,这种通道也被成为无缓冲通道

ch := make(chan int)
ch := make(chan int, 0)	
ch := make(chan int, 2)	// (补充:第二个参数指定通道的大小,默认为0)

看例子,其中第一个和第二个初始化是等价的。第三个初始化创建了一个大小为2的通道,这种称为有缓冲通道

1. 无缓冲的通道

无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送goroutine和接收goroutine同时准备好,才可以完成发送和接收操作。

从上面无缓冲的通道定义来看,发送goroutine和接收goroutine必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道

func main() {
	ch := make(chan int)

	go func() {
		var sum int = 0
		for i := 0; i < 10; i++ {
			sum += i
		}
		ch <- sum
	}()
	
	fmt.Println(<-ch)
}

在前面的例子中,我们为了演示goroutine,防止程序提前终止,都是使用sync.WaitGroup进行等待,现在的这个例子就不用了,我们使用同步通道来等待。

在计算sum和的goroutine没有执行完,把值赋给ch通道之前,fmt.Println(<-ch)会一直等待,所以main主goroutine就不会终止,只有当计算和的goroutine完成后,并且发送到ch通道的操作准备好后,同时<-ch就会接收计算好的值,然后打印出来。

image-20211119142743043

2. 管道

我们在使用Bash的时候,有个管道操作|,它的意思是把上一个操作的输出,当成下一个操作的输入,连起来,做一连串的处理操作。

➜  ~ ls |grep 'D'  
Desktop
Documents
Downloads

比如上面这个例子的意思是,先使用ls命令,把当前目录下的目录和文件列出来,作为下一个grep命令的输入,然后通过grep命令,匹配我们需要显示的目录和文件,这里匹配以D开头的文件名或者目录名。

其实我们使用通道也可以做到管道的效果,我们只需要把一个通道的输出,当成下一个通道的输入即可。

func main() {
	one := make(chan int)
	two := make(chan int)

	go func() {
		one <- 100
	}()

	go func() {
		v := <-one
		two <- v
	}()

	fmt.Println(<-two)
}

这里例子中我们定义两个通道onetwo,然后按照顺序,先把100发送给通道one,然后用另外一个goroutine从one接收值,再发送给通道two,最终在主goroutine里等着接收打印two通道里的值,这就类似于一个管道的操作,把通道one的输出,当成通道two的输入,类似于接力赛一样。

3. 有缓冲的通道

有缓冲通道,其实是一个队列,这个队列的最大容量就是我们使用make函数创建通道时,通过第二个参数指定的。

ch := make(chan int, 3)

这里创建容量为3的,有缓冲的通道。对于有缓冲的通道,向其发送操作就是向队列的尾部插入元素,接收操作则是从队列的头部删除元素,并返回这个刚刚删除的元素。

当队列满的时候,发送操作会阻塞;当队列空的时候,接受操作会阻塞。有缓冲的通道,不要求发送和接收操作时同步的,相反可以解耦发送和接收操作。

(补充:梳理一下,有缓冲的通道和无缓冲的通道之间有一个很大的不同:无缓冲的通道保证进行发送和接收的goroutine会在同一时间进行数据交换;有缓冲的通道没有这种保证。

image-20211119143101717)

想知道通道的容量以及里面有几个元素数据怎么办?其实和map一样,使用caplen函数就可以了。

cap(ch)
len(ch)

cap函数返回通道的最大容量,len函数返回现在通道里有几个元素。

func mirroredQuery() string {
    responses := make(chan string, 3)
    go func() { responses <- request("asia.gopl.io") }()
    go func() { responses <- request("europe.gopl.io") }()
    go func() { responses <- request("americas.gopl.io") }()
    return <-responses // return the quickest response
}
func request(hostname string) (response string) { /* ... */ }

这是Go语言圣经里比较有意义的一个例子,例子是想获取服务端的一个数据,不过这个数据在三个镜像站点上都存在,这三个镜像分散在不同的地理位置,而我们的目的又是想最快的获取到数据。

所以这里,我们定义了一个容量为3的通道responses,然后同时发起3个并发goroutine向这三个镜像获取数据,获取到的数据发送到通道responses中,最后我们使用return <-responses返回获取到的第一个数据,也就是最快返回的那个镜像的数据。

(补充:再来看一个示例

// 这个示例程序展示如何使用有缓冲的通道和固定数目的goroutine来处理一堆工作
package main

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

const (
	numberGoroutines = 4		// 要使用的goroutine的数量
    taskLoad		= 10	// 要处理的工作的数量
)

var wg sync.WaitGroup		// wg用来等待程序完成

// init初始化包,Go语言运行时会在其他代码执行之前,优先执行这个函数
func init() {
    // 初始化随机数种子
    rand.Seed(time.Now().Unix())
}

// main是所有Go程序的入口
func main() {
    // 创建一个有缓冲的通道来管理工作
    tasks := make(chan string, taskLoad)
    
    // 启动goroutine来处理工作
    wg.Add(numberGoroutines)
    for gr := 1; gr <= numberGoroutines; gr++ {
        go worker(tasks, gr)
    }
    
    // 增加一组要完成的工作
    for post := 1; post <= taskLoad; post++ {
        tasks <- fmt.Sprintf("Task: %d", post)
    }
    
    // 当所有工作都处理完时关闭通道,以便所有goroutine退出
    close(tasks)
    
    // 等待所有工作完成
    wg.Wait()
}

// worker作为goroutine启动来处理从有缓冲通道传入的工作
func worker(tasks chan string, worker int) {
 	// 通知函数已经返回
    defer wg.Done()
    for {
        // 等待分配工作
        task, ok := <-tasks
        if !ok {
            // 这意味着通道已经空了,并且已被关闭
            fmt.Printf("Worker: %d : Shutting Down\n", worker)
            return
        }
        // 显示我们开始工作了
        fmt.Printf("Worker: %d : Started %s\n", worker, task)
        // 随机等一段时间来模拟工作
        sleep := rand.Int63n(100)
        time.Sleep(time.Duration(sleep) * time.Millisecond)
        // 显示我们完成了工作
        fmt.Printf("Worker: %d : Completed %s\n", worker, task)
    }
}
// 运行结果:
Worker: 4 : Started Task: 3
Worker: 2 : Started Task: 1
Worker: 1 : Started Task: 2
Worker: 3 : Started Task: 4
Worker: 2 : Completed Task: 1
Worker: 2 : Started Task: 5
Worker: 3 : Completed Task: 4
Worker: 3 : Started Task: 6
Worker: 4 : Completed Task: 3
Worker: 4 : Started Task: 7

4. 单向通道

有时候,我们有一些特殊场景,比如限制一个通道只可以接收,但是不能发送;有时候限制一个通道只能发送,但是不能接收,这种通道我们称为单向通道。

定义单向通道也很简单,只需要在定义的时候,带上<-即可。

var send chan<- int //只能发送
var receive <-chan int //只能接收

注意<-操作符的位置,在后面是只能发送,对应发送操作;在前面是只能接收,对应接收操作。

单向通道应用于函数或者方法的参数比较多,比如

func counter(out chan<- int) {
}

例子这样的,只能进行发送操作,防止误操作,使用了接收操作,如果使用了接收操作,在编译的时候就会报错的。

使用通道可以很简单的在goroutine之间共享数据,下一篇会具体介绍一些例子,以便更好的理解并发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ClimberCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值