抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

不要通过共享内存来通信,而应该通过通信来共享内存

1-channel

注意:通道 channel 是引用类型!!!

channel 的作用
单纯的并发执行函数的没有意义的。函数间需要交换数据才能体现并发执行的意义。
就像 OS 的并发性和共享性,没有并发谈不上共享,没有共享并发没有意义。
所以 channel 就是用在 goroutine 之间的通信上的。

Communicate through shared memory rather than through shared memory. —– Golang

Golang 提倡通过通信共享内存而不是通过共享内存实现通信

goroutine 是程序并发的执行体,channel 就是它们之间的通信管道。
channel 有时候简写为:chan,遵循 FIFO 先进先出 的规则,保证了收发数据的顺序。
每一个 chan 都是一个具体类型的管道,即声明 channel 时需要为其指定元素类型。

创建 channel

1
2
3
4
var identifier chan T
identifier = make(chan T[, size])

identifier := make(chan T[, size]) // 短声明
  1. chan 的创建需要 make() 分配内存才能使用。单纯的声明时,默认值为 nil
  2. 分配内存时可以指定通道大小,也就是这条管子的容量。
1
2
3
4
5
6
7
8
9
func main() {
var ch chan int // 声明通道变量
fmt.Println(ch) // nil
ch = make(chan int) // 无缓冲区,分配内存
fmt.Println(ch) // 0xc0000d4000

ch2 := make(chan int, 2) // 有缓冲区
fmt.Println(ch2) // 0xc0000d5000
}

对于无缓冲的通道:一次发送、一次接收,都是阻塞的
对于有缓冲的通道:发送->缓冲区满了,才会阻塞;接收->缓冲区空了,才会阻塞。

无缓冲通道就好像快递员送快递,只能当面送给你,你签收之前他就一直阻塞在那里。
有缓冲通道就好像有了快递柜,放到快递柜里等你自己来拿。快递柜满了快递员也只能等着,阻塞在那里。

操作 channel

通道有 发送(通道读入数据)接收(通道写出数据)关闭 三种操作

1
ch := make(chan int)

发送和接收有点容易混淆,可以这么记:左发右收、左写右读。通道在左边:向通道发送;通道在右边:从通道接收

发送

将一个值发送到通道中(通道写入一个值)。

1
ch <- 10    // 把 10 发送到 ch 中

接收

从一个通道中接收值(通道读出一个值)。

1
2
x := <-ch     // 从 ch 中接收值并赋值给变量 x
<- ch // 从 ch 中接收值,忽略结果

关闭

调用内置函数 close() 来关闭通道。

1
close(ch)
  • 已关闭的通道仍然可以获取数据直到通道为空
  • 已关闭的通道数据取完只会接收到零值
  • 已关闭的通道再发送值会导致 panic
  • 关闭已经关闭的通道会导致 panic,最好用 new(sync.Once).Do(func() { close(ch) }) 关闭

关于关闭通道需要注意的事情是:

  • 只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。
  • 通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

判断通道是否被关闭

x, ok := <-ch
从通道中接收值时,会返回两个值:一个是数据,一个是通道开启状态。通道被关闭后 okfalse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)

for {
x, ok := <-ch
if !ok {
break
}
fmt.Println(x)
}
}

// Output:
10
20
30

单向通道

有时候我们会将通道作为参数在多个任务函数之间传递,通常我们在不同的任务函数中使用通道都会对其进行限制,如只能发送或接收。

上面介绍的都是双向通道,接下来我们使用单向通道可以处理这种情况(单向通道也常用于参数)。

  • <-chan T 是一个只读单向通道,只能从通道中读取数据,通道可以执行接收操作但不能执行发送操作。
  • chan<- T 是一个只写单项通道,只能向通道中写入数据,通道可以执行发送操作但不能执行读取操作。
  • 在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来不可以。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func producer(out chan<- int) {    // 生产者,只能向通道写入数据
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}

func consumer(in <-chan int) { // 消费者,只能从通道读取数据
for i := range in {
fmt.Println(i)
}
}

func main() {
ch := make(chan int, 100)
go producer(ch)
go consumer(ch)
}

通道总结

channel 常见异常总结:

关闭已经关闭的channel也会引发panic

worker pool

Worker Pool 是一种模型,其中固定数量的 m 个 worker,通过 worker 队列中的 n 个任务工作。
worker 一直排在队列中,直到 worker 完成其当前任务并提出新任务为止。
在 Golang 中 worker 使用 goroutine 实现,任务使用通道实现。

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
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker:", id, "start job:", j)
time.Sleep(time.Second)
fmt.Println("worker:", id, "end job:", j)
results <- j * 2
}
}

func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)

// 开启3个goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 5个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// close(results)

for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}

select 多路复用

1
2
3
4
5
6
7
8
9
select {
case communication clause:
statements
case communication clause:
statements
...
default: /* 可选 */
statements
}
  • 唯一一个可用的通道会被选择。
  • 如果多个通道可用,会随机公平地选择一个,其他不会执行。
  • 如果没有通道可用,default 情况将被执行。
    • 如果没有 default,select 将会阻塞,直到某个通道可以运行;Golang 不会重新对 channel 或 值进行求值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
ch := make(chan int)

go func() {
ch <- 10
fmt.Println("数据已写入")
}()

select {
case x := <-ch:
fmt.Println("数据已读出:", x)
default:
fmt.Println("default..")
}
}
// ---------------------------------------
// Output:
default..

由于 goroutine 来不及启动完成,select 就执行了,此时 case 不满足,所以运行 default
如果 select 之前睡眠一下,给 goroutine 点时间,就可以运行到 case 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
ch := make(chan int)

go func() {
ch <- 10
fmt.Println("数据已写入")
}()

time.Sleep(500 * time.Millisecond)
select {
case x := <-ch:
fmt.Println("数据已读出:", x)
default:
fmt.Println("default..")
}
}
// ---------------------------------------
// Output:
数据已写入
数据已读出: 10

哔哔