理解Golang并发编程

kaichao · · 1127 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Concurrency vs Parallelism

并发和并行是彼此相关的两个概念,但不完全等价。在程序中,并发强调的是独立执行的程序的组合;并行强调的是同时执行计算任务[1]。

计算机核心的数量决定了并行计算的能力,人作为“单核”动物(老顽童小龙女除外),可以说自己在并发某些任务,如我在听歌写代码,但不应该认为这两件事在并行执行,参考下图:

Golang的并发模型源于Communicating Sequential Processes (CSP),通过提供goroutine和channel来实现并发编程模式。


Goroutine

Goroutine由Go运行时创建和管理,是Go用于调度CPU资源的“最小单元”,和OS的线程相比更轻量[2]:

  • 内存消耗更低只需2kB初始栈空间,而线程初始要1Mb的空间;

  • 由golang的运行时环境创建和销毁,更加廉价,不支持手动管理;

  • 切换效率更高等。


Goroutine和线程的关系如下图所示:

通过goroutine可以让一个函数和其他的函数并行执行,我们可以轻松地创建成百上千的goroutine,而不会降低程序的执行效率。只需要在函数调用前面加上 go 这一关键字,就可以创建一个goroutine,ni bu ni hai?  

main 函数本身也是一个goroutine[3]。  

举个栗子:

输出:

begin main goroutine

end main goroutine

输出的结果中,并木有 begin hello goroutine,这是因为,通过使用goroutine,我们不需要等待函数调用的返回结果,而会接着执行下面的代码。在 go hello() 后面添加 time.Sleep(1 * time.Second),让go运行时缓个神儿,就会输出了。


Channel

Go提供了一种机制能够让多个goroutine之间进行通信和同步,它就是channel。channel是一种类型,关键字 chan 和channel传输内容的类型共同定义了channel的类型。定义方式为 var c chan string = make(chan string) ,这一channel中传输的类型为字符串。也可以简写为 var c = make(chan string) c := make(chan string)

通过左箭头 <- 操作符操作channel变量:

  • c <- "ping" 表示向channel发送一个值为“ping”的字符串,

  • msg := <- c 表示接收channel中的一个值,并赋給了msg

输出:

reveving: 1

reveving: 2

reveving: 3

   ...

按功能,可以将channel分为只发送或只接收channel,通过修改函数签名的channel形参类型来指定channel的“方向”:  

  • 只允许发送: func ping(c chan<- string)

  • 只允许接收: func print(c <-chan string)

  • 任何对只发送channel的接收操作和只接收channel的发送操作都会产生编译错误

  • 不指定方向的channel被称作“双向”channel,可以将“双向”channel作为参数,传递给接收单向channel的函数,反之,则不行。


Unbuffered channel

非缓冲channel,也就是缓冲池大小为0的channel或者同步channel,上面的栗子中使用的都是非缓冲channel,定义方式为:

  • ch := make(chan int)

  • ch := make(chan int, 0)

接收非缓冲channel中的数据时,如果channel中没有数据则接收方被阻塞,如果channel中有数据则发送方被阻塞,直到channel中数据被接收。  

使用非缓冲channel,可以通过数据交换来保证两个goroutine的状态同步。


Buffered channel

缓冲channel只能容纳固定量的数据,当缓冲池满之后,发送方被阻塞,直到数据被接收释放缓冲池,定义如下:

  • ch := make(chan int, 5)

缓冲channel可以用来限制吞吐量,如下:

上面的栗子中,我们定义了一个容量为5的缓冲channel,每当从请求队列中接收到一个请求时,就发送一个信号给channel,然后用一个新的goroutine去处理请求,当处理完毕,释放(接收一个信号)缓冲池。这样做,就可以限制并发处理的请求数量不超过5个。每隔4秒钟,输出:

process

process

process

process

process


Select

针对于channel,Golang提供了一个功能类似 switch 的关键字 select ,基本原则如下:

  1. select选择第一个就绪的channel进行处理

  2. 如果有多个就绪的channel,则随机选择一个channel进行处理

  3. 如果没有就绪的channel,则等待直到某一channel就绪

  4. 如果有 default ,则在3中不会等待,而是立即执行default中的语句

两秒钟之后,输出:1 在select语句中添加:

default:

fmt.Println("nothing received.")

那么程序执行时,就会立即输出: `nothing received.`


总结

Golang将线程抽象出来成为轻量级的goroutine,开发者不再需要过多地关注OS层面的操作,终于能够从并发编程中解放出来。  

Channel作为通信的媒介,安全高效的实现了goroutine之间的通信和内存共享。  

最后,用Effetive go中的一句话来总结[4]:

Do not communicate by sharing memory; instead, share memory by communicating.


Reference

[1] https://blog.golang.org/concurrency-is-not-parallelism  

[2] http://blog.nindalf.com/how-goroutines-work/  

[3] https://www.golang-book.com/books/intro/10  

[4] https://golang.org/doc/effective_go.html  






有疑问加站长微信联系(非本文作者)

本文来自:微信公众平台

感谢作者:kaichao

查看原文:理解Golang并发编程

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1127 次点击  
加入收藏 微博
1 回复  |  直到 2017-08-11 07:20:51
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传