一、开篇语
我的上篇文章《关于如何提供Web服务端并发效率的异步编程技术》又成为了博客园里“编辑推荐”的文章,这是对我写博客很大的鼓励,也许是被推荐的原因很多童鞋在这篇文章里发表了评论,有童鞋说我这篇文章理论化很严重,没有实际代码和具体项目做支撑,这个评论让我有种理论和实践脱节的味道,所以我想在这里谈谈我为什么要写这篇文章的原因,这篇文章是把我前不久学习多线程编程的一个总结。
当我从我书堆里找到所有与多线程开发相关的书籍简单阅读后,我发现了一个问题,在java里开发多线程最强有力的实践就是做服务端的并发处理,其他的应用都是小儿科,而这个实践对于每个web程序员而言都是极具经济价值的,所以我就想延着这个实践继续学习多线程,接着我发现并发处理这块都离不开IO处理,同时IO处理的优劣直接关系着并发处理的效率问题, 关于并发的知识,有人说最好是写写代码,告诉我们API如何运用,说实话简简单单写几行不是太难的事情,写几行代码并不代表你能掌握它,掌握多线程绝不是集中精力看懂几行代码,看懂后顿悟的来一下:哦,明白了。
要真的掌握某种技术你就必须要知其所以然,但是要理清并发的所以然就不那么容易了,这其实是一种设计的思想,而不是简单几行代码,几个jdk提供的api,代码和api只不过是实践这些思想的实现,我之前学习线程开发,学习io的api,这个过程很早就开始,但这个过程都基本都是记住遗忘再记住再遗忘的死循环,究其原因就是因为它们本身的设计思想太深奥,你没有理解这个设计思想,你就无法将那些api的内在关系串联起来,所以最终的结果只会是遗忘。技术的本质就是工具,要用好工具的前提就是要真正理解这个工具的价值,这就好比斧头设计出来时用来砍柴的,菜刀设计出来是为了切菜,如果我们用斧头用来切菜,菜刀用来砍柴,这肯定要闹笑话了,对于像线程和IO这样复杂的工具,使用起来和使用像这种简单工具菜刀和斧头一样,应该是物尽其才,各司其职,复杂工具的麻烦就是理解起来比较困难些了,自己得多花点时间和功夫了。
我早期的文章都会写好多代码,但是最近的文章我会写更多文字,我希望对我熟悉的技术有更进一步的理解,因此理论和猜想会更多点。
本篇文章的标题是《关于Web开发里并发、同步、异步以及事件驱动编程的相关技术》,本文的目的就是要和大家一起好好的理解下标题里很常见的概念,通过理解它们,指导我们今后对这些概念的学习和运用。
二、我们来好好理解下并发、同步和异步这三个概念
标题的三个概念在web开发领域太常见了,就算是入门web开发的童鞋随便找本web开发的书籍都能找到这三个概念,但是如果你对这三个概念理解不够透彻,如果在实际开发中正好碰到三个概念的交织在一起的问题,估计很多人就会麻了头。
并发
首先是并发这个概念,我们看看现实中并发的例子。
第一个例子:
有一天你在家里做饭,你这天的心情非常好,想大吃一顿,所以要有菜有汤还有酒喝,开始干了,你首先会在电饭煲里把饭煮好,饭在煮的时候,你会抽空去超市买瓶啤酒,买好了啤酒回到家里你就要做菜了,你想做个汤,为了节约时间,你会先烧开水,烧开水同时,你会切菜,切好的菜就得炒菜,后面的过程此处省略一万字了,地球人我想都明白以后是咋样了,我就不再详细讲述了。
上面的描述里煮饭、烧水、买啤酒、切菜、炒菜,它们都可以当作独立的任务,这些任务都是为了达到某种特定的目的,它们之间是不能相互替代替换的,但是在实际操作里你并不是等待某个任务做完了再去做另一个任务,而是会运用统筹方法,交叉进行,这么做的好处就是效率很高,节约时间,这和程序员平时工作类似,一边编写程序,一边听歌,一边和朋友同事在QQ里聊天。
第二个例子:
现实生活里还有一种并发,例如我们在食堂里吃饭,去食堂吃饭的人很多,因此食堂开启了好几个打饭的窗口,如果打饭的窗口开的越多,那么吃饭人打到饭所用时间也就最少,如果每个食客都有一个窗口一对一的服务,那么食客基本就不用浪费任何时间去等待了,这种vip感在中国这么资源紧张的国家里,简直太爽了,食堂打饭的并发就和我上篇文章里讲到的web服务端并发类似。
这里有列举了两个并发的实例,似乎有点多此一举之嫌,但是如果我们仔细研究下这两个例子,它们其实有本质的不同,第一个例子其实是一个人的并发,第二个例子就是多人的并发,用计算机的概念描述就是前一个例子就是一个线程完成的并发,后一个例子就是多线程完成的并发,第一个例子的线程载体就是做饭的人,第二个线程的载体就是每个打饭窗口的工作人员,这两个例子其实就可以类比到我上篇文章提到的做法二和做法三,特别是第一个例子,虽然这个场景人们非常熟悉,但是到了程序开发里却有很多人把第一个实例并发给遗忘掉,特别是做web开发的人,并发就是多线程,一个线程怎么可能做并发了,但是例子一说明一个线程是可以做并发的。
由上面的描述,下面我要给出我自己总结的并发定义:
并发就是让一系列独立的任务同时执行,同时执行包含两种情形,一种就是完全独立执行,例如在拥有双核处理器的计算机里,每个CPU在同一个时间里处理不同的任务,另一种情形是一个任务还没执行完,而CPU计算被闲置的时候CPU用来处理别的任务。
而现实里我们要做到高效的并发就得把两种模式的并发混合起来使用,食堂打饭就是混合了两种模式的并发,对于计算机两种混合的并发才能把计算机的计算能力深度挖掘,所以做法三就是混合了两种并发,做法三是高效和先进的,做法三是值得推广的。
接下来就要谈谈同步和异步两个概念了,记得刚工作不久,有次参加面试,面试官问我什么是同步什么是异步,我当时的回答是:同步就是单线程,异步就是多线程,当时那位面试官停顿了下,跟我说这个解释没有问题,但是在实际开发里这个答案不能让你把同步和异步用的很好。现在回忆那位面试官给我答案的评定还是有道理的,下面我开始讲同步和异步了。
同步:
在计算机领域同步应该是指相互独立的任务一个接着一个运行,拿上面做饭的例子描述就是煮饭煮好了才能去超市买啤酒,酒买好了才能切菜,菜切好了才能炒菜,菜炒好了才能烧水烧汤,明显同步的方式在现实生活里效率是很低的,不过同步有个很大的好处就是简单,简单的理论基础就是一次就做一件事情,做的时候一心一意专心致志。
异步:
下面就是异步的概念,同步和异步在当代的计算机领域里是两个对等的概念,但是如果我们要追溯异步的本质,就会发现同步和异步其实是一个因果关系,同步是因,异步是果,换个说法同步就是异步这个孩子他娘,所以我们要正确理解异步就要明白同步做不好的事情,在web前端有个鲜活的例子可以说明这个问题,这个例子就是ajax,在ajax出现前,浏览器的一个独立页面和服务端的交互是通过一个socket进行的,这个socket相当于一个线程,它是采用同步处理的方式,所以没有ajax的时代,单独页面每次和服务端的交互都只能做一件事情,而网络处理的速度往往是最慢,而且网络的效率很容易受到外界影响,因此同步请求会导致用户使用页面的体验很不好,同步网站时刻都在考验着用户的耐心,这就导致ajax的出现,ajax本质就是浏览器提供了一个新的socket链接,这个socket是有别于同步的socket,它可以独立于同步socket运行,有了ajax这个socket我们就可以在不影响页面同步操作的前提下也能从服务端获取数据,这就好比浏览器由原来一个人完成和服务端的交互变成了现在两个人完成和服务端的交互操作,它们各司其职,共同完成页面上的功能。
异步和并发的关系和区别:
异步操作和并发看起来很像,特别是和我前面讲到并发的做饭实例很像,的确并发和异步常常是交织在一起的,但是它们还是有很大的区别,这个区别在于它们所达到的业务目的,并发是业务含义是能做更多事情,而异步是让多个人共同完成一个任务,异步其实是通过专业角度把一个大任务拆分成相对独立小任务,让更加专业的人完成这个小任务,小任务完成后最后汇总成一个大任务的结果,上面ajax就是这样的道理,其实我以前着重研究的hadoop就是一个典型的异步任务系统。
异步和并发共同点都是通过多线程来实现,通过它们在业务场景的区别,我们反过来学习多线程,就知道多线程能为我们做好哪些事情,那么当你碰到需要使用多线程的业务场景就知道按什么思路来分析这个业务场景了。
三、关于事件驱动编程
在我上篇文章里我反复写了好多事件驱动这四个字,到了本文里事件驱动后面我加了两个字编程,为什么加它我后面会讲到。
全世界最熟悉事件驱动的程序员是哪种程序员?答案是前端工程师,不管是桌面前端还是web前端都是世界上最熟悉事件驱动的,以web前端为例,我们做页面可以不去想什么面向对象编程,什么jQuery框架咋用,但是为按钮,为页面元素添加相关事件操作肯定是不可缺少的,而web前端的事件处理机制就是标准的事件驱动机制,为了讲清楚事件驱动,这里我回顾下页面里事件机制,我们开发页面的事件时候,第一步就是定义事件(定义事件就是在定义一个函数)或者说为事件定义一个动作,并把事件绑定到指定的元素上,如果我们没有触发元素上的事件,那么定义好的函数也就不会执行,如果元素上的事件被触发了,定义好的函数才会执行。代码不提供了,这个太简单和平常了。
关于浏览器里事件机制实现方案,我找了许久都没有找到完整的资料,因此这里我大胆揣测下事件机制的实现方案,下面的内容完全是我的猜想,不一定和实际相符,具体如下:
首先我要说多线开发程里有一个经典的设计方法,这个方法就是生产-消费者模型,生产-消费模型特点就是生产者和消费者被一个中间队列分隔开来,不管是生产请求还是消费结果都是通过这个中间队列中转,这样就可以把生产者和消费者关系解耦,事件实现机制从宏观上和生产-消费模式类似,这个类似不是指设计思想,而是沟通双方联系的那个中间层。
事件处理的机制里应该有个事件处理器,事件处理器位于元素和事件处理方法的中间位置,我们在定义事件的时候就是等于在事件处理器里定义元素和事件处理方法的关系,当这种对应关系定义好后,事件处理器就会启动一个死循环,这个循环反复检测元素的状态变化,当事件处理器发现某个状态产生了变化,处理器就会找到对应的事件处理方法,然后执行这个方法。
Nodejs是一个事件驱动的语言,这是官方对nodejs的定义,很多评论说nodejs是第一个把事件驱动上升到语言层级的编程语言。所以本文我在事件驱动后面加上了编程两字。
传统语言做开发都是按时间先后顺序进行的,这么做既可以降低语言的学习成本,也让开发代码思路比较容易控制,但是现实场景是复杂的,这种按事件顺序的开发流程并不一定是我们解决现实问题的最佳方式。这好比我们做一件事情,在做的时候我们会碰到很多情况,由于发生的情况的不同,那么这件事情的结果可能就会因为情况不同而发生变化,如果按照时间顺序的编程方式想做好上面的事情会让程序变得十分复杂,因为我们要按照时间顺序做出各种不同执行路径,这就是排列组合的办法了,这显然让事情变得复杂了,如果用事件驱动编程方式,我们只要定义好事务的起因,各种不同的过程情况,以及所能得到结果,换句话说我们首先只要关注实体内容而忽略事务关系问题,而事务关系则是在事件处理器里定义的,当我们发送给事件处理器一个指示信号,处理器就会对应找到某个行为,那么事件驱动编程就简化了程序开发的流程。
事件驱动编程实现的核心技术就是能让方法变成对象能在事件处理的流程里传递,方法得到事件管理器的指令后在合适的位置上被促发,这就是回调函数,而javascript语言里函数可以当做对象传递,也就保证了事件驱动编程上升到语言层级变成了可能,我想这就是nodejs作者使用google的V8引擎设计出nodejs的重要原因之一。
回调函数改变了传统程序开发的流程,但是大量使用回调函数的代码常常会变得晦涩难懂,这也是javascript语言很掌握的重要原因之一。鉴于回调函数这个毛病出现了promise编程,promise的目的是让回调编程看起来像按时间顺序编程的方式,我前端时间研究了下promise,但是没有深究,原因是回调本来就很难理解,回调变成顺序编程,那岂不是更加糊里糊涂,按现在技术特点,我会选择慎用promise技术。
四、小结
本文的主要内容就到此为止,鉴于上篇文章有些内容有很大的争议,本文想做一定的解释说明:上文最大的问题还是IO的解释上,我承认自己对IO其实理解不太深入,所以我只是用文字描述非阻塞IO的处理,这段文字写的时候我还是很注意的,尽量不讲太多,我当时只用一个理念来写这个实现,就是非阻塞IO的具体实现里一定会有一个和事件处理器相类似的中间层来协调IO操作和CPU的操作,这点我自信不会有错。其实IO技术在java里相当复杂的,比较难学,现在jdk提供的IO的模型有三大类,BIO(阻塞IO),NIO(阻塞IO或者叫新IO)以及AIO(异步IO或者叫NIO2.0),jdk的io是建立在操作系统IO上的,所以学习IO真的要多花点心思和精力,这是我今后学习研究的一个重点。
此外,在计算机里不管执行什么任务都会包含IO操作和CPU计算两个过程,IO的速度问题常常拖累了CPU的计算,由于某些IO太慢了,如果站在CPU的角度,它等待IO处理的代价实在太高了,所以先进的IO就是为了调整IO处理和CPU计算的关系,我觉得新IO解决方案要解决的核心问题。
好了,本文写到这里,本文和上文类似都是谈自己对某些技术的理解,文字很多,几乎没啥代码,希望童鞋们多拍拍砖,有问题才能进步的更快。