Ezreal 书架 Ezreal 书架
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
  • 逃逸分析

  • 延迟语句

  • 数据容器

  • 通道

    • CSP是什么
    • 通道有哪些应用
      • 1. 停止信号
      • 2. 定时任务
      • 3. 解耦生产方和消费方
      • 4. 控制并发数
    • 通道的底结构
    • 通道的关闭过程发生了什么
    • 从一个关闭的通道里仍然能读出数据吗
    • 如何优雅地关闭通道
    • 关于通道的 happens-before 有哪些
    • 通道在什么情况下会引起资源泄漏
    • 通道操作的情况总结
  • 接口

  • unsafe

  • context

  • Go程序员面试笔试宝典
  • 通道
ezreal_rao
2023-05-05
目录

通道有哪些应用

channel 和 goroutine 的结合是 Go 并发编程的大杀器。而 channel 的实际应用也经常让人眼前一亮, 通过与 select、cancel、timer 等结合,它能实现各种各样的功能。接下来梳理一下 channel 的应用。

# 1. 停止信号

channel 用于停止信号的场景很多,通常是通过关闭某个 channel 或者向 channel 发送一个元素,使得接收 channel 的那一方获知道此信息,进而做一些其他的操作,如停止某个循环等。

# 2. 定时任务

与计时器结合,一般有两种做法:实现超时控制、实现定期执行某个任务。

有时候,需要执行某项操作,但又不想它耗费太长时间,上一个定时器就可以搞定。这就是超时控制:

select {
case <-time.After(100 * time.Millisecond):
case <-s.stopc:
    return false
}
1
2
3
4
5

等待 100 ms 后,如果 s.stopc 还没有读出数据或者被关闭,就直接结束。

这是来自 etcd 源码里的一个例子,这样的写法随处可见。 定时执行某个任务,也比较简单:

func worker() {
    ticker := time.Tick(1 * time.Second)
    for {
        select {
        case <- ticker:
            // 执行定时任务
            fmt.Println("执行 1s 定时任务")
        }
    }
}
1
2
3
4
5
6
7
8
9
10

每隔 1s,执行一次定时任务。

和定时任务相关的两个例子虽然主要依赖于 timer/ticker 的作用,但收到定时消息的途径仍然是 channel。

# 3. 解耦生产方和消费方

服务启动时,启动 N 个 worker, 作为工作协程池,这些协程工作在一个 for {} 无限循环 里,从某个 channel 消费工作任务并执行:

func main() {
    taskCh := make(chan int, 100)
    go worker(taskCh)
    // 阻塞任务
    for i := 0; i < 10; i++ {
        taskCh <- i
    }
    // 等待 1 小时
    select {
    case <-time.After(time.Hour):
    }
}

func worker(taskCh <-chan int) {
    const N = 5
    // 启动 5 个工作协程
    for i := 0; i < N; i++ {
        go func(id int) {
            for {
                task := <- taskCh
                fmt.Printf("finish task: %d by worker %d\n", task, id)
                time.Sleep(time.Second)
            }
        }(i)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

作为消费方的 5 个工作协程不断地从工作队列里取任务,生产方只管往 channel 发送任务即可,解耦了生产方和消费方。

程序输出:

finish task: 1 by worker 4
finish task: 2 by worker 2
finish task: 4 by worker 3
finish task: 3 by worker 1
finish task: 0 by worker 0
finish task: 6 by worker 0
finish task: 8 by worker 3
finish task: 9 by worker 1
finish task: 7 by worker 4
finish task: 5 by worker 2
1
2
3
4
5
6
7
8
9
10

# 4. 控制并发数

有时需要定时执行几百个任务,例如每天定时按城市来执行一些离线计算的任务。但是并发数又不能太高,因为任务执行过程会依赖第三方的一些资源,对请求的速率有限制。这时就可以通过 channel 来控制并发数:

var token = make(chan int, 3)

func main() {
    // …………
    for _, w := range work {
        go func() {
            token <- 1
            w()
            <-token
        }()
    }
    // …………
}
1
2
3
4
5
6
7
8
9
10
11
12
13

构建一个缓冲型的 channel,容量为 3。接着遍历任务列表,每个任务启动一个 goroutine 去完成。真正执行任务、访问第三方的动作在 w () 中完成,在执行 w () 之前,先要从 token 中拿 “许可证”,拿到许可证之后,才能执行 w ()。并且执行完任务后,要将 “许可证” 归还,这样就可以控制同时运行的 goroutine 数目。

这里,token <- 1 放在 func 内部而不是外部,原因是:

如果放在外层,就是控制系统 goroutine 的数量,可能会阻塞 for 循环,影响业务逻辑。而 token 其实和逻辑无关,只是性能调优,放在内层和外层的语义不太一样。

还有一点要注意的是,如果 w () 发生 panic,那 “许可证” 可能就还不回去了,这可以使用 defer 来保证。

#通道#csp#channel
上次更新: 5/9/2023, 10:58:32 AM
CSP是什么
通道的底结构

← CSP是什么 通道的底结构→

最近更新
01
为什么我的MySQL会抖一下
07-15
02
HTTP 性能优化面面观
07-12
03
WebSocket:沙盒里的 TCP
07-12
更多文章>
Theme by Vdoing | Copyright © 2022-2024 Ezreal Rao | CC BY-NC-SA 4.0
豫ICP备2023001810号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式