在图像超清项目中,服务端的耗时优化是通过gevent的协程,进而服务端进行远端的并发调用完成的。因此对服务端的协程进行了较为深入的理解,在此进行总结和整理,方便后面对此方面知识的使用。
python语言因为进程锁的原因导致多线程被锁死,尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。 实际上,解释器被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。 GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势 (比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行)。
在讨论普通的GIL之前,有一点要强调的是GIL只会影响到那些严重依赖CPU的程序(比如计算型的)。 如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用多线程就很合适, 因为它们大部分时间都在等待。实际上,你完全可以放心的创建几千个Python线程, 现代操作系统运行这么多线程没有任何压力,没啥可担心的。
解决全局锁的方式 1、使用进程而非线程来实现多任务加速;2、借助Ctypes使用c语言处理计算,规避GIL的限制。深入的可见:Python的全局锁问题
1、进程、线程与协程
Python脚本的执行效率一直来说并不是很高,特别是Python下的多线程机制,长久以来一直被人们诟病。很多人都在思考如何让Python执行的更快一些,其中典型的方式包括:
- 将复杂的代码转由C语言完成
- 多进程并发执行
- 多线程完成IO操作
除此之外。还可以通过Gevent的协程机制来提高python程序。在理解Gevent之前,我们需要弄明白几个基本的概念:进程(Process)、线程(Thread)、协程(Coroutine)、计算机IO方式等。
协程
进程和线程都是操作系统中的模型,操作系统通过进程和线程这两种模型来执行程序。进程是操作系统分配资源(如CPU、内存等)和调度的基本单位,可以将其看作是包含系统分配的资源和执行流两部分,通过进程模型,操作系统可以灵活地管理程序的执行。线程是执行流,一般而言一个进程只包含一个执行流,也就是说一个进程只包含一个线程。但是通过线程模型,一个进程可以拥有多个执行流,进而提供程序的处理能力。
协程coroutine其实是corporate routine的缩写,简称为协程。在Linux中线程是轻量级的进程,因此也将协程coroutine称为轻量级的线程,又称为微线程。协程简单的理解就是程序的执行流,在暂停和再次执行的过程中可以记录当前的状态,在暂停后需要再次执行时可以从暂停前的状态继续执行。协程暂停执行时,可以调度到另一个协程执行,这两个协程之间的关系是对等的。
协程和生成器generator的概念很像,生成器也可以保存当前执行状态并再次运行时恢复之前的状态,不过区别在于协程暂停时可以调度到另一个协程执行,而生成器暂停时会由它的调用者继续执行。 python中的generator与yiled确实是这样的工作状态。
协程的调度由使用者所决定,而不像进程和线程那样由操作系统进行调度,Gevent中的协程在暂停时会执行一个称为Hub的协程,由Hub选择处于等待执行状态的协程继续执行流。
线程是抢占式的调度,多个线程并行执行时抢占共同的系统资源,而协程则是协同式的调度。其实Greenlet并非一种真正的并发机制,而是在同一线程内的不同函数的执行代码块之间进行切换,实施“你运行一会儿,我运行一会儿”,在进行切换时必须制定何时切换以及切换到哪儿。
进程和协程
下面对比一下进程和协程的相同点和不同点:
相同点:相同点存在于,当我们挂起一个执行流的时,我们要保存的东西:
- 栈, 其实在你切换前你的局部变量,以及要函数的调用都需要保存,否则都无法恢复
- 寄存器状态,这个其实用于当你的执行流恢复后要做什么
而寄存器和栈的结合就可以理解为上下文,上下文切换的理解:CPU看上去像是在并发的执行多个进程,这是通过处理器在进程之间切换来实现的,操作系统实现这种交错执行的机制称为上下文切换
在任何一个时刻,操作系统都只能执行一个进程代码,当操作系统决定把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从它上次停止的地方开始。
不同点:
- 执行流的调度者不同,进程是内核调度,而协程是在用户态调度,也就是说进程的上下文是在内核态保存恢复的,而协程是在用户态保存恢复的,很显然用户态的代价更低
- 进程会被强占,而协程不会,也就是说协程如果不主动让出CPU,那么其他的协程,就没有执行的机会。
- 对内存的占用不同,实际上协程可以只需要4K的栈就足够了,而进程占用的内存要大的多
- 从操作系统的角度讲,多协程的程序是单进程,单协程
线程和协程
既然我们上面也说了,协程也被称为微线程,下面对比一下协程和线程:
- 线程之间需要上下文切换成本相对协程来说是比较高的,尤其在开启线程较多时,但协程的切换成本非常低。
- 同样的线程的切换更多的是靠操作系统来控制,而协程的执行由我们自己控制。
协程只是在单一的线程里不同的协程之间切换,其实和线程很像,多线程是在一个进程下不同的线程之间做切换,这也可能是协程称为微线程的原因吧。
计算机IO方式
python里面的多线程也是在节约IO操作的时间,具体可见python Gevent
Gevent
Gevent是一种基于协程的Python网络库,使用Greenlet提供并封装了libevent事件循环的高层同步API,使开发者在不改变编程习惯的同时,以同步的方式编写异步IO代码。简单来说,Gevent是基于libev和Greenlet的一个异步的Python框架。
Libev
libev是一个高性能的事件循环event loop实现。事件循环(IO多路复用)是解决阻塞问题实现并发的一种方式。事件循环event loop会捕获并处理IO事件的变化,当遇到阻塞时就会跳出,当阻塞结束时则会继续。这一点依赖于操作系统底层的select函数及其升级版poll和epoll。而Greenlet则是一个Python的协程管理和切换的模块,通过Greenlet可以显式地在不同的任务之间进行切换。
Gevent的基本原理来自于libevent&libev,熟悉C语言的同学应该对这个lib不陌生。本质上libevent或者说libev都是一种事件驱动模型。这种模型对于提高CPU的运行效率,增强用户的并发访问非常有效。但因为其本身是一种事件机制,所以编写起来有些绕,并不是很直观。因此为了修正这个问题,有人引入了用户态上下文切换机制,也就是说,如果代码中引入了带IO阻塞的代码时,lib本身会自动完成上下文的切换,全程用户都是没有察觉的,这又是Gevent的由来。
Libev是高性能事件循环模型的网络库,包含大量新特性,是继libevent之后的一套全新的网络库。libev追求的目标是速度更快、bug更少、特性更多、体积更小。和libevent类似,可以作为其替代者,提供更高的性能且无需复杂的配置。
libev机制提供了对指定文件描述符发生时调用回调函数的机制,libev是一个事件循环器,向libev注册感兴趣的事件,比如Socket可读事件,libev会对所注册的事件的源进行管理,并在事件发生时出发相应的程序。
Yield
Python对协程的支持是非常有限的,使用生成器generator中的yield可以一定程序上实现协程。比如传统的生产者-消费者模型,即一个线程写消息一个线程读消息,通过锁机制控制队列和等待,但一不小心就可能出现死锁。如果改用协程,生产者生产消息后直接通过yield跳转到消费者并开始执行,等消费者执行完毕后再切换回生产者继续生产,这样做效率极高。
详细的协程介绍亦可参见:python协程
常见代码应用:
1、相同函数做并发
gevent_threads = []
for i in range(len(boblist)):
bbox, bob = bboxes[i], boblist[i]
gevent_threads.append(gevent.spawn(xxx_func, bbox, bob))
gevent.joinall(gevent_threads)
for thread in gevent_threads:
if not thread.successful():
raise thread.exception
beng, shaka, laka = thread.value
another_list.append((beng, shaka, laka))
2、不同的函数在不同协程内做并发
gevent_threads = []
gevent_threads.append(gevent.spawn(self.func1, buff1))
gevent_threads.append(gevent.spawn(self.func2, buff2))
gevent.joinall(gevent_threads)
if not gevent_threads[0].successful():
raise gevent_threads[0].exception
res0, scale0, diff0 = gevent_threads[0].value
if not gevent_threads[1].successful():
raise gevent_threads[1].exception
res1, out1 = gevent_threads[1].value