协程(微线程)
一种用户态的轻量线程。
拥有自己的寄存器和上下文栈。协程调度切换的时候,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器的上下文和栈。因此协程能调用上一次调用时的状态(即所有的一个局部状态的一个特定组合),每次过程重入时,相当于进入上一次调用的状态,就是进入上一次离开所处的逻辑流的位置。
协程的好处
1.无需线程上下文切换的开销
2.无需原子操作锁定及切换的开销
3.方便切换控制流,简化编程模型
4.高并发+高扩展性+低成本:一个cpu支持上万的协程都没有问题,所以很适用与高并发处理。
缺点:
1.无法利用多核资源:协程的本质是单线程,它不能同时将单个cpu的多个核用上,协程需要和进程配合才能运行在多cpu上,当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
2.进行阻塞操作(如i0),会阻塞掉整个程序1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28# Author: Diedline
import time
import queue
"""
yield 实现最简单的协程
"""
def consumer(name):
print("--->starting eating baozi...")
while True:
new_baozi = yield
print("[%s] is eating baozi %s" % (name, new_baozi))
# time.sleep(1)
def producer():
r = con.__next__()
r = con2.__next__()
n = 0
while n < 5:
n += 1
con.send(n)
con2.send(n)
time.sleep(1)
print("\033[32;1m[producer]\033[0m is making baozi %s" % n)
if __name__ == '__main__':
con = consumer("c1")
con2 = consumer("c2")
p = producer()
使用greenlet实现手动切换两个协程(相当于手动挡的汽车)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# Author: Diedline
from greenlet import greenlet
"""
用switch() 手动切换这两个协程
"""
def test1():
print(12)
gr2.switch()
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch()
print(78)
gr1 = greenlet(test1) #启动一个携程
gr2 = greenlet(test2)
gr1.switch()
gevent是一个第三方库,可以轻松的通过gevent实现并发同步或者异步编程,在gevent中用到的主要模式是greenlet,它是以c拓展模块接入python的轻量级协程.Greenlet全部运行在主程序操作系统进程的内部,但他们被协作式的调度。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27# Author: Diedline
import gevent
"""
整体程序还是需要2s来执行的,由最长的io来决定的
"""
def foo():
print('Running in foo')
gevent.sleep(2)
print('Explicit context switch to foo again')
def bar():
print('Explicit精确的 context内容 to bar')
gevent.sleep(1)
print('Implicit context switch back to bar')
def func3():
print("running func3 ")
gevent.sleep(0)
print("running func3 again ")
gevent.joinall([ #启动了三个协程 自动切换io
gevent.spawn(foo), #生成,
gevent.spawn(bar),
gevent.spawn(func3),
])
使用gevent实现简单协程的爬虫1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34# Author: Diedline
from urllib import request
import gevent
import time
"""
为什么使用协程速度没有变快?
因为gevent 是无法检测urllib的
所以相当于是一直阻塞的就相当于是串行的
"""
from gevent import monkey
monkey.patch_all()
"""
上面这两行相当于给gevent打了补丁就能检测到urllib的io了所以速度大大提升了
"""
def f(url):
print("Get :%s"% url)
resp = request.urlopen(url)
data = resp.read()
f = open("url.html","wb")
f.write(data)
f.close()
print("%d bytes receive from %s"%(len(data), url))
async_time_start = time.time()
gevent.joinall([
gevent.spawn(f,"https://diedline.github.io/"),
gevent.spawn(f,"https://diedline.github.io/archives/"),
gevent.spawn(f, "https://www.python.org/")
]
)
print("异步运行时间:",time.time()-async_time_start)
论事件驱动与异步io
通常我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程来处理该请求。
(2)每收到一个请求,创建一个新的线程来处理该请求。
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞io方式来处理请求
上面的几种方法,各有千秋:
第(1)种方法,由于创建新的进程开销比较大,所以会导致服务器性能比较差,但实现比较简单。
第(2)种方法,涉及到线程的同步可能会面临死锁等问题。
第(3)种方式,在写应用程序代码的时候,逻辑比前面两种都复杂。
综合考虑各种因素,一般普遍认为第三种方式是大多数网络服务器所采用的的方法。
在ui编程中常常要对鼠标点击进行相应检测,那么该如何进行检测鼠标点击呢?
方式一:创建一个线程,线程一直循环检测鼠标是否点击,那么这个方式有以下几种缺点:
1:cpu资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这样会造成很多的cpu资源的浪费,如果扫描鼠标点击的接口是阻塞的呢?
2:如果是阻塞的那么又会出现以下问题,如果我们不但要扫描鼠标点击还要扫描键盘是否按下,由于扫描鼠标时被阻塞了,那么永远的不可能去扫描键盘;
3:如果一个循环需要的扫描设备非常多,那么又会引来响应时间的问题。
方式二:
事件驱动模型(就是根据一个事件做反应)
目前大部分的ui编程都是事件驱动模型,如很多ui平台都会提供onClicck()事件。
事件驱动模型大体思路如下:
1.有一个事件(消息)队列。
2.鼠标按下时,往这个队列中增加一个点击事件(消息)
3.有个循环不断从队列中取出事件,根据不同的事件调用不同的函数,如onClick(),
onKeyDown()等。
4.事件(消息)一般都各自保存各自的处理函数指针,这样每个消息都有独立的处理函数。
事件驱动模型是一种编程范式,这里的程序执行流由外部事件来决定,它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。