理解Golang并发编程



  • Concurrency vs Parallelism

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

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

    0_1501303195253_f417f87b-9e53-4940-b2a0-70906ea89abf-image.png

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

    Goroutine

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

    • 内存消耗更低只需2kB初始栈空间,而线程初始要1Mb的空间;
    • 由golang的运行时环境创建和销毁,更加廉价,不支持手动管理;
    • 切换效率更高等。

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

    0_1501303217864_ee117940-1bdf-439d-8af5-497a337705d8-image.png

    通过goroutine可以让一个函数和其他的函数并行执行,我们可以轻松地创建成百上千的goroutine,而不会降低程序的执行效率。只需要在函数调用前面加上 go 这一关键字,就可以创建一个goroutine, main 函数本身也是一个goroutine[3]。举个栗子:

    0_1501303270647_3587a315-b8b8-4d84-aa5c-f8662818bebb-image.png

    输出:

    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。

    0_1501303331054_2d5e5b90-b3d7-4563-9f20-df4ef535cc75-image.png

    输出:

    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可以用来限制吞吐量,如下:

    0_1501303411067_bd3e4e02-a0d9-4106-a97f-91532ac3261e-image.png

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

    process
    process
    process
    process
    process
    

    Select

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

    • select选择第一个就绪的channel进行处理
    • 如果有多个就绪的channel,则随机选择一个channel进行处理
    • 如果没有就绪的channel,则等待直到某一channel就绪
    • 如果有 default ,则在3中不会等待,而是立即执行default中的语句

    0_1501303449755_d60fbcc3-e96f-4c48-9da0-d65ee22d2f88-image.png

    两秒钟之后,输出: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


登录后回复
 

与 区块链大学 | qkldx.net 的连接断开,我们正在尝试重连,请耐心等待