从0开始go语言-13|Go主题月

244 阅读8分钟

并发编程

Go语言的特色:优雅的并发编程范式完善的并发支持出色的并发性能

原始的程序是没有并发的概念的,因为命令式程序设计语言是以串行化为基础的,就是一条道走到黑,程序会顺序执行每一条命令,整个程序只有一个执行的上下文,即一个调用栈,一个调用堆。

并发则意味着运行时需要执行多个上下文,对应着多个调用栈,每个进程在调用的时候都要自己的调用栈和堆,有自己完整的上下文,操作系统在调用进程的时候,会保存调度进程的上下文环境,等该进程获得时间片后,再恢复该进程的上下文对应的系统中去。

串行化存在的问题

  • 一般系统都是多种逻辑一起处理,一方面我们需要灵敏响应的图形用户界面,一方面程序还需要执行大量的运算或者IO密集操作,而我们需要让界面响应与运算同时执行。
  • 当我们的服务面临很多用户请求的时候,需要更多的服务来分别响应用户,如果顺序执行,最后面来的需要等前面的排队执行完,这个响应时间无法估量会很久。
  • 事务在不同的分布式环境中,相同的工作单元在不同的计算机上处理着被分片的数据。
  • 计算机的CPU从单内核(core)向多内核发展,而我们的程序都是串行的,计算机硬件的能力没有得到发挥。
  • 我们的程序因为IO操作被阻塞,整个程序处于停滞状态,其他IO无关的任务无法执行。

并发的优点

  • 并发能更客观地表现问题模型,就是更符合我们现实中问题的表现形式,大多数问题模型不是单进程不是串行化执行的。
  • 并发可以充分利用CPU核心的优势,提高程序的执行效率。硬件的发展驱动软件程序设计的发展。
  • 并发能充分利用CPU与其他硬件设备固有的异步性,并发更符合硬件场景。

并发实现模型

  • 多进程:多进程是在操作系统层面的进行并发的基本模式,同时也是开销最大的模式,比如在Linux平台上,系统有专门的进程负责网络端口和链接管理,还要专门的进程负责运算和事务,这种方法的好处在于简单,进程间互不干扰,坏处在于开销大,因为所有的进程都是由操作系统的内核控制的。
  • 多线程:多线程在大多数操作系统上都属于系统层面的并发编程,也是我们使用最多最有效的模式,线程比进程开销小很多,但开销依旧比较大,且在高并发的模式下,效率会受到影响。
  • 基于回调的非阻塞/异步IO:这种模式实际上是多线程的危机感慢慢促进的,当多线程不满足的时候新的技术就会出现,在很多高并发服务器开发实践中,使用多线程模式会很快耗尽服务器的内存和CPU资源,通过事件驱动的方式使用异步IO,使服务器持续运转,且尽可能地少用线程,降低开销,它目前在Node.js中得到了很好的实践。但是使用这种模式,编程比多线程要复杂,因为它把流程做了分割,对于问题本身的反应不够自然。
  • 协程:协程(Coroutine)本质上是一种用户态线程,不需要操作系统来进行抢占式调度,且在真正的实现中寄存于线程中,因此,系统开销极小,可以有效提高线程的任务并发性,而避免多线程的缺点。使用协程的优点是编程简单,结构清晰;缺点是需要语言的支持,如果不支持,则需要用户在程序中自行实现调度器。目前,原生支持协程的语言还很少。

人的思维模式是串行的,串行的事务具有确定性,如果并发执行就会出现很多不确定性,举例:我上班如果让我一件事一件事干完再去干接下来的事,一般不会出错,但是每个事情干到一半,我可能要切换去干别事情,我丢下的这个事情,有可能别的人会接着干,别人干不完又走了,我回去还需要接着干,这种并发的处理事情就很容易出错,程序也一样,并发的不确定性给程序带来了意外和危害,也让程序变的不可控,为了保证线程之间通信而采取的共享内存,为了保证共享内存的有效,我们会采用很多措施,比如加锁,但是并发的问题任然不可避免的在发生。

我们可以将这种处理方式可以归纳为“共享内存系统”,后来解决并发的模式出现了“消息传递系统”,将线程间共享状态的各种操作都被封装在线程之间传递的消息中,这通常要求:发送消息时对状态进行复制,并且在消息传递的边界上交出这个状态的所有权。从逻辑上来看,这个操作与共享内存系统中执行的原子更新操作相同,但从物理上来看则非常不同。由于需要执行复制操作,所以大多数消息传递的实现在性能上并不优越,但线程中的状态管理工作通常会变得更为简单。随着时间的推移,很多语言开始采用完善和采用消息传递系统,并以此为核心支持并发,比如Erlang,比如后面大名鼎鼎的RabbitMq消息中间件底层就是由Erlang语言编写的。

goroutine

goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。你将会发现,它的使用出人意料的简单。假如我需要实现两个数相加的函数,并把相加的结果打印输入如下:

func Add(x, y int) { 
     z := x + y 
     fmt.Println(z) 
}

意思很简单吧,函数参数传入x和y让其相加赋值给z然后打印输出z。那么我要并发执行找个相加的函数怎么调用呢?go语言就在前面加个go!!!!!如此的简单粗暴!

go Add(1, 2)

在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束了。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。 光说不练假把戏,咱循环调用下试试如下:

func main() { 
 for i := 0; i < 10; i++ { 
     go Add(i, i) 
 } 
}

简单粗暴的执行10次调用,预料的结果是打印输出十次i和i相加的结果。那么结果会让你大呼被骗,因为这个程序什么都不会输出,纳尼???这和预想的根本不一样,为什么呢?这个问题不是程序有问题是Go语言的程序执行机制的问题,Go语言在程序初始化main package并执行main()函数开始,当main()函数返回时,程序退出,且程序并不会等待其他goroutine(非主goroutine)结束。这下搞清楚了,领导(主函数)指派一堆活给我们程序员(非主goroutine),并不等我们把代码写出来运行出来,就去给甲方说我们的程序做好了,真正写代码的在屏幕输出的是程序员,领导说的时候甲方当然看不到结果没来得及写代码。

对于上面的例子,主函数启动了10个goroutine,然后返回,这时程序就退出了,而被启动的执Add(i, i)的goroutine没有来得及执行,所以程序没有任何输出。那咋办要我想我拿出我的的sleep函数就是一顿操作,每次循环睡他5秒钟,够你两个数相加返回了吧,那么50秒之后我就会看到打印输出的结果了!!!

如果这样解决那的确怎么看的都不优雅,在Go语言中有自己推荐的方式,它要比这些方法都优雅得多!要让主函数等待所有goroutine退出后再返回,如何知道goroutine都退出了呢?这就引出了多goroutine之间通信的问题且听我下回学习分解!!!!

备注

本文正在参与「掘金Golang主题学习月」, 点击查看活动详情