【Go简明手册】Go并发编程
知识点
- 并发与并行
- 协程
- goroutine
- channel
- select
并发编程
并发与并行
并发指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,通过 CPU 时间片轮转使多个进程快速交替的执行。而并行的关键是你有同时处理多个任务的能力。并发和并行都可以是很多个线程,就看这些线程能不能同时被(多个)CPU 执行,如果可以就说明是并行,而并发是多个线程被(一个)CPU 轮流切换着执行。一个经典且通俗易懂的例子这样解释并发与并行的区别:并发是两个队列,使用一台咖啡机;并行是两个队列,使用两台咖啡机。如果串行,一个队列使用一台咖啡机,那么哪怕前面那个人有事出去了半天,后面的人也只能等着他回来才能去接咖啡,这效率无疑是最低的。图解:
协程
*
协程也叫轻量级线程。与传统的进程和线程相比,协程最大的优点就在于其足够“轻”,操作系统可以轻松创建上百万个协程而不会导致系统资源枯竭,而线程和进程通常最多不过近万个。而多数语言在语法层面上是不支持协程的,一般都是通过库的方式进行支持,但库的支持方式和功能不够完善,经常会引发阻塞等一系列问题,而 Go 语言在语法层面上支持协程,也叫
goroutine
。这让协程变得非常简单,让轻量级线程的切换管理不再依赖于系统的进程和线程,也不依赖 CPU 的数量。
goroutine*
goroutine
是 Go 语言并行设计的核心。goroutine
是一种比线程更轻量的实现,十几个 goroutine
可能在底层就是几个线程。 不同的是,Golang 在 runtime、系统调用等多方面对 goroutine
调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine
的 CPU (P) 转让出去,让其他 goroutine
能被调度并执行,也就是 Golang 从语言层面支持了协程。要使用 goroutine
只需要简单的在需要执行的函数前添加 go
关键字即可。当执行 goroutine
时候,Go 语言立即返回,接着执行剩余的代码,goroutine
不阻塞主线程。下面我们通过一小段代码来讲解 go
的使用:
1 |
|
Go 的并发执行就是这么简单,当在一个函数前加上 go
关键字,该函数就会在一个新的 goroutine 中并发执行,当该函数执行完毕时,这个新的 goroutine 也就结束了。不过需要注意的是,如果该函数具有返回值,那么返回值会被丢弃。所以什么时候用 go
还需要酌情考虑。
接着我们通过一个案例来体验一下 Go 的并发到底是怎么样的。新建源文件 goroutine.go
,输入以下代码:
1 |
|
执行 goroutine.go
文件会发现屏幕上什么都没有,但程序并不会报错,这是什么原因呢?原来当主程序执行到 for 循环时启动了 10 个 goroutine
,然后主程序就退出了,而启动的 10 个 goroutine
还没来得及执行 Add()
函数,所以程序不会有任何输出。也就是说主 goroutine
并不会等待其他 goroutine
执行结束。那么如何解决这个问题呢?Go 语言提供的信道(channel
)就是专门解决并发通信问题的
channel
channel
是goroutine
之间互相通讯的东西。类似我们 Unix 上的管道(可以在进程间传递消息),用来 goroutine
之间发消息和接收消息。其实,就是在做 goroutine
之间的内存共享。channel
是类型相关的,也就是说一个 channel
只能传递一种类型的值,这个类型需要在 channel
声明时指定。
声明与初始化
channel
的一般声明形式:var chanName chan ElementType。
与普通变量的声明不同的是在类型前面加了 channel
关键字,ElementType
则指定了这个 channel
所能传递的元素类型。示例:
1 |
|
初始化一个 channel
也非常简单,直接使用 Go 语言内置的 make()
函数,示例:
1 |
|
channel
最频繁的操作就是写入和读取,这两个操作也非常简单,示例:
1 |
|
select
select
用于处理异步 IO 问题,它的语法与 switch
非常类似。由 select
开始一个新的选择块,每个选择条件由 case
语句来描述,并且每个 case
语句里必须是一个 channel
操作。它既可以用于 channel
的数据接收,也可以用于 channel
的数据发送。如果 select
的多个分支都满足条件,则会随机的选取其中一个满足条件的分支。
新建源文件 channel.go
,输入以下代码:
1 |
|
以上代码先初始化两个 channel
c1 和 c2,然后开启两个 goroutine
分别往 c1 和 c2 写入数据,再通过 select
监听两个 channel
,从中读取数据并输出。
运行结果如下:
1 |
|
超时机制
通过前面的内容我们了解到,channel
的读写操作非常简单,只需要通过 <-
操作符即可实现,但是 channel
的使用不当却会带来大麻烦。我们先来看之前的一段代码:
1 |
|
观察上面三行代码,第 2 行往 channel
内写入了数据,第 3 行从 channel
中读取了数据,如果程序运行正常当然不会出什么问题,可如果第二行数据写入失败,或者 channel
中没有数据,那么第 3 行代码会因为永远无法从 a
中读取到数据而一直处于阻塞状态。相反的,如果 channel
中的数据一直没有被读取,那么写入操作也会一直处于阻塞状态。如果不正确处理这个情况,很可能会导致整个 goroutine
锁死,这就是超时问题。Go 语言没有针对超时提供专门的处理机制,但是我们却可以利用 select
来巧妙地实现超时处理机制,下面看一个示例:
1 |
|
这样的方法就可以让程序在等待 1 秒后继续执行,而不会因为 ch 读取等待而导致程序停滞,从而巧妙地实现了超时处理机制,这种方法不仅简单,在实际项目开发中也是非常实用的。
channel 的关闭
channel
的关闭非常简单,使用 Go 语言内置的 close()
函数即可关闭 channel
,示例:
1 |
|
关闭了 channel
后如何查看 channel
是否关闭成功了呢?很简单,我们可以在读取 channel
时采用多重返回值的方式,示例:
1 |
|
通过查看第二个返回值的 bool
值即可判断 channel
是否关闭,若为 false
则表示 channel
被关闭,反之则没有关闭。