channel 是一个比较神奇的东西,以前很少研究,不过最近的项目有这方面的需求就看了一下。下面主要从 channel 的功能谈起。
channel 的读取和写入问题
channel 的读取写入取决于当前的 channel 的状态,大家应该都知道下面的情况是一定会产生死锁:
ch := make(chan int)
<-ch
但是针对于 close 掉的 channel,则是一定可以读取成功的:
ch := make(chan int)
close(ch)
i:=<-ch //i=0
对于这种情况,golang 添加了一个结果类型判断 i, succ :=<-ch //i=0,succ=false
,若结果为 false,说明该 channel 已经关闭了。但是 close 过的 channel 再写入,是会 panic 的。
有时可能还有一些时间上的要求,比如说判断是否超时。这个时候可以结合上一篇文章中的 select 来解决这个问题:case <- time.After(time.Second*2):
这样就可以设置一个超时时间,解决这个问题。这个问题我们之前有探讨过,select 会判断 channel 的状态是否 ready。超时时,需要取得数据的 channel 应该是未完成的,这个时候就可以进入超时 block。
但是对于在上一篇文章中的另外一种情况,还需要在解决问题时根据具体情况确定具体的解决方案。
利用 channel 实现去异步化
比如一个比较经典的生产者消费者模型:
package main
import ("fmt"
"time"
)
// 生产者
func Producer(id int, item chan int) {
for i:=0; i<10; i++ {
item <- i
fmt.Printf("Producer %d produces data: %d\n", id, i)
time.Sleep(10 * 1e6)
}
}
// 消费者
func Consumer(id int, item chan int) {
for i:=0; i<20; i++ {
c_item := <-item
fmt.Printf("Consumer %d get data: %d\n", id, c_item)
time.Sleep(10 * 1e6)
}
}
func main() {item := make(chan int, 6)
go Producer(1, item)
go Producer(2, item)
go Consumer(1, item)
time.Sleep(5 * time.Second)
}
在写法上,golang 通过 channel 实现了一个忽略异步等待的代码,channel 其实相当于一个队列,当队列中为空时,会阻塞在取队列的操作上,直到可以从中取到内容。而在代码中,生产者和消费者共用了一个 channel item,在生产后放入 item 中,消费者从中读取消费。
执行结果为:
Producer 1 produces data: 0
Producer 2 produces data: 0
Consumer 1 get data: 0
Producer 1 produces data: 1
Producer 2 produces data: 1
Consumer 1 get data: 0
Producer 1 produces data: 2
Consumer 1 get data: 1
Producer 2 produces data: 2
....
再谈 goroutines 的控制
最后一个例子用 golang 自己提供的一个 例子 来探讨下 channel 的使用。
这个例子也在官方博客中进行了探讨(参考链接 1),其中 result 的是用来保存结果的结构体,sumFiles 函数的功能是遍历目录,计算文件的 MD5 hash 值,MD5All 则是 sumFiles 的上层调用函数,对外提供功能,并且封装结果 channel 数据成字典类型。
sumFiles 函数传入参数有两个:一个是 done,这个是用于定义结束,另外一个是 root,指定遍历的路径;传出数据为一个 channel 和 error。sumFiles 会将每个文件放到一个 goroutine 中计算 hash 值,并且将结果保存到 result channel 中。 值得注意的是,在检查是否终止遍历时,使用下面的方法来进行了一个检查:
select {
case <-done: // HL
return errors.New("walk canceled")
default:
return nil
}
根据 MD5All 中的 defer close(done),当函数 MD5All 返回时,会关闭 done 这个 channel,这样从 done 中取值就可以始终成功,select 在执行时就可以一直成功,这也对我之前想法中退出 channel 填值有可能比取值少导致阻塞的情况做了一个比较完美的解答。