Go 并发编程
选择 Go
编程的原因可能是看中它简单且强大,那么你其实可以选择C语言;除此之外,我看中 Go
的地方还有原生支持并发编程,对于开发网络编程有着一定的优势,实际上很多地方也谈到,Go
目前是作为云编程的最为流行的编程语言。Go
从语法层面支持并发编程,这可能是其他语言不多见的地方。其实,无所谓孰优孰劣,关键是你如何应用。
在谈并发编程之前,似乎需要知道什么是并发编程,为什么要并发编程? 并发程序指立即可以对多个任务进行的程序,注意这里是立即而非同时,同时处理多个任务,通常叫并行。如何理解立即呢?假定A、B、C三个人在快餐店排队点餐准备吃饭,如果快餐店接到A的订单,告诉A受到订单,并不理会B,C,而是等A的订单完成后,对A结账处理,再依次服务B和C。这种模式叫顺序式或者说独占式。服务窗口仅会对当前用户服务,而实际上准备订单是后厨的人,但服务窗口只能等待后厨准备后之后,再服务客人,服务窗口有很多时间等在等待,并没有做任何事情,这样的效率非常低下。假如,服务窗口接受A订单后,告知A,你的订单已经接受,你可以去旁边取餐口等待一下,准备好快餐会告知你领取,那么服务窗口就可以接受B和C的订单,依次往前,而不用让后面的客人长时间排队等待,而不知道任何事情。这里的立即,就是这样的含义,一个一个处理告知,可能并不能立即提供结果,但有结果一定会告知。同样,还有另外一种情况,就是食堂情况,当A,B,C同时排队等待,服务窗口,立即开启3个,同时对A,B,C进行服务,互不相关,这样情况,叫做并行。但并行一定会快吗?有时候也不一定?因为无论开设多少窗口,如果后厨只有一名厨师,那么客户等待时间其实并没有减少,反而需要服务人员,那么增加后厨厨师不就可以了吗?如果后厨有多名厨师,但多名厨师需要共享某一个设备时,其实效率也不会,从系统来说,总会有一些资源处于非独立的共享状态,只要涉及资源共享,就必然存在竞争环境,而竞争环境就会导致一定的不确定性,因为无法保证竞争的后果。
Go
为了解决上面的情况,原生支持的是并发编程,即按照顺序立即处理而非等待,也并不是同时执行。让后厨一直在忙,而非要求增加窗口服务和后厨人员数量。Go 使用 Go
协程(Goroutine
)和信道(Channel
)来处理并发问题。语法是非常简单的,以至于2个关键字就可以解决:go, channel
。但什么是协程,什么是信道?并没有说清楚,还需要补充一些东西。
协程,是用户空间的并发运行的一些函数或方法。在曾经的电脑,有很多程序要执行,操作系统会将他们标记为一个一个的进程,每一个进程有自己的独立的空间和资源,并接受系统内核调度。具体来讲操作系统的内核按照某种优先级关系从进程中选择一个,让它执行,如果它处于等待状态(例如等待网络用户连接或者IO读取等等),内核就将其休眠,并打包放入栈中,然后再让另一个程序从休眠中唤醒执行(这样的过程叫做上下文切换),由于进程非常有自己的各种资源,如果程序是网络服务或者IO密集型,上下文切换非常频繁,系统对上下文切换的开销就会非常大(可以看作是一种无用功)。所以随着技术演进就出现线程,线程就是一种轻量级的进程,是程序执行的最小单元,一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组 成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个 进程的其它线程共享进程所拥有的全部资源。这样,系统调度线程会快很多。线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。协程是一个更小的单位,如同线程一样,有自己的上下文,切换受系统控制;协程也相对独立,有自己的上下文,但是其切换由用户控制而非系统,所以称之为用户态并发函数或方法。具体可以参见该 博文。
借用别人一段话原文 Go 协程相比于线程的优势:
相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。所有这一切都在运行时进行,作为程序员,我们没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。 Go 协程使用信道(Channel)来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是 Go 协程之间通信的管道。
再谈更多内容之前,先看几个实例。如何开启协程。
package main
import (
"fmt"
)
func hello(name string) {
fmt.Println("Hello", name)
}
func main() {
SomePeople := []string{"Jack", "Perter", "John"}
for _, v := range SomePeople {
go hello(v) // 用go 关键字开启了一个新的协程,这个循环开启了三个协程
}
fmt.Println("There is no nobody!")
//time.Sleep(1 * time.Second)
}
如果你按照上述代码运行,那么很可能你只能看到 >>There is no nobody!
的提示,运气好,可能看到像某个People打招呼的情况。这里为了看的直观,将time一行去掉注释再次运行,即可看到类似如下的结果。
//-------result-------------------
There is no nobody!
Hello Jack
Hello Perter
Hello John
//-----another result------
Hello Jack
There is no nobody!
Hello John
Hello Perter
如何按照顺序执行的理念,OhmyGod,这怎么可能?循环是按照"Jack", "Perter", "John"的顺序逐个开启协程的,然后打印 nobody
,为什么?
首先需要知道的是,我们通过 main()
函数的一个循环,通过 go
关键字创建了三个 Hello()
的协程,然后打印 There is no nobody!
。由于 main()
是其他三个协程的创建者,如果 main()
结束,其他协程则结束。 从之前谈到的概念,可以知道协程一旦创建,就相当于已经下发任务,不需要等任务完成后再返回执行,所以 main
函数自己通过循环创建完三个协程后,马上就执行打印没有人的语句了,而不会等协程打印完再打印。同理,main()
执行完打印就退出,如果没有延时1s的退出的语句,我们很可能什么打招呼过程都看到。而加上延时后,不同次执行的结果也可能不完全一样,这是由于系统或Goruntime调度的原因,我们无法确定哪个协程会被系统先执行,只知道会被执行,所以There is no nobody!可能发生在任何时候,向"Jack", "Perter", "John"打招呼的顺序也可能是任何顺序。这就是协程。
这时候,也许该发生一点点担忧,因为我们通常编写的代码和程序需要需要匹配到现实世界的某一个确定过程,例如把大象关进冰箱,首先需要打开冰箱门、把大象放进去、关上冰箱门。这三个步骤必须按照既定顺序执行,不能够错乱,否则大象不可能被关进冰箱。那么就需要一个模型来指导协程之间如何配合工作。
Go
的原生并发模型叫“顺序通信进程” (communicating sequential processes) 或被简称为 CSP。常见与之区别较大的另一个并发模型叫共享内存,关于共享内存模型可以参考博文。CSP并发模型是在1970年左右提出的概念,属于比较新的概念(提出一个崭新的概念是多么的不容易),不同于传统的多线程通过共享内存来通信,CSP 讲究的是 Do not communicate by sharing memory; instead, share memory by communicating。更多关于并发编程了解可以阅读 并发模型比较 或者《七周七并发模型》。
https://www.jianshu.com/p/36e246c6153d
https://i6448038.github.io/2017/12/04/golang-concurrency-principle/
https://books.studygolang.com/gopl-zh/ch8/ch8.html
有疑问加站长微信联系(非本文作者)