go语言之并发编程

Go语言最大的特色就是并发,它不受CPU核心数的限制,只要你愿意,就可以启动成千上万个Goroutine(例程或协程)。

并发经典案例

如何启动Goroutine ? 使用关键字go + 函数调用

注意main函数在运行时也会产生一个Goroutine,通常叫为main-Goroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main(){
fmt.Println("begin call goroutine")
//启动goroutine
go func() {
fmt.Println("I am a goroutine")
}()
fmt.Println("end call goroutine")

}

​ 这里运行结果显示并没有”I am goroutine”。原因是main函数在运行结束后,不会等待其他Goroutine的结束,一旦main函数的退出,整个进程也会退出。因此Goroutine还没开始就结束了。

​ 解决此问题最简单的办法就是让主函数”睡个觉”,使用time包的Sleep函数可以让主函数睡一下,

1
func Sleep (d Duration)//其中d代表睡眠时长,Duration就是时间长度的枚举。

修改之后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"time"
)

func main(){
fmt.Println("begin call goroutine")
//启动goroutine
go func() {
fmt.Println("I am a goroutine")
}()
time.Sleep(time.Second*1)
fmt.Println("end call goroutine")

}

同步与channel

我们虽然可以用睡眠的方式解决Goroutine运行不成功的情况,但这种方法一方面会浪费资源,另一方面会我们不能总预判每个Goroutine的具体执行时间。

因此,我们就需要协调运行各个Goroutine步调应一致。也就是说通过同步控制,我们可以精准地控制多个Goroutine的运行先后。

这里介绍WaitGroup工具 有三个方法如下:(在官方包sync中可以找到)

1
2
3
func (wg *WaitGroup) Add(delta int)//增加一个计数
func (wg *WaitGroup) Done()//减少一个计数
func (wg *WaitGroup) Wait() //阻塞等待计数变为0

案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"sync"
"time"
)

var w sync.WaitGroup
func main(){
for i:= 0; i<10;i++{
w.Add(1)//添加一个要监控的Goroutine数量
go func(num int) {
time.Sleep(time.Second*time.Duration(num))
fmt.Printf("I am %d Goroutine\n",num)
w.Done()//释放一个
}(i)//通过i确定顺序
}
w.Wait()//如果没有wait将不会有输出,就和上面出现的问题一样
}

每次循环前计数加一,输出一个Goroutine后计数减一,直到循环结束,计数为0,结束等待,此时main-Goroutine结束。

image-20220218105733424

channel的设计目的很简单,就是信息交换,类似于战争时期敌后工作者们的接头行为,两个goroutine 就是要接头的两个人,两个人会阻塞等待(无缓冲区的情况)对方前来接头。至于消息是什么,可以是指令,也可以是有用的消息。对于channel,它类似于unix编程里的管道,在知道如何创建它后,接下来要搞懂它的读写行为。

  • 写行为
  • 通道缓冲区已满(无缓冲区),写阻塞直到缓冲区有空间(或读端有读行为)

  • 通道缓冲区未满,顺利写入,结束

  • 读行为
  • 缓冲区无数据(无缓冲区时写端未写数据),读阻塞直到写端有数据写入

  • 缓冲区有数据,顺利读数据,结束

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"time"
)
var c chan string
func reader() {
msg := <-c //读通道
fmt.Println("I am reader,", msg)
}
func main() {
c = make(chan string)
go reader()
fmt.Println("begin sleep")
time.Sleep(time.Second * 3) //睡眠3s为了看执行效果
c <- "hello" //写通道
time.Sleep(time.Second * 1) //睡眠1s为了看执行效果
}

区分是读还是写,只需看<-在通道左边还是右边即可

实现一个需求:

通过goroutine传递数字的游戏, goroutine1循环将1,2,3,4,5传递给goroutine2,goroutine2负责将数字平方后传递给 goroutine3,goroutine3负责打印接收到的数字。

分析该需求,我们至少需要2个channel,3个goroutine,其中main函数可以直接是第三个goroutine, 所以再创建2个就够了。

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
package main

import (
"fmt"
"time"
)

var c1 chan int
var c2 chan int

func main(){
c1 = make(chan int)
c2 = make(chan int)
go func() {
for i:=0;i<10;i++{
c1 <-i//通道c1写入数据
time.Sleep(time.Second*1)
}
}()
go func() {
for {
num :=<-c1//读c1数据
c2<- num*num//将c1数据写入c2
}
}()
//printer
for {
num := <-c2
fmt.Println(num)
}
}

这样执行完效果不太好,因为当第一个goroutine循环结束后,由于没有goroutine再向通道写数据,这样就会出现错误而导致goroutine在死等。这种错误是可预⻅的,goroutine被锁死了,因此该错误被称 为Deadlock(死锁)。

image-20220218141310345

这是由于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
33
34
35
36
37
38
39
package main

import (
"fmt"
"time"
)

var c1 chan int
var c2 chan int

func main(){
c1 = make(chan int)
c2 = make(chan int)
go func() {
for i:=0;i<10;i++{
c1 <-i//通道c1写入数据
time.Sleep(time.Second*1)
}
close(c1)
}()
go func() {
for {
num ,ok:=<-c1//读c1数据
if !ok {
break
}
c2<- num*num//将c1数据写入c2
}
close(c2)
}()
//printer
for {
num ,ok:= <-c2
if !ok {
break
}
fmt.Println(num)
}
}

输出结果如下:

image-20220218142502218