Goroutine和channel

Golang 实现并发

对于 Goroutine 和 channel 我是这么理解的:
Goroutine 就是用来实现并发的,而通过 channel 实现协程的调度

匿名函数

匿名函数和 Python,Java 里面的匿名函数差不多,都是没有名字的函数,随写随用
理解起来也很简单,用法如下:

1
2
3
func() {
fmt.Println("hello world")
}()

感觉在前面闭包里面碰到过,就是把函数赋值一个值,匿名函数也可以这样
把一个匿名函数赋值给了 a, 然后就可以使用a来调用匿名函数了

1
2
3
4
a := func() {
fmt.Println("hello world, 1")
}
a()

匿名函数和普通函数的区别只是没有名字,所以它也可以带返回值

1
2
3
4
5
6
a := func(str string) string {
fmt.Println(str)
return "fuck me"
}
x := a("hello world")
fmt.Println(x)

匿名函数也可以带参数

1
2
3
4
5
6
7
8
b := func(str string) {
fmt.Println(str)
}
b("hello world, 3")

func(str string) {
fmt.Println(str)
}("hello world, 4")

带参数的有两种情况
一种是如果匿名函数会被赋值给一个值,那么它结束后就不能带括号
另一种就是匿名函数不会被赋值给一个值,那么它结束就要带一个括号,里面放参数

Goroutine

Go 使用 go 关键字来创建 goroutine,我觉得 goroutine 虽然叫协程,但它的实际上就是一种轻量级的线程

1
go 函数名(参数)

使用示例:

1
2
3
4
5
6
7
8
9
10
11
func say(word string) {
for i := 0; i < 10; i++ {
fmt.Println(word)
time.Sleep(100 * time.Millisecond)
}
}

func main() {
go say("Hello")
say("World")
}

使用 goroutine 不难理解,在函数调用前面加一个 go 关键字就可以了
难理解的是和 channel 一起用

channel

信道要和 goroutine 一起使用才能发挥它的最大作用,所以单独讲信道很难懂,要结合 goroutine 一起来理解
信道就是一个管道,和 Linux 里面的管道很像,都是数据从一头放,从另一头出
信道使用 <- 符号来发送或者接收

1
2
ch <- v    // 将 v 发送至信道 ch
v := <-ch // 从 ch 接收值并赋予 v

创建信道要使用 make

1
ch := make(chan int)

带缓冲的信道,就是在 make 的时候有第二个参数,那就是它的缓冲区大小

1
ch := make(chan int, 100)

发送者可以使用 close 来关闭一个信道,表示没有数据要传送来

1
close(c)

接收者可以通过接受到的第二个参数来判断信道是否被关闭

1
v, ok := <-ch

select 语句可以让一个 goroutine 等待多个通信操作
select 会一直阻塞,直到某个信道传来数据为止,这时就会执行那个分支
当多个分支都准备好时会随机选择一个执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tick := time.Tick(100 * time.Millisecond) // tick每100毫秒输出一次
boom := time.After(500 * time.Millisecond) // boom每500毫秒输出一次
for {
select {
case <-tick: // tick有数据时匹配到这个分支
fmt.Println("tick.")
case <-boom: // boom有数据时匹配到这个分支
fmt.Println("BOOM!")
return
default:
fmt.Println(" .") // 没有分支匹配到时,默认走这条分支
time.Sleep(50 * time.Millisecond)
}
}

当 select 中的其它分支都没有准备好时, default 分支就会执行,就跟 switch 里面的 default 一样

goroutine 和 channel 一起使用

前面的 goroutine 和 channel 概念都很飘,尤其是 channel,刚开始学的时候我都有点搞不懂这玩意有什么用
但是当我理解了把 goroutine 和 channel 结合起来使用后,我恍然大悟,直呼太强了

为什么要它们两个一起用?
这是为了方便实行用户层面的进程调度

当用 go 关键字创建 goroutine 时,会把这个 goroutine 放入就绪区,然后系统在就绪区内选择进程来运行(这不就是操作系统里面的进程调度吗?)
但是如果我想要某个进程先执行,产生一个结果后再调用另外一个进程该怎么办?
在这里使用 channel 阻塞那个需要等待数据的进程就可以了
channel 有一个输入口和一个输出口,当某个信道需要数据输入,但还没有数据输入时,它会进入阻塞状态,直到有数据传来
反过来也是如此,如果一个信道需要输出数据,但输出端还没有准备好,它也会进入阻塞状态,直到可以输出数据

可以理解为利用 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
func addNum(num int, ch chan int) {
ch <- num
}

func main() {
arr := []int{1, 2, 3, 4, 5, 6, 7, 8}
channel := make(chan int)
go addNum(arr[0], channel)
go addNum(arr[1], channel)
go addNum(arr[2], channel)
go addNum(arr[3], channel)
go addNum(arr[4], channel)
go addNum(arr[5], channel)
go addNum(arr[6], channel)
go addNum(arr[7], channel)
a := <-channel
b := <-channel
c := <-channel
d := <-channel
e := <-channel
f := <-channel
g := <-channel
h := <-channel
fmt.Printf("a=%v\n", a)
fmt.Printf("b=%v\n", b)
fmt.Printf("c=%v\n", c)
fmt.Printf("d=%v\n", d)
fmt.Printf("e=%v\n", e)
fmt.Printf("f=%v\n", f)
fmt.Printf("g=%v\n", g)
fmt.Printf("h=%v\n", h)
}

先用 go 创建了 8 个 goroutine,他们都是调用 addNum 函数
但是 addNum 函数需要信道输出,所以直到 go addNum(arr[7], channel) 执行,都没有信道来接受数据
于是他们就都被阻塞了
直到执行到 a := <-channel 从缓冲区中随机激活了一个进程,于是某个数字就被赋给了 a 变量
就这样不断的激活进程, a~h 这 8 个变量都随机拿到了 1~8 数字中的一个

注意:每一次的输出结果都是不一样的

所以通过使用信道,可以在用户层面实现进程的调度
当然也不是说 channel 只能用来阻塞/激活进程,他还可以实现进程之间的数据传输,上面的例子就是向进程中传输了数据


学了一天的成果果然还是有的 (ᕑᗢᓫ∗)˒
channel 的用法可算是搞懂了,理解之后才感觉到它的强大
开心,感觉搞定一了 Go 语言学习的一大障碍 |•’-‘•) ✧