先看的这篇文章:http://blog.csdn.net/qq910894904/article/details/41699541
以nginx为代表的事件驱动的异步server正在横扫天下,那么事件驱动模型会是server端模型的终点吗?
我们可以深入了解下,事件驱动编程的模型。
事件驱动编程的架构是预先设计一个事件循环,这个事件循环程序不断地检查目前要处理的信息,根据要处理的信息运行一个触发函数。其中这个外部信息可能来自一个目录夹中的文件,可能来自键盘或鼠标的动作,或者是一个时间事件。这个触发函数,可以是系统默认的也可以是用户注册的回调函数。
事件驱动程序设计着重于弹性以及异步化上面。许多GUI框架(如windows的MFC, Android的GUI框架),Zookeeper的Watcher等都使用了事件驱动机制。未来还会有其他的基于事件驱动的作品出现。
基于事件驱动的编程是单线程思维,其特点是异步+回调。
协程也是单线程,但是它能让原来要使用异步+回调方式写的非人类代码,可以用看似同步的方式写出来。它是实现推拉互动的所谓非抢占式协作的关键。
总结
协程的好处:
- 跨平台
- 跨体系架构
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序:这一点和事件驱动一样,可以使用异步IO操作来解决
https://www.zhihu.com/question/32218874
协程方便是因为“用代码来表示状态,而不是维护一坨数据结构来表示状态”,则客观得多。
作者:Tim Shen
链接:https://www.zhihu.com/question/32218874/answer/56255707
来源:知乎
著作权归作者所有,转载请联系作者获得授权。 讲点历史吧。 早期的Unix时代崇尚同步编程,当时设计的时候也没多想,管他什么性能不性能,先做出来再说。所以最开始人是开多进程(process)再用管子(pipe)连接起来的;多直观啊,我把东西推进管子里,你走你那头接一下。 不知道哪天人注意到开个千儿八百个进程机器就卡死了,这怎么行,所以我们只搞一个进程,里面有一坨打开的网络连接和文件,用select这个系统调用对io事件同时进行监听,谁先来我就处理谁。然而发现性能也不好,没什么卵用。 人们在这两条路上都想办法提升性能。进程这边人们又搞了多线程,结果还是不够。多线程还是吃内核资源,抗不住。 Linux忍不住了,搞出个epoll(不过我不知道和bsd的kqueue哪个在前)系统调用,就是红黑树改良版的select。这下子人开心了,真他妈快啊,开千儿八百个连接小意思,同时进行事件监听,判断连接的的id和事件的类型,再打(dispatch)到不同的回调函数里。 不知是不是市场原因,正巧这时候互联网开始火了,性能越好赚钱越多嘛,所以肯定来这套高效的接口,管他好不好用呢,反正俺们吃苦耐劳,外加抖M,才不怕手动维护状态。你多线程开太多最后不也是要手动维护状态。 不熟悉操作系统的开发人员们只知道有什么用什么,也没去多想怎么优化多线程。现在多线程优化出来了,叫协程,调度器有的有(比如go的用户态调度器)有的没有(比如yield),取决于怎么用。其内存开销仍然比异步回调要大(一个协程一个栈,而异步回调的话一个event loop一共就一个栈),但是现在内存也是便宜了,不算是瓶颈。人最关注的是“千万不能堵着(block)”,要千方百计让CPU转起来,这样concurrency才能上去。而到处乱开协程(因为比线程便宜啊)就能达到这个效果,开十万个协程内存也没爆炸,跑上24个就能把CPU打满。所以异步回调的这个优势已经没了。
至于趋势什么的,也就赶个时髦吧,这些枝叶伎俩学学也要不了几个小时。如果是担心要花很多精力学习某个“技术”,又怕学了没用,故有“是不是趋势啊”这一问,那多半是根基不牢,打打基础就行了。
我是写C++的,我发现我唯一用callback/closure/lambda的地方是我有个函数,但是我想把这个函数扣个洞,让用户来填这个洞。用得还真不算多。
注:之前想学Java的时候,花了很多时间来了解趋势。现在想想,的确是瞻前顾后呀,学一下就知道趋势了,哈哈。
------另一个人的回答---------
有时候总觉得 gevent(python的一个库) 这类协程库是被 python 的 GIL 逼出来的,如果原生线程支持足够好,协程的必要性可能并不一定很大。
协程最早来自高性能计算领域的成功案例,协作式调度相比抢占式调度而言,可以在牺牲公平性时换取吞吐。
Node.js需要按顺序执行异步逻辑时一般采用后续传递风格,也就是将后续逻辑封装在回调函数中作为起始函数的参数,逐层嵌套。这种风格虽然可以提高CPU利用率,降低等待时间,但当后续逻辑步骤较多时会影响代码的可读性,结果代码的修改维护变得很困难。根据这种代码的样子,一般称其为"callback hell"或"pyramid of doom",本文称之为回调大坑,嵌套越多,大坑越深。
注:monkey patch可以看:http://www.tuicool.com/articles/2aIZZb
monkey patch指的是在运行时动态替换,一般是在startup的时候. (感觉大多数是Python程序的一种技巧) 用过gevent就会知道,会在最开头的地方gevent.monkey.patch_all();把标准库中的thread/socket等给替换掉.这样我们在后面使用socket的时候可以跟平常一样使用,无需修改任何代码,但是它变成非阻塞的了. 之前做的一个游戏服务器,很多地方用的import json,后来发现ujson比自带json快了N倍,于是问题来了,难道几十个文件要一个个把import json改成import ujson as json吗? 其实只需要在进程startup的地方monkey patch就行了.是影响整个进程空间的. 同一进程空间中一个module只会被运行一次. 下面是代码. main.py import json
import ujson
def monkey_patch_json():
json.__name__ = 'ujson'
json.dumps = ujson.dumps
json.loads = ujson.loads monkey_patch_json()
print 'main.py',json.__name__
import sub
sub.py
import json
print 'sub.py',json.__name__
运行main.py,可以看到都是输出'ujson',说明后面import的json是被patch了的. 最后,注意不能单纯的json = ujson来替换.
1. 用户态栈,更轻量地创建“轻量线程”;
2. 协作式的用户态调度器,更少线程上下文切换;
3. 重新实现 mutex 等同步原语;
协程的创建成本更小,但是创建成本可以被线程池完全绕开,而且线程池更 fine grained,这时相比线程池的优势更多在于开发模型的省力,而不在性能。此外,"轻量线程" 这个名字有一定误导的成分,协程作为用户态线程,需要的上下文信息与系统线程几乎无异。如果说阻碍系统线程 scale 的要素是内存(一个系统线程的栈几乎有 10mb 虚拟内存,线程的数量受虚拟地址空间限制),那么用户态线程的栈如果使用得不节制,也需要同量的内存。
$ ulimit -s
10240
也就是10M;修改的方法如下:
linux查看修改线程默认栈空间大小 ulimit -s 1、通过命令 ulimit -s 查看linux的默认栈空间大小,默认情况下 为10240 即10M 2、通过命令 ulimit -s 设置大小值 临时改变栈空间大小:ulimit -s 102400, 即修改为100M 3、可以在/etc/rc.local 内 加入 ulimit -s 102400 则可以开机就设置栈空间大小 4、在/etc/security/limits.conf 中也可以改变栈空间大小: #<domain> <type> <item> <value> * soft stack 102400 重新登录,执行ulimit -s 即可看到改为102400 即100M
至于并发量导向的业务,一般也是状态上下文较少的业务,比如推送,这时 callback hell 基本可控,使用协程相比事件循环依然更容易编程,但效益并不显著。
最后尝试总结一下个人的想法:
协程不是趋势,它是一个在历史中被挖掘出来的、对现有问题的一个有用的补充。
适用的场景:
- 高性能计算,牺牲公平性换取吞吐;
- 面向 IO Bound 任务,减少 IO 等待上的闲置,这其实和高性能计算领域内的优势是一致的;
- Generator 式的流式计算;
- 消除 Callback Hell,使用同步模型降低开发成本的同时保留更灵活控制流的好处,比如同时发三个请求;这时节约地使用栈,可以充分地发挥 "轻量" 的优势;
但并不是万灵丹:
- 如果栈使用得不节制,消耗的内存量和系统线程无异,甚至内存管理还不如系统线程(系统线程可以动态地调整虚拟内存,用户线程的 Segmented Stack 方案存在严重的抖动问题,Continous Stack 方案管理不当也会抖动,为了避免抖动则成了空间换时间,而内核在这方面做了多少 heuristic 呢);
- IO Bound 任务可以通过调线程池大小在一定程度上缓解,目标是把 CPU 跑满即可,这点线程池的表现可能不完美,但在业务逻辑这个领域是及格的;
- 此外,一般的 python/ruby 任务并不是严格的 IO Bound,比如 ORM 的对象创建、模版渲染、GC 甚至解释器本身,都是 CPU 大户;单个请求扣去 redis 请求和数据库请求的时间,其它时间是否仍不少呢?
- CPU 上长时间的计算,导致用户线程的调度变差,不能更快地响应,单个请求的平均时间反而可能更长(诚然并发可能更高);然而这在 python 这类 GIL 语言来看并不算劣势,甚至比 GIL 的调度更好,至少 gevent 可以知道各 IO 任务的优先级,而 GIL 的调度是事实上的 FIFO;
References
- Xiao-Feng Li: Thread mapping: 1:1 vs M:N
- http://thread.gmane.org/gmane.comp.lang.rust.devel/6479