Go语言圣经 - 第8章 Goroutines 和 Channels - 8.4 Channels

第8章 Goroutines 和 Channels

Go语言中的并发程序可以用两种手段来实现:goroutine 和 channel,其支持顺序通信进程,或被简称为CSP,CSP是一种并发编程模型,在这种并发编程模型中,值会在不同运行实例中传递,第二个手段便是多线程共享内存

8.4 Channels

我们可以把goroutine看成并发体,把channel看成它们之间的通信机制,有了这个,独立的goroutine可以通过它来发送数据,channel根据具体的数据类型不同也不同比如 channel int 和 channel string 是两个发送不同类型数据的channel

使用内置的make 函数可以创建channel

ch = make(chan int)

和map类似,channel也对应一个make创建的底层数据结构的引用。channel 可以与 == 或 nil来比较

一个channel 主要有两个操作,都是通信行为,接收和发送,还有一个是close,除了这些之外的其它操作都会导致panic

ch <- x        //把x发送进通道
x  <- ch       //把接受通道里的值
   <- ch       //把通道里的值丢弃
close(ch)      //关闭通道,但是仍然可以接收到值

channel是有容量的,如果容量大于零就是有缓存的channel

ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

8.4.1 不带缓存的Channels

在使用无缓存channel的情况下,只有发送操作或者接收操作先发生都会导致阻塞。我们来把上一节的程序更改一下

func main() {
	conn, err := net.Dial("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	done := make(chan struct{})
	go func() {
		io.Copy(os.Stdout,conn)
		log.Println("done")
		done <- struct{}{}
	}()
	mustCopy(conn, os.Stdin)
	conn.Close()
	<-done
}

我们在这里的go语句中调用了函数字面量,这是go语言中启用goroutine 常见的方法

主函数的goroutine关闭后,另外一个goroutine可能还在工作,为了让主函数和goroutine都结束,我们采用channel技术,让主函数结束后再接收另一个goroutine作为发送端发送的信息,这样我们就能确保两个goroutine都结束了,这里的信息我们采用了一个空的结构体struct{},当然也可以使用int和bool类型

8.4.2 串联的channels(Pipeline)

channel是可以串联的,我们来写一个程序:先生成数,然后平方,最后打印

package main

import "fmt"

func main() {
	naturals := make(chan int)
	squares := make(chan int)

	go func() {
		for x := 0; ; x++ {
			naturals <- x
		}
	}()

	go func() {
		for {
			x := <-squares
			squares <- x * x
		}
	}()

	for {
		fmt.Println(<-squares)
	}
}

按照期望,程序会输出1,4,9…,但是上述的输出是无限的,而且包含死循环

我们可以使用close(ch)来关闭通道,并且通过多发送一个值至我们不确定是否关闭的通道,这时候会返回bool值,我们可以使用这个机制跳出循环

go func() {
		x, ok := <-naturals
		if !ok {
			squares <- x * x
		}
		close(squares)
	}()

上面处理是很笨拙的,下面我们来限制100个数,然后关闭通道

package main

import "fmt"

func main() {
	naturals := make(chan int)
	squares := make(chan int)

	go func() {
		for x := 0; x < 100; x++ {
			naturals <- x
		}
		close(naturals)
	}()

	go func() {
		for x := range naturals{
			squares <- x * x
		}
		close(squares)
	}()

	for x := range squares {
		fmt.Println(x)
	}
}

其实不需要关闭每一个channel,因为不管有没有被关闭,当它没有引用时,将会被go语言的垃圾自动回收器回收

8.4.3 单方向的channel

随着程序的增长,我们一般更倾向于把大函数拆分为小函数

单方向的channel表示为 chan <- type,如 chan <- int

现在我们把上述程序拆分一下并且使用单通道发送数据,其中夹在中间的函数既要发送又要接收

package main

import "fmt"

func main() {
	naturals := make(chan int)
	squares := make(chan int)

	go counter(naturals)
	go squarer(squares, naturals)
	printer(squares)
}

func counter(out chan<- int) {
	for x := 0; x < 100; x++ {
		out <- x
	}
}

func squarer(out chan<- int, in chan<- int) {
	for v := range in {
		out <- v * v
	}
	close(out)
}

func printer(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
}

函数调用时,会隐式地从chan int转换成 chan <- int

8.4.4 带缓存的Channels

带缓存的channel创建如下,3是缓存大小

ch = make(chan string,3)

如果缓存满则不能发送值,缓存空则不能接收值,缓存中的值消耗遵循队列原则,先进先出,可以使用cap(ch)来获取缓存中有几个值

我们来看一个应用:并发向三个站点发出请求,三个镜像站点分散在不同的地理位置,站点响应后会把响应发送至带缓存的channel,接收者只接收最快的响应(多个goroutines同时并发的向同一个channel发送或者从同一个channel接收数据,都非常常见)

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
}
func request(hostname string) (response string) { /*...*/}

在上述代码中,我们使用了有缓存的channel,但是如果是用了无缓存的channel,另外两个goroutines将会永远卡住,这种goroutines泄漏是BUG,不会被垃圾回收机制回收,因此确保goroutine正常退出是重要的

无缓存和有缓存的channel,甚至channel的缓存大小都要根据实际需求慎重选择

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值