分享

并发和异步编程语言不能停下的脚步

 千秋鹤 2017-12-08

陈叶皓 chenyehao@gmail.com

每到假期,是写博客的好时机,这次试图写一篇软件技术发展的演义。 

一、分时操作系统

计算机硬件技术的发展大家习以为常,每18个月cpu主频翻番的摩尔定律,以及近年来的多核cpu出现,大众对于计算能力的提高已经不太在意,最近慢慢受关注的armintel之争,预示未来比拼的是同等功耗下的计算能力。

 

一般认为,软件技术发展的速度落后于硬件,好像多核出现以后,传统的编程语言还没有做好准备,还不能够编写出可以充分每个cpu内核的软件,那我就来讲一讲什么是“传统”的软件技术。

 

插入:应用软件和操作系统

操作系统,其实是把大多数应用软件会使用到的通用功能,抽象集中,使得应用软件可以直接使用,不必每个应用软件都实现诸如内存分配等操作。所以操作系统,程序库,应用软件也没有太明显的界线,只是代码抽象(共享)的不同层次。

 

最早的计算机语言,直接和硬件打交道,使用打孔机输入代码,然后执行代码所指定的任务,一次只能执行一个任务,这是为“单任务”。那时候也没有操作系统,代码直接操作寄存器,内存,给cpu发指令,完成工作。

 

然后巨型计算机诞生,计算能力有了显著地提高(也许还比不上我们手头的iPhone),于是要求一台计算机可以同时为多个人服务,以提高计算机的使用效率(计算机可是非常昂贵的),伟大的Unix诞生了。Unix是一个操作系统,它实现了给不同用户分享“时间片”的功能,两个人用同一台计算机,系统自动分配时间片,给你用几毫秒,给他用几毫秒,彼此感觉不到对方的存在(世界上最远的距离)。即便是当今世界的软件,也是基于时间片分享的思想,100人同时访问一个网站,每个人都差不多时间看到结果,没有显著的时间差别,就是使用的时间片的原理。

 

二、进程与通信

时间片解决了“分工”,却没有解决“合作”的问题(我来到这个世界,就是为了遇见你)。大部分情况下,分工,也就是顺序操作,都能解决问题,一件事情可以分几个步骤来完成,前一个步骤的输出是后一个步骤的输入(你也许想起了unix下的管道操作符)。

那“合作”是什么,合作可以让工作完成得更快(同一件事情由多个cpu同时完成),或者使软件模块更加独立和简单(也就是我们现在所说的service)。现在,浏览器访问网页服务器,或者网页服务器访问数据库服务器,都是合作。可是在30年前,大家都是在同一台主机(一个cpu)上工作,“合作”的好处没有那么明显,所以一直都不是编写软件的主流方法。

 插入:什么是“合作”

“合作”,就是软件模块间的通讯,如果是跨计算机,就是网络通讯,如果在同一台主机,也可以是网络协议,也可以是进程间通讯,也可以是线程共享数据,也可以是基于消息的事件触发,这就是今天要讲的主题。

 虽然不是主流,操作系统还是提供了进程间通讯的支持(IPC,随着网络的出现,主机的跨网络通讯不可少,逐渐地,基于socket的通讯成了软件编写的主流。

请想象一下,在一个网络中,每个进程可以提供一个或多个功能,进程和进程之间通过socket通讯,进程可以启动在不同的服务器上,这就是现在主流的面向服务架构(SOA)。软件架构的基石,在30年前早已经打造好,有了进程(功能)和socket(通讯),可以开发任何(网络)应用,在这个架构下,只要独立运行的进程足够多,就能充分利用多台服务器、或者是多核的服务器的计算资源。

 讲到这里,软件系统架构在30年前早已完备,那后来出现的线程是怎么回事,异步编程为什么还要学习,有多大的价值。

 没错,在分时操作系统和socket之后出现的软件技术,都不是必须掌握的,都是锦上添花。但这些技术,源自于非功能的需求,使用这些技术,要么大大提高了硬件资源的利用率,要么带来了编程语言的简化,又或者是同时满足,在多核的服务器时代,掌握异步编程已经成为必须。

 插入:单台服务器还是多台服务器?

接下去要讲的多线程技术和异步编程,都是针对单台服务器才有效的技术,多台服务器之间,就是30年前早已成熟的进程加socket,没有其它选择。

 三、线程的产生

 Unix被发明的时候没有线程这个概念,分工合作通过进程间的通讯来完成,如果通讯是基于socket,那么进程在本机内的通讯和进程在网络间的通讯无差别。进程间不共享数据,交换信息只能通过socket互相发送。

线程被称为“轻量级进程”,这是为了提高本机内的进程间通讯效率而发明的。线程的功能完全可以被进程代替,除了一些性能上的优势

1.       创建线程占用的内存更少。

2.       线程间传输数据时,因为是在同一个进程的地址空间,可以通过引用来访问共享的数据块,节省了调用内核发送/接收数据的开销

从上面两点看,线程的最大优势,是可以充分利用单台服务器的多核cpu计算资源,并发地处理任务。虽然使用进程能达到同样的目的,但使用了线程,速度可以提高一个数量级,由于支持线程带来的性能提升是如此明显,大约在10年前,继Windows之后,Linux内核支持了多线程。

四、异步编程的出现

即便被称为“轻量级进程”,线程消耗的内存资源仍然不少,通常情况下,操作系统创建一个线程需要消耗1Mb的内存。设想我们开发一个有大量用户并发访问的网站,我们为每个访问请求创建一个线程(这样编写软件是有理由的,提供了较好的并发性,编写代码简单),当并发访问量急剧上升时,就会把系统内存耗尽。同时,创建过多的线程对整体性能提高也没有帮助,同时执行的线程数受cpu内核数目的限制,比如服务器的cpu16核,那么最多可有16个线程同时(并发)执行,其它的线程也只能等待下一个cpu时间片。

说到这里,一种完美的软件模型呼之欲出。有没有可能创建一种“轻量级线程”,它的内存占用比线程更少,它可以在一个线程内分“时间片”,在一个操作系统的线程上,创建多个“轻量级线程”,来实现多任务切换,而只占用一个线程的内存资源。进一步,可以创建上万个“轻量级线程”,共享一个线程池,线程池里面的线程数目只需要和cpu内核数相当,就可以充分利用计算资源,而上万个“轻量级线程”在线程池内并发执行,既保证了并发性,又严格控制了整体资源的消耗。

 停一下,请把上面这一段话再读一遍,确保已经理解。其思想就是使用有限的线程,来创建无数个并发任务。在之前的网站场景下,可以创建一千个“轻量级线程”,来响应用户的一千个并发请求,而操作系统可能只需要创建30个线程,用于执行这些“轻量级线程”。

另一个例子是网游,假设可以使用单台服务器支持5万个在线用户,可以创建5万个“轻量级线程”来服务每一个用户,而实际上,也只需要消耗操作系统30个线程,基于“轻量级线程”的代码编写一定是简单的,因为这一段代码只需要服务一个游戏用户,功能非常单一。

 

插入:在当今支持“轻量级线程”的编程语言中,创建一个“轻量级线程”所需要的内存在1k以下,所以100Mb的内存10万个“轻量级线程”,资源消耗完全不是问题。

 

其实时间片,是由操作系统来分配,只能分配给进程和线程,所谓线程内的“时间片”是不存在的,那么“轻量级线程”如何实现在一个线程内的交替执行,这就是异步编程。

异步编程,是基于事件触发的,简单来说,是由消息触发“轻量级线程”的一段代码执行,执行完毕后,“轻量级线程”交出线程控制权,进入等待状态,等待下一个消息触发,这时线程可以去服务另一个等待执行的“轻量级线程”。有一个全局的线程,是用来监视所有的消息,来决定哪个“轻量级线程”被触发执行。

五、总结

为了说(shui)服你学习异步编程,我做了些铺垫。首先,Unix早就解决了进程间的分工合作,使用多进程编程就可以充分利用多核cpu的计算资源,而进程间通讯基于socket协议,你所见到的任何服务软件(数据库,web),都是使用socket编程。然后,线程的发明,使得单台服务器内的通讯,速度和效率大大提高。最后,使用异步编程,可以使用很少的内存资源,和线程数目(与cpu内核数相当),创建数以万计的“轻量级线程”,方便地编写安全、高效的单机服务系统。至于多机,内部用异步编程,服务器之间走socket协议。

使用支持“轻量级线程”的编程语言和框架,程序员并不需要太多的额外工作,最终代码和传统的命令式代码相比,也没有太多差别,也有分支语句,循环语句,函数调用。那么,现在有哪些编程语言和框架支持“轻量级线程”,而值得你在学习一门新的编程语言时应该优先考虑呢,我所知道的有

1.       Erlang

2.       Go语言

3.       F# with MailboxProcessor

4.       Scala with Akka

你会发现,上述语言中,有三个是纯函数式语言(ErlangF#Scala),Go语言是混合式语言,也支持函数式编程,其实,异步编程和函数式编程有着密切的关系,有机会(通常是无限期推迟借口)再向大家介绍。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多