天天看点

Go并发编程中的那些事[译]本文讲的是Go并发编程中的那些事,

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fun_photo%2F5853737946%2F" target="_blank"></a>

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fun_photo%2F5853737946%2F" target="_blank">bouncing balls</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Thread">1. 多线程执行</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Chan">2. Channels</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Sync">3. 同步</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Dead">4. 死锁</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Race">5. 数据竞争</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Lock">6. 互斥锁</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Race2">7. 检测数据竞争</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Select">8. Select标识符</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Match">9. 最基本的并发实例</a>

<a href="https://juejin.im/post/59ecb058f265da43346f10e5#Parallel">10. 并行计算</a>

线程的并发执行(goroutines)

基本的同步技术(channel和锁)

Go中的基本并发模式

死锁和数据竞争

并行计算

相比于分配栈空间,goroutine 更加轻量,花销更小。栈空间初始化很小,需要通过申请和释放堆空间来扩展内存。Goroutines 内部是被复用在多个操作系统线程上。如果一个goroutine阻塞了一个操作系统线程,比如正在等待输入,此时,这个线程中的其他 goroutine 为了保证继续运行,将会迁移到其他线程中,而你不需要去关心这些细节。

下面的程序将会打印 <code>"Hello from main goroutine"</code>. 是否打印<code>"Hello from another goroutine"</code>,取决于两个goroutines谁先完成.

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2Fgoroutine1.go" target="_blank">goroutine1.go</a>

下一段程序 <code>"Hello from main goroutine"</code> 和 <code>"Hello from another goroutine"</code> 可能会以任何顺序打印。但有一种可能性是第二个goroutine运行的非常慢,以至于到程序结束之前都不会打印。

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2Fgoroutine2.go" target="_blank">goroutine2.go</a>

这有一个更实际的例子,我们定义一个使用并发来推迟事件的函数。

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2Fpublish1.go" target="_blank">publish1.go</a>

你可能用下面的方式调用 <code>Publish</code> 函数

该程序很有可能按以下顺序打印三行,每行输出会间隔五秒钟。

一般来说,我们不可能让线程休眠去等待对方。在下一节中, 我们将会介绍 Go 的一种同步机制, channels 。然后演示如何使用channel来让一个 goruntine 等待另外的 goruntine。

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Ferikjaeger%2F35008017%2F" target="_blank"></a>

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Ferikjaeger%2F35008017%2F" target="_blank">Sushi conveyor belt</a>

寿司输送带

。 <code>&lt;-</code> 标识符表示了channel的传输方向,接收或者发送。如果没有指定方向。那么 channel 就是双向的。

Channels 是一种被 make 分配的引用类型

通过 channel 发送值,可使用 &lt;- 作为二元运算符。通过 channel 接收值,可使用它作为一元运算符。

如果 channel 是无缓冲的,发送者会一直阻塞直到有接收者从中接收值。如果是带缓冲的,只有当值被拷贝到缓冲区且缓冲区已满时,发送者才会阻塞直到有接收者从中接收。接收者会一直阻塞直到 channel 中有值可被接收。

伴有 range 分句的 for 语句会连续读取通过 channel 发送的值,直到 channel 被关闭

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2Fsushi.go" target="_blank">sushi.go</a>

下一个例子中,<code>Publish</code> 函数返回一个channel,它会把发送的文本当做消息广播出去。

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2Fpublish2.go" target="_blank">publish2.go</a>

注意我们使用一个空结构的 channel : <code>struct{}</code>。 这表明该 channel 仅仅用于信号,而不是传递数据。

你可能会这样使用该函数

程序将按给出的顺序打印下列三行信息。在信息发送后,最后一行会立刻出现

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Flasgalletas%2F263909727%2F" target="_blank"></a>

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Flasgalletas%2F263909727%2F" target="_blank">traffic jam</a>

让我们去介绍 <code>Publish</code> 函数中的一个bug。

这时由 <code>Publish</code> 函数开启的 goroutine 打印重要信息然后退出,留下主 goroutine 继续等待。

在某些情况下,程序将不会有任何进展,这种情况被称为死锁。

deadlock 是线程之间相互等待而都不能继续执行的一种情况

在运行时,Go 对于运行时死锁检测具有良好支持。但在某种情况下goroutine无法取得任何进展,这时Go程序会提供一个详细的错误信息. 下面就是我们崩溃程序的日志:

多数情况下下,在 Go 程序中很容易搞清楚是什么导致了死锁。接着就是如何去修复它了。

死锁可能听起来很糟糕, 但是真正给并发编程带来灾难的是数据竞争。它们相当常见,而且难于调试。

一个 数据竞争 发生在当两个线程并发访问相同的变量,同时最少有一个访问是在写.

数据竞争是没有规律的。举个例子,打印数字1,尝试找出它是如何发生的 — 一个可能的解释是在代码之后.

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2Fdatarace.go" target="_blank">datarace.go</a>

两个goroutines, <code>g1</code> 和 <code>g2</code>, 在竞争过程中,我们无法知道他们执行的顺序.下面只是许多可能的结果性的一种.

<code>g1</code> 从<code>n</code>变量中读取值<code>0</code>

<code>g2</code> 从<code>n</code>变量中读取值<code>0</code>

<code>g1</code> 增加它的值从<code>0</code>变为<code>1</code>

<code>g1</code> 把它的值把<code>1</code>赋值给<code>n</code>

<code>g2</code> 增加它的值从<code>0</code>到<code>1</code>

<code>g2</code> 把它的值把<code>1</code>赋值给<code>n</code>

这段程序将会打印n的值,它的值为<code>1</code>

"数据竞争” 的称呼多少有些误导,不仅仅是他的执行顺序无法被设定,而且也无法保证接下来会发生的情况。编译器和硬件时常会为了更好的性能而调整代码的顺序。如果你仔细观察一个正在运行的线程,那么你才可能会看到更多细节。

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fbrandoncwarren%2F2953838847%2F" target="_blank"></a>

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fbrandoncwarren%2F2953838847%2F" target="_blank">mid action</a>

在Go中,处理并发数据访问的首选方式是使用一个 channel,它将数据从一个goroutine传递到另一个goroutine。有一句经典的话:"不要通过共享内存来传递数据;而要通过传递数据来共享内存"。

在这份代码中 channel 充当了双重角色。它作为一个同步点,在不同 goroutine 中传递数据。发送的 goroutine 将会等待其它的 goroutine 去接收数据,而接收的 goroutine 将会等待其他的 goroutine 去发送数据。

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fdzarro72%2F7187334179%2F" target="_blank"></a>

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fdzarro72%2F7187334179%2F" target="_blank">lock</a>

要让这种类型的锁正确工作,所有对于共享数据的操作(包括读和写)必须在一个 goroutine 持有该锁时进行。这一点至关重要,goroutine 的一次错误就足以破坏程序和导致数据竞争。

因此你需要为API去设计一种定制化的数据结构,并且确保所有同步操作都在内部执行。在这个例子中,我们构建了一种安全易用的并发数据结构,<code>AtomicInt</code>,它存储了单个整型,任何goroutines 都能安全的通过 <code>Add</code> 和 <code>Value</code> 方法访问数字。

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2FraceClosure.go" target="_blank">raceClosure.go</a>

对于输出 <code>55555</code> 较为合理的解释是执行 <code>i++</code> 操作的 goroutine 在其他 goroutines 打印之前就已经执行了5次。事实上,更新后的 <code>i</code> 对于其他 goroutines 可见是随机的。

一个非常简单的解决办法是通过使用本地变量作为参数的方式去启动另外的goroutine。

这段代码是正确的,他打印了期望的结果,<code>24031</code>。回想一下,在不同 goroutines 中,程序的执行顺序是乱序的。

我们仍然可以使用闭包去避免数据竞争。但是我们需要注意在每个 goroutine 中需要有不同的变量。

这个工具使用下来非常简单: 仅仅增加 <code>-race</code> 到 <code>go</code> 命令后。运行上述程序将会自动检查并且打印出下面的输出信息。

这个工具发现在程序20行存在数据竞争,一个goroutine向某个变量写值,而22行存在另外一个 goroutine 在不同步的读取这个变量的值。

注意这个工具只能找到实际执行时发生的数据竞争。

这有一个例子,显示了如何用 select 去随机生成数字.

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2FrandBits.go" target="_blank">randBits.go</a>

更简单,这里 select 被用于设置超时。这段代码只能打印 news 或者 time-out 消息,这取决于两个接收语句中谁可以执行.

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fjulia_manzerova%2F4617019027%2F" target="_blank"></a>

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fjulia_manzerova%2F4617019027%2F" target="_blank">couples</a>

多花点时间仔细理解这个例子。当你完全理解它,你将会彻底的理解 Go 内部的并发工作机制。

程序演示了单个 channel 同时发送和接受多个 goroutines 的数据。它也展示了 select 语句如何从多个通信操作中选择执行。

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2Fmatching.go" target="_blank">matching.go</a>

实例输出:

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fsomegeekintn%2F4819945812%2F%2F" target="_blank"></a>

<a href="https://link.juejin.im/?target=http%3A%2F%2Fwww.flickr.com%2Fphotos%2Fsomegeekintn%2F4819945812%2F%2F" target="_blank">CPUs</a>

具有并发特性应用会将一个大的计算划分为小的计算单元,每个计算单元都会单独的工作。

多 CPU 上的分布式计算不仅仅是一门科学,更是一门艺术。

每个计算单元执行时间大约在100us至1ms之间.如果这些单元太小,那么分配问题和管理子模块的开销可能会增大。如果这些单元太大,整个的计算体系可能会被一个小的耗时操作阻塞。很多因素都会影响计算速度,比如调度,程序终端,内存布局(注意工作单元的个数和 CPU 的个数无关)。

尽量减少数据共享的量。并发写入是非常消耗性能的,特别是多个 goroutines 在不同CPU上执行时。共享数据读操作对性能影响不是很大。

数据的合理组织是一种高效的方式。如果数据保存在缓存中,数据的加载和存储的速度将会大大加快。再次强调,这对写操作来说是非常重要的。

下面的例子将会显示如何将多个耗时计算分配到多个可用的 CPU 上。这就是我们想要优化的代码。

这个想法很简单:识别适合大小的工作单元,然后在单独的 goroutine 中运行每个工作单元. 这就是 <code>Convolve</code> 的并发版本.

<a href="https://link.juejin.im/?target=https%3A%2F%2Fwww.nada.kth.se%2F~snilsson%2Fconcurrency%2Fsrc%2Fconvolution.go" target="_blank">convolution.go</a>

当定义好计算单元,通常最好将调度留给程序执行和操作系统。然而,在 Go1.*版本中,你需要指定 goroutines 的个数。

<a href="https://link.juejin.im/?target=https%3A%2F%2Fplus.google.com%2F%2BStefanNilsson%2Fabout%3Frel%3Dauthor" target="_blank">Stefan Nilsson</a>

原文发布时间为:2017年10月22日

本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。

继续阅读