高性能服务器架构思路「不仅是思路」
在我们使用多线程的API时,情况就会好很多,我们可以用一个函数指针,或者一个带回调方法的对象,作为线程执行的主体,并且以句柄或者对象的形式来控制这些线程。作为开发人员,我们只要掌握了对线程的启动、停止等有限的几个API,就能很好的对并行的多线程进行控制。这对比多进程的fork()来说,从代码上看会更直观,只是我们必须要分清楚调用一个函数,和新建一个线程去调用一个函数,之间的差别:新建线程去调用函数,这个操作会很快的结束,并不会依序去执行那个函数,而是代表着,那个函数中的代码,可能和线程调用之后的代码,交替的执行。 由于多线程把“并行的任务”作为一个明确的编程概念定义了出来,以句柄、对象的形式封装好,那么我们自然会希望对多线程能更多复杂而细致的控制。因此出现了很多多线程相关的工具。比较典型的编程工具有线程池、线程安全容器、锁这三类。线程池提供给我们以“池”的形态,自动管理线程的能力:我们不需要自己去考虑怎么建立线程、回收线程,而是给线程池一个策略,然后输入需要执行的任务函数,线程池就会自动操作,比如它会维持一个同时运行线程数量,或者保持一定的空闲线程以节省创建、销毁线程的消耗。在多线程操作中,不像多进程在内存上完全是区分开的,所以可以访问同一份内存,也就是对堆里面的同一个变量进行读写,这就可能产生程序员所预计不到的情况(因为我们写程序只考虑代码是顺序执行的)。还有一些对象容器,比如哈希表和队列,如果被多个线程同时操作,可能还会因为内部数据对不上,造成严重的错误,所以很多人开发了一些可以被多个线程同时操作的容器,以及所谓“原子”操作的工具,以解决这样的问题。有些语言如Java,在语法层面,就提供了关键字来对某个变量进行“上锁”,以保障只有一个线程能操作它。多线程的编程中,很多并行任务,是有一定的阻塞顺序的,所以有各种各样的锁被发明出来,比如倒数锁、排队锁等等。java.concurrent库就是多线程工具的一个大集合,非常值得学习。然而,多线程的这些五花八门的武器,其实也是证明了多线程本身,是一种不太容易使用的顺手的技术,但是我们一下子还没有更好的替代方案罢了。 ![]() 多线程的对象模型 在多线程的代码下,除了启动线程的地方,是和正常的执行顺序不同以外,其他的基本都还是比较近似单线程代码的。但是如果在异步并发的代码下,你会发现,代码一定要装入一个个“回调函数”里。这些回调函数,从代码的组织形态上,几乎完全无法看出来其预期的执行顺序,一般只能在运行的时候通过断点或者日志来分析。这就对代码阅读带来了极大的障碍。因此现在有越来越多的程序员关注“协程”这种技术:可以用类似同步的方法来写异步程序,而无需把代码塞到不同的回调函数里面。协程技术最大的特点,就是加入了一个叫yield的概念,这个关键字所在的代码行,是一个类似return的作用,但是又代表着后续某个时刻,程序会从yield的地方继续往下执行。这样就把那些需要回调的代码,从函数中得以解放出来,放到yield的后面了。在很多客户端游戏引擎中,我们写的代码都是由一个框架,以每秒30帧的速度在反复执行,为了让一些任务,可以分别放在各帧中运行,而不是一直阻塞导致“卡帧”,使用协程就是最自然和方便的了——Unity3D就自带了协程的支持。 在多线程同步程序中,我们的函数调用栈就代表了一系列同属一个线程的处理。但是在单线程的异步回调的编程模式下,我们的一个回调函数是无法简单的知道,是在处理哪一个请求的序列中。所以我们往往需要自己写代码去维持这样的状态,最常见的做法是,每个并发任务启动的时候,就产生一个序列号(seqid),然后在所有的对这个并发任务处理的回调函数中,都传入这个seqid参数,这样每个回调函数,都可以通过这个参数,知道自己在处理哪个任务。如果有些不同的回调函数,希望交换数据,比如A函数的处理结果希望B函数能得到,还可以用seqid作为key把结果存放到一个公共的哈希表容器中,这样B函数根据传入的seqid就能去哈希表中获得A函数存入的结果了,这样的一份数据我们往往叫做“会话”。如果我们使用协程,那么这些会话可能都不需要自己来维持了,因为协程中的栈代表了会话容器,当执行序列切换到某个协程中的时候,栈上的局部变量正是之前的处理过程的内容结果。 ![]() 协程的代码特征 (编辑:晋中站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |