###【同步异步、阻塞非阻塞的区别】
同步与异步
同步和异步关注的是消息通信机制。所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。
或许可以这么理解,阻塞和非阻塞,应该描述的是一种状态,同步与非同步描述的是行为方式。
1、同步阻塞,代码执行到某一行时暂停在这里等待结果,并且执行这行代码的当前线程/进程被挂起,让出 CPU,然后在未来某个时刻,由其它线程触发事件通知和回调,告知结果。
2、同步非阻塞,代码执行到某一行时暂停在这里等待结果,但执行这行代码的当前线程/进程不被挂起,继续执行其它操作,然后在未来某个时刻,由其它线程触发事件通知和回调,告知结果,这种模型典型的代表是协程。
3、异步非阻塞,代码执行即不会暂停在某一行,也不会挂起当前线程/进程,然后在未来某个时刻,由其它线程触发事件通知和回调,告知结果。
4、异步阻塞,这种则极少有
最近又理解了一下:所谓的异步最终会转化为某种形式的同步,也就是消息或者回调。 消息形式的异步一般是进程之间的通信,而回调本身是一种同步形式,只是采用快响应、慢处理的策略,推后了这个同步的时间。不知道理解的是否正确。
###【异步非阻塞】
I/O复用
1.Epoll异步非阻塞
epoll技术的编程模型就是异步非阻塞回调,也可以叫做Reactor,事件驱动,事件轮循(EventLoop)。
Nginx,libevent,node.js这些就是Epoll时代的产物。
2.Coroutine协程
接着又有一个问题,异步嵌套回调太TM难写了。尤其是Node.js层层回调,缩进了几十层,要把程序员逼疯了。于是一个新的技术被提出来了,那就是协程(coroutine)。这个技术本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。程序员就像写阻塞代码一样简单。比如调用 client->recv() 等待接收数据时,就像阻塞代码一样写。实际上是底层库在执行recv时悄悄保存了一个状态,比如代码行数,局部变量的值。然后就跳回到EventLoop中了。什么时候真的数据到来时,它再把刚才保存的代码行数,局部变量值取出来,又开始继续执行。
这个就像时间禁止的游戏一样,国王对巫师说“我必须马上得到宝物,不然就砍了你的脑袋”,巫师念了一句时间停止的咒语,直到过了1年后勇士们才把宝物送来。这时候巫师解开咒语,把宝物交给国王。这里国王就可以理解成协程,他根本没感觉到时间停止,在他停止到醒来期间发生了什么他不知道,也不关心。
这就是协程的本质。协程是异步非阻塞的另外一种展现形式。Golang,Erlang,Lua协程都是这个模型。
###【同步阻塞】
再回到同步阻塞这个话题,不知道大家看完协程是否感觉得到,实际上协程和同步阻塞是一样的。答案是的。所以协程也叫做用户态进/用户态线程。区别就在于进程/线程是操作系统充当了EventLoop调度,而协程是自己用Epoll进行调度。
协程的优点是它比系统线程开销小,缺点是如果其中一个协程中有密集计算,其他的协程就不运行了。操作系统进程的缺点是开销大,优点是无论代码怎么写,所有进程都可以并发运行。
Erlang解决了协程密集计算的问题,它基于自行开发VM,并不执行机器码。即使存在密集计算的场景,VM发现某个协程执行时间过长,也可以进行中止切换。
实际上同步阻塞程序的性能并不差,它的效率很高,不会浪费资源。当进程发生阻塞后,操作系统会将它挂起,不会分配CPU。直到数据到达才会分配CPU。多进程只是开多了之后副作用太大,因为进程多了互相切换有开销。所以如果一个服务器程序只有1000左右的并发连接,同步阻塞模式是最好的。
###【不同语言的异步非阻塞实现】
Erlang和Go受CSP[1]模型启发。
Erlang的并发编程是actor模型,通过进程(process)间发消息。
Go大体上是走了抄erlang的路子,不过引入了coroutine(goroutine)和channel的概念,简单的来说就是一个我等待IO的时候控制权交给你这样的行为,channel负责调度和通讯。
Go是用channel来共享内存(Share Memory By Communicating),状态可变的(mutable)。
erlang进程间是隔离的,状态是不可变的(immutable)。
目前的情况,erlang的并发更强大成熟一些。
而传统意义上python/ruby的并发是线程,以前ruby不是native的,现在也是native的了,GIL负责大颗粒的线程安全,但是,目前流行的做法却还是coroutine这类的实现。
但由于解释器先天限制,Cpython能做到的也就是差不多而已,因为缺乏channel,因此有一个调度中心负责干这事,等于就是说我等待IO的时候控制权给调度中心调度中心再去给你控制权这样。
###【其他问题】
1、异步回调和协程哪个性能好?
协程虽然是用户态调度,实际上还是需要调度的,既然调度就会存在上下文切换。所以协程虽然比操作系统进程性能要好,但总还是有额外消耗的。而异步回调是没有切换开销的,它等同于顺序执行代码。所以异步回调程序的性能是要优于协程模型的。这里是指Nginx这种多进程异步非阻塞程序。Node.js/Redis此类程序如果不开多个进程,由于无法利用多核计算优势,所以性能并不好。
2、在Django 的视图中起一个while循环,其他的视图会拥塞吗,其他的框架如flask,tornada等的情况又是如何 ?
单进程的全部会阻塞(包括 Tornado),除非 wsgi server 用了某些肮脏手法进行了抢占(gevent 应该只会在 I/O 上激活调度,所以一样没用)。
多线程的(如 flup)仍然可以执行其他线程。
多进程的同多线程。
Tornado 是一个 server,所以讨论是有意义的。
Django / Flask 只是 web 框架,wsgi 执行模型交给下面的引擎,所以与这个问题是无关的。
可以讨论的对象是:Gunicorn, Gevent-wsgi / gevent-pywsgi, Flup。
如果是阻塞当前请求,while当然会阻塞当前的请求,所以耗时的操作通常会进行异步处理。
如果是阻塞其它请求,取决于web服务器等配置,如当前的请求数量已经达到web服务器最大的处理请求数量,则会阻塞其它请求,否则不会。
###【python的异步非阻塞实现】
twisted、gevent、tornado、asyncio
异步的编码方式——无论是 Tornado 的回调函数,还是 Twisted 的 Deferred,借助 Python 的 generator 功能,Twisted 和 Tornado都实现了用同步的方式来编写异步执行的代码。
这种需要显式地写 yield 的代码叫做显式的异步切换。
而Gevent 就是隐式的异步切换。
隐式的异步切换有个好处很明显,就是可以很容易地将阻塞式的遗留代码迁移到 Gevent 上来,而不需要额外修改大量代码,这对于需要异步并发支持的许多大型现有项目来说,无疑是为数不多的几个选择之一——比如说 Django。
但隐式的异步切换也有代价——倒不是说它的性能有多差,而是这种写法把异步切换隐藏的太深了,不知道什么时候就切换到别的地方去执行了。很难保证在一段程序的执行过程中,某些本地状态不会被别的代码修改。
Gevent 之所以能实现隐式的异步切换,主要归功于 Greenlet。
Greenlet 是 Stackless Python 的一个分项目,用于在标准 CPython 中实现微线程coroutine(也称协程、绿色线程)。
这个greenlet可以理解为就和go的goroutine。
greenlet 和 generator、Deferred 一样,其实都是用来实现回调封装的一些工具。
gevent和tornado都是基于greenlet协程库实现的异步事件框架。
asyncio 这个项目其实叫做 tulip。
新语法:yield from,和yield类似,将内层迭代器的元素无缝地合并到外层的迭代器里,很容易地做微线程的嵌套,也就是在一个微线程里面等待另一个结束返回结果。
asyncio 作为又一个异步并发框架,与其他现有框架差别并不大:主循环类似于 Twisted 的 reactor,Future 对回调函数进行封装类似于 Deferred,可选的微线程类似于 inlineCallbacks,基于 yield from 的显式的异步切换类似于 yield。
在Py3.5中asyncio语法有了优化。
###【Python 的多线程】
在python的原始解释器CPython中存在着GIL(Global Interpreter Lock,全局解释器锁),因此在解释执行python代码时,会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数达到一定数目时才会释放GIL。
所以,虽然CPython的线程库直接封装了系统的原生线程,但CPython整体作为一个进程,同一时间只会有一个获得GIL的线程在跑,其他线程则处于等待状态。这就造成了即使在多核CPU中,多线程也只是做着分时切换而已。
不过muiltprocessing的出现,已经可以让多进程的python代码编写简化到了类似多线程的程度了。
但系统资源过的消耗在进程线程切换上面,为什么不用异步并发框架?2.x 系列里我们可以使用 gevent 啊,在 3.x 系列的标准库里又有了 asyncio。对爬虫来说异步框架的连接池是个大杀器。
多线程在 Python 里就是个鸡肋。尤其实在 3.x 系列里。
Python解释器由于设计时有GIL全局锁Global Interpreter Lock,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。但可以通过多进程实现多核任务。
###【Python 标准库线程安全的队列是哪一个?不安全的是哪一个?logging是线程安全的吗? 】
线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。
也就说没有互斥锁同步的队列不安全。
logging,在一个进程内的多个线程同时往同一个文件写日志是安全的。但是(对,这里有个但是)多个进程往同一个文件写日志不是安全的。