• Go 通过通信来共享内存,而不是共享内存来通信

channel

  1. 单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
  2. 虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
  3. Go语言的并发模型是CSPCommunicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
  4. 如果说goroutineGo程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
  5. Go 语言中的通道(channel)是一种特殊的类型。
    1. 通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
    2. 每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
  6. 通道是类型相关的,一个通道只能传递(发送或接收)一种类型的值,这个类型需要在声明通道时指定。

channel 类型

  1. channel是一种类型,一种引用类型。声明通道类型的格式如下:
1
var identily chan type
1
2
3
var ch1 chan int   // 声明一个传递整型的通道    nil
var ch2 chan bool  // 声明一个传递布尔型的通道  nil
var ch3 chan []int // 声明一个传递int切片的通道 nil

创建 channel

  1. 通道是引用类型,通道类型的空值是nil
1
2
var ch chan in  // 默认值 nil
fmt.Println(ch) // nil
  1. make() 函数创建通道。
1
make(chan type, [缓冲大小])
1
2
3
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
1
2
3
4
// 无缓冲
var channel1 chan int = make(chan int)  // 通道大小默认值为0
channel2 := make(chan int)              // 通道大小默认值为0
channel3 := make(chan int, 1)           // 有缓冲
  1. Go 通道有三种:发送(send)、接收(receive)、同时发送和接收。
    1. chan<-:表示数据进入通道,要把数据写进通道,对于调用者就是发送 send,chan <- int
    2. <-chan:表示数据从通道出来,对于调用者就是得到通道的数据,当然就是接收 recv,<-chan int
1
2
3
receive_only := make(<-chan int)    // 定义接收的channel
send_only := make(chan<- int)       // 定义发送的channel
send_receive := make(chan int)      // 定义同时发送接收

channel 操作

  1. 通道有发送(send)、接收(receive)和关闭(close)三种操作。发送和接收都使用 <- 符号。
  2. 定义通道,无缓冲通道。
1
ch := make(chan int)    // size默认等于0
  1. 发送:将一个值发送到通道中。
1
ch <- 10    // 把10发送到ch中
  1. 接收:从一个通道中接收值。
1
2
x := <- ch  // 从ch中接收值并赋值给变量x
<-ch        // 从ch中接收值,忽略结果
  1. 关闭:通过调用内置的close函数来关闭通道。
1
close(ch)
  1. 关于关闭通道需要注意的事情是:
    1. 只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。
    2. 通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的。
    3. 在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
  2. 关闭后的通道有以下特点:
    1. 对一个关闭的通道再发送值就会导致panic
    2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
    3. 对一个关闭的并且没有值得通道执行接收操作会得到对应类型的零值。
    4. 关闭一个已关闭的通道会导致panic

无缓冲的通道

  1. 无缓冲的通道又称为阻塞的通道。
1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}
  1. 上面这段代码能够通过编译,但是执行的时候会出现以下错误:
fatal error: all goroutines are asleep - deadlock!
所有的goroutine都睡眠 - 死锁

goroutine 1 [chan send]:
main.main()
        D:/True-False/WWW/GoLang/src/xuexi/channel1.go:7 +0x5b
exit status 2
  1. 为什么会出现deadlock错误呢?
    1. 因为我们使用ch := make(chan int)创建的是无缓冲的通道。
    2. 无缓冲的通道只有在有人接收值的时候才能发送值。无缓冲的通道必须有接收才能发送。
  2. 上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?
    1. 一种方法是启用一个goroutine去接收值,例如:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
    ch := make(chan int)
    go recv(ch)
    ch <- 10
    fmt.Println("发送成功")

    // Output:
    // 10
    // 发送成功
}

func recv(c chan int) {
    recv := <- c
    fmt.Println(recv)
}
  1. 无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作。
    1. 这时值才能发送成功,两个goroutine将继续执行。
    2. 相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
  2. 使用无缓冲通道进行通信将导致发送和接收的goroutine同步化,因此,无缓冲通道也被称为同步通道。

有缓冲的通道

  1. 解决上面问题的方法还有一种就是使用有缓冲区的通道。
  2. 我们可以在使用make函数初始化通道的时候为其指定通道的容量。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    fmt.Println("发送成功")
    
    // Output:
    // 发送成功
}
  1. 只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。
  2. 我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。
    1. len(chan) 获取通道内的元素个数。
    2. cap(chan) 获取通道容量。

关闭通道

  1. 可以通过内置的close()函数关闭channel,如果你的管道不往里存值或者取值的时候一定记得关闭管道。
 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
36
37
38
39
package main

import "fmt"

func main() {
    c := make(chan int)

    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }

        close(c)	// 关闭通道
    }()

    for  {
        if data, ok := <- c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }
    
    /*
    for v := range c {
        fmt.Println(v)
    }
    */

    fmt.Println("main结束")
    
    // Output:
    // 0
    // 1
    // 2
    // 3
    // 4
    // main结束
}

从通道循环取值

  1. 当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待,使用close关闭通道,会触发那些在等待中的goroutine
  2. 当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。
  3. 那如何判断一个通道是否被关闭了呢?
 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
36
37
38
39
40
41
42
43
44
45
46
package main

import "fmt"

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        for i := 0; i < 10; i++ {
            ch1 <- i
        }

        close(ch1)
    }()

    go func() {
        for {
            i, ok := <-ch1  // 通道关闭后再取值 ok = false
            if !ok {        // 通道关闭时退出循环
                break
            }

            ch2 <- i * i
        }

        close(ch2)
    }()

    // 打印ch2中值,通道关闭后会退出for range循环
    for i := range ch2 {
        fmt.Println(i)
    }
    
    // Output:
    // 0
    // 1
    // 4
    // 9
    // 16
    // 25
    // 36
    // 49
    // 64
    // 81
}
  1. 从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range的方式。

单向通道

  1. 有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
  2. Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:
 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
36
37
38
39
40
41
42
43
44
45
46
47
package main

import "fmt"

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go counter(ch1)
    go squarer(ch2, ch1)

    printer(ch2)

    // Output:
    // 100
    // 121
    // 144
    // 169
    // 196
    // 225
    // 256
    // 289
    // 324
    // 361
}

func counter(out chan <- int) {
    for i := 10; i < 20; i++ {
        out <- i
    }

    close(out)
}

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

    close(out)
}

func printer(in <- chan int) {
    for i := range in {
        fmt.Println(i)
    }
}
  1. 其中,chan <- int 是一个只能发送的通道,可以发送但是不能接收。<- chan int 是一个只能接收的通道,可以接收但是不能发送。
  2. 在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

总结

  1. 关闭已经关闭的channel也会引发panic
channel nil 非空 空的 满了 没满
接收 panic 接收值 阻塞 接收值 接收值
发送 panic 发送值 发送值 阻塞 发送值
关闭 panic 关闭成功,读完数据后返回零值 关闭成功,返回零值 关闭成功,读完数据后返回零值 关闭成功,读完数据后返回零值值

select

select 多路复用

  1. 某些场景下我们需要同时从多个通道接收数据,通道在接收数据时,如果没有数据可以接收将会发生阻塞。
  2. 你也许会写出如下代码使用遍历的方式来实现:
1
2
3
4
5
6
7
for {
    // 尝试从 ch1 接收值
    data, ok := <-ch1
    // 尝试从 ch2 接收值
    data, ok := <-ch2
    // ...
}
  1. 这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。
  2. select的使用类似于switch语句,它有一系列case分支和一个默认的分支。
    1. 每个case会对应一个通道的通信(接收或发送)过程。
    2. select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。
  3. 具体格式如下:
1
2
3
4
5
6
7
8
select {
    case <- chan1:
        // 如果chan1成功读到数据,则进行该case处理语句
    case chan <- 1:
        // 如果成功向chan2写入数据,则进行该case处理语句
    default:
        // 如果上面都没有成功,则进入default处理流程
}
  1. select 可以同时监听一个或多个channel,直到其中一个channel ready
 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
36
37
38
package main

import (
    "fmt"
)

func test11(ch chan string) {
    ch <- "test1"
}

func test21(ch chan string) {
    ch <- "test2"
}

func main()  {
    // 创建2个通道
    output1 := make(chan string)
    output2 := make(chan string)

    // 跑2个子协程,写数据
    go test11(output1)
    go test21(output2)

    // 由于select只能选择其中一个执行,因此上面两个goroutine至少有一个要阻塞
    // 因此在case里面添读取另外一个goroutine,防止goroutine被一直挂起。
    select {
    case s1 := <-output1: // recv
        <-output2
        fmt.Println("s1=", s1)
    case s2 := <-output2: // recv
        <-output1
        fmt.Println("s2=", s2)
    }


    // Output:
    // s2= test2
}
  1. 如果多个channel同时ready,则随机选择一个执行。
 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
package main

import (
    "fmt"
)

func main() {
    // 创建2个通道
    intChan := make(chan int, 1)
    stringChan := make(chan string, 1)

    go func() {
        intChan <- 1
    }()

    go func() {
        stringChan <- "hello"
    }()

    select {
        case value := <- intChan:
            fmt.Println("int:", value)
        case value := <- stringChan:
            fmt.Println("string:", value)
    }

    fmt.Println("main结束")
    
    // Output:
    // string: hello
    // main结束
}
  1. 可以用于判断通道是否存满。
 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
36
37
38
39
40
41
42
43
44
45
46
package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建通道
    output1 := make(chan string, 10)

    // 字协程写数据
    go write(output1)

    // 取数据
    for s := range output1 {
        fmt.Println("res:", s)
        // 延迟数据读取
        time.Sleep(time.Second)
    }

    // Output:
    // write hello
    // res: hello
    // write hello
    // write hello
    // res: hello
    // write hello
    // res: hello
    // write hello
    // write hello
    // ...
}

func write(ch chan string) {
    for {
        select {
            case ch <- "hello":
                fmt.Println("write hello")
            default:
                fmt.Println("channel full")
        }
        // 延迟 等到数据被取出 不然一直在执行default条件语句
        time.Sleep(500 * time.Millisecond)
    }
}

参考

  1. https://www.topgoer.com