Advertisement
  1. Code
  2. Go

与Go同行:Golang的并发,第一篇

by
Read Time:6 minsLanguages:
This post is part of a series called Let's Go: Golang Concurrency.
Let's Go: Golang Concurrency, Part 2

Chinese (Simplified) (中文(简体)) translation by Qiang Ji (you can also view the original English article)

概述

每一种编程语言都有一些使其成功的杀手级的功能。 Go语言的杀手级功能就是并发编程。 它的设计基于一个强大的理论模型(CSP),Go语言以“go”关键字的形式提供语言层的语法支持来启动一个异步任务(是的,该语言以此关键字命名)以及在并发任务之间进行通信的一个内建方式。

在本文(第一篇)中,我将介绍用Go语言并发实现的CSP模型,goroutines,以及如何同步多个协作goroutine的操作。 在将来的文章(第二篇)中,我将介绍Go的管道(channels)以及如何在没有同步数据结构的情况下协调goroutine。

CSP

CS代表通信顺序处理(Communicating Sequential Processes)。 它由 Tony (C. A. R.) Hoare于1978年首次提出。 CSP是一个描述并发系统的高级框架。 当在CSP抽象层操作时,编写正确的并发程序比在典型的线程和线程锁的抽象层编写更容易。

Goroutines

Goroutines是协同程序(coroutines)的一种运用。 但它们不完全相同。 一个goroutine是一个在由启动线程生成的单独线程上执行的函数,因此启动线程不会阻碍这个单独的线程。 多个goroutine可以共享相同的操作系统线程。 与协同程序(coroutines)不同,goroutine不能直接控制另一个goroutine。 当一个特定的goroutine在读写访问时产生阻塞,Go的runtime负责隐式传输控制。

让我们看一些代码。 下面的Go程序定义了一个名叫“f”的函数,它会在任意0到半秒间隔后休眠然后打印出它的参数。 main()函数在一个有4个迭代的循环中调用f()函数,在每个迭代中它调用f()函数三次并每行打印出1,2 和3。 正如你所期望的,输出是:

main函数在一个类似的循环中调用f()函数作为一个goroutine。 现在结果不同了,因为Go的runtime将同时运行f的goroutines,然后由于goroutine之间的随机休眠时间是不同的,所以值的打印顺序与f()被调用时的顺序不同。 这里是输出结果:

此程序使用time和math/rand标准库包实现随机休眠并在程序最后等待所有的goroutines完成处理。 这很重要,因为当主线程退出时,程序就结束了,即使有未完成的goroutines仍在运行。

Sync Group

当你有许多goroutine在各处运行,你经常想知道什么时候它们都完成了。

有很多不同的方式可以实现它,但是最优的方式之一是使用一个WaitGroup。 一个WaitGroup是一个在sync包中被定义的类型,此包提供Add()Done()Wait()操作函数。 它就像一个计数器,它记录有多少go routines仍在运行并且等着直到它们都完成处理。 无论什么时候你启动一个新的goroutine,你调用Add(1)(如果你启动多个goroutines,你可以不止一次地调用它)。 当一个goroutine完成时,它调用Done()函数,此函数把计数器值减少一个,并且Wait()函数阻止程序运行直到计数器值为零。

我们把之前的程序转换为在程序最后使用WaitGroup以替代休眠六秒。 注意f()函数使用defer wg.Done()代替直接调用wg.Done()。 这对确保wg.Done()始终被调用很有用,即使在程序发生问题并且goroutine提前终止的情况下。 否者,计数器永远也不会变为零,并且wg.Wait()会永远阻止程序运行。

另一个小技巧是在调用f()三次之前只调用wg.Add(3)一次。 注意,即使调用f()作为常规函数,我也调用wg.Add()。 这是必要的,因为f()调用wg.Done(),不管它作为函数还是goroutine运行。

同步数据结构

goroutines在1,2,3程序中相互之间没有通信或在共享的数据结构中操作。 在现实世界中,互相通信和共享数据操作通常是必要的。 sync包提供Mutex类型与Lock()Unlock()函数,它们提供互斥功能。 一个很好的例子就是标准的Go map数据结构。

此数据结构本身并没有被设计成同步的。 这意味着如果多个goroutines在没有外部同步机制的帮助下同时访问相同的map数据,结果将是不可预测的。 但是,如果所有goroutines同意在每个访问之前去获取一个共享的互斥体并且在访问之后释放它,那么这些访问将会按顺序发生。

综上所述

综上所述。 著名的Tour of Go有一个编写网页爬虫的练习。 他们提供了一个很好的带模拟提取器的框架,这能让你专注于手上的问题。 我强烈建议你试着自己去解决这个问题。

我用两种方法写了一个完整的解决方案:使用同步的map和使用管道。 完整的源代码在这里

这里是sync解决方案相关的部分。 首先,让我们定义一个带互斥struct的map用来保存提取的URL。 注意,一个匿名类型被创建的有趣语法,在一个语句中同时实现对一个变量的初始化和赋值。

现在,代码能够在访问URL map之前对m互斥体加锁并且在访问完成时解锁。

这并不完全的安全因为任何其他人能访问fetchedUrls并同时忘记对它加锁和解锁。 一个更牢靠的设计会提供一个数据结构,此数据结构通过自动地加锁/解锁来支持安全的操作。

结论

Go对使用轻量级的goroutines实现的并发超级地支持。 这比使用传统的线程更容易。 当你需要同步访问共享的数据,Go用sync.Mutex向你提供支持。

关于Go的并发还有很多可以说。 敬请关注...

关注我们的公众号
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.