更多关于 channel 的扯淡

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 填值有可能比取值少导致阻塞的情况做了一个比较完美的解答。

参考链接

[1]. Go Concurrency Patterns: Pipelines and cancellation

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计