目录
进程:
关于进程的理解:
进程的创建(进程间全局变量)
自定义进程:
进程池(阻塞式和非阻塞式-多进程):
队列:Queue
进程通信(queue):
线程:
对线程的简单理解:
线程优点:
关于线程的使用方法:
如何创建并使用线程?
线程(共享全局变量):
线程(锁问题):
多线程同步问题:
线程(死锁问题):
生产者和消费者问题(两线程间的通信):
协程:
协程的优点:
协程的实现(yield):
协程的实现(greenlet):
协程的实现(gevent+猴子补丁):
协程模拟下载多个任务:
进程:
关于进程的理解:
多任务概念:
生活中,你可能一边听歌,一边写作业;一边上网,一边吃饭... 这些都是生活中的多任务场景。 电脑也可以执行多任务,比如你可以同时打开浏览器上网,听音乐,打开pycharm编写代码......
简单的说**多任务就是同一时间运行多个程序**
单核和多核:
--单核CPU实现多任务原理:操作系统轮流让各个任务交替进行,QQ执行2us,切换到微信,再执行2us,再切换到陌陌,执行2us...。 表面是看,每个任务反复执行下去,但是CPU调度执行速度太快了,导致我们感觉所以任务都在同时执行一样。
--多核CPU实现多任务原理:真正的并行执行多任务只能在多核CPU上实现,但是由于任务数量远远多于CPU的核心数量, 所以,操作系统也会自动把很多任务调度到每个核心上执行。
实现多任务的方式:
--多进程模式
--多线程模式
--协程
进程 > 线程 > 协程
大致上来说,进程是线程的容器,是程序的实体;进程是分配空间,线程是分配时间。
进程优点:
稳定性高,一个进程崩溃了不会影响其他进程
进程缺点:
1.创建进程开销巨大
2.操作系统能同时运行的进程数目有限
在linux下可以使用fork()函数创建进程;在windows系统上可以引用multiprocessing模块,创建进程,我们可以使用multiprocessing模块中Process类创建新的进程。python里的fork()在os模块里。
进程的创建(进程间全局变量)
from multiprocessing import Process #导入模块
process = Process(target = 函数,name = 进程名,args = (给函数传递的参数))
process.start() #启动进程并执行任务
process.run() #只是执行了任务没有启动进程
process.join() #起到堵塞的作用,完成上面的任务后才可以往下进行
terminate() #终止
os模块里的getpid()可以获取当前进程号。
###举例说明
from multiprocessing import Process # 使用该模块创建进程
from time import sleep
import os
m = 1 # 不可变类型
def task1(s, name):
global m
while True:
m += 1
sleep(s)
print('这是任务1...', os.getpid(), m) # 两进程并不共享全局变量m,相当于每个进程单独拥有一份m
def task2(s, name):
global m
while True:
m += 1
sleep(s)
print('这是任务2...', os.getpid(), m)
if __name__ == '__main__':
number = 0
print(os.getpid())
# 子进程
p = Process(target=task1, name='任务1', args=(1, 'aa'))
p.start()
print(p.name) # 打印动作在主进程里
p1 = Process(target=task2, name='任务2', args=(2, 'bb'))
p1.start()
print(p1.name) # 打印动作在主进程里
输出:
7368 任务1 任务2 这是任务1... 15988 2 这是任务2... 1692 2 这是任务1... 15988 3 这是任务1... 15988 4 这是任务2... 1692 3 这是任务1... 15988 5 这是任务1... 15988 6 这是任务2... 1692 4 这是任务1... 15988 7 这是任务1... 15988 8 这是任务2... 1692 5 这是任务1... 15988 9 这是任务1... 15988 10 ... ... ...
1.该输出条件没有限制,将一直输出。
2.主进程里声明了整型变量m,在子进程里的m仅共享初始化的m,值为1;而后对m的操作并不共享,说明在进程里的变量具有独立性。
自定义进程:
当父类进程里的内容不满足需求时,我们可以自定义进程。
1.继承主进程(super的使用)
2.重写run()方法
3.创建进程对象并启动(start)进程,start的调用会默认执行自定义进程里的run()函数
###举例说明
from multiprocessing import Process
from time import sleep
class MyProcess(Process): # 继承了主进程
def __init__(self,name):
super(MyProcess,self).__init__()
self.name = name
# 重写run方法
def run(self):
n = 1
while True:
sleep(1)
print('{}____自定义进程:{}'.format(n,self.name))
n += 1
if __name__ == '__main__':
p = MyProcess('小明')
p.start()
p1 = MyProcess('小红')
p1.start()
输出:
1____自定义进程:小明 1____自定义进程:小红 2____自定义进程:小明 2____自定义进程:小红 3____自定义进程:小明 3____自定义进程:小红 4____自定义进程:小明 4____自定义进程:小红 ... ... ...
以上代码仅简单作一个自定义进程,不做过多解释;其中输出没有设置限制条件,故会一直执行;要注意定义进程类时继承主进程,看情况写初始化函数 __init__(),需重写run()方法。
进程池(阻塞式和非阻塞式-多进程):
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态生成多个进程, 但是如果是成百甚至上千个目标时,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块里的(进程池)Pool方法。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满 那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待, 直到池中有进程结束,才会创建新的进程来执行。
1.进程池中的进程在创建的时候就已经分配好了进程号,任务的迭代不影响进程号的改变。
2.进程池的存活依赖于主进程,同生共死。
非阻塞式:全部添加到队列中,立刻返回,并不等待其他进程执行完毕才结束, 但是回调函数是等待任务完成之后才会调用执行。
pool = Pool(5) #创建了可容纳5个进程的进程池
pool.apply_async(func=函数名,args=(参数,),callback=回调函数名) #当函数参数只有一个,在参数后面加逗号
非阻塞式标志是apply_async;callback是回调函数的意思,等待任务完成后执行。
###举例说明
import os
import time
import random
from multiprocessing import Pool
#非阻塞式进程
def task(task_name):
print('开始做任务啦!',task_name)
start = time.time() #算出当前时间
#使用sleep()
time.sleep(random.random()*2)
end = time.time()
return '完成任务{} 用时:{},进程ID:{}'.format(task_name,end-start,os.getpid())
container = []
def callback_func(n):
container.append(n)
if __name__ == '__main__':
#创建了容量为5的进程池,在创建的时候就已经分配好了进程号,任务的迭代不影响进程号的改变
pool = Pool(5) #进程池的存活依赖于主进程,同生共死
tasks = ['LOL','洗衣服','吃饭','遛狗','听歌','睡觉']
for i in tasks:
#进程池里可装5个进程,但是有6个任务所以最后一个任务需要等其他进程结束才能开始,需要排队
pool.apply_async(func=task,args=(i,),callback=callback_func) #使用非阻塞模式(非同步);callback为回调,当func执行完毕后,return的东西会给到回调函数callback
pool.close() #添加任务结束;关闭进程池,不再接收新的请求。
pool.join() #堵住主进程,必须在完成任务后才能往下进行
for i in container:
print(i)
print('OVER!')
输出:
开始做任务啦! LOL 开始做任务啦! 洗衣服 开始做任务啦! 吃饭 开始做任务啦! 遛狗 开始做任务啦! 听歌 开始做任务啦! 睡觉 完成任务LOL 用时:0.12997722625732422,进程ID:19996 完成任务遛狗 用时:0.11489439010620117,进程ID:18380 完成任务吃饭 用时:0.5681507587432861,进程ID:19520 完成任务洗衣服 用时:0.6339731216430664,进程ID:980 完成任务睡觉 用时:1.4290874004364014,进程ID:19996 完成任务听歌 用时:1.6031692028045654,进程ID:6704 OVER!
代码解读:
在主进程里我们申请了容量为5的进程池,利用for循环的方式给这些进程分配任务,一共有六个任务,所以最后一个任务需要先等待,等到有进程把一个任务执行完成,再去执行最后的任务。故在该输出中,进程19996存在两次,执行了两个任务。并且在非阻塞式下,可先将任务全部添加到等待队列中,分配进程资源执行任务,同时进行;callback为回调,当func执行完毕后,return的东西会给到回调函数callback。
阻塞式:
添加一个任务,然后再执行一个任务,如果一个任务不结束,其他任务进不来。
pool = Pool(max) #创建进程池对象
pool.apply_async(func=函数名,args=(参数,)) #非阻塞的
pool.close() #停止添加进程
pool.join() #堵住主进程,必须在完成任务后才能往下进行
###举例说明###
import os
import time
import random
from multiprocessing import Pool
def task(task_name):
print('开始做任务啦!',task_name)
start = time.time()
#使用sleep()
time.sleep(random.random()*2)
end = time.time()
print('完成任务{} 用时:{},进程ID:{}'.format(task_name,end-start,os.getpid()))
if __name__ == '__main__':
pool = Pool(5)
tasks = ['LOL', '洗衣服', '吃饭', '遛狗', '听歌', '睡觉']
for i in tasks:
pool.apply(func=task, args=(i,)) # 使用阻塞模式,没有回调函数;一人任务结束后才加入下一个任务
pool.close()
pool.join()
输出:
开始做任务啦! LOL 完成任务LOL 用时:1.8951187133789062,进程ID:13080 开始做任务啦! 洗衣服 完成任务洗衣服 用时:0.6655244827270508,进程ID:19708 开始做任务啦! 吃饭 完成任务吃饭 用时:1.7410509586334229,进程ID:19284 开始做任务啦! 遛狗 完成任务遛狗 用时:1.5534477233886719,进程ID:5752 开始做任务啦! 听歌 完成任务听歌 用时:0.3398125171661377,进程ID:11576 开始做任务啦! 睡觉 完成任务睡觉 用时:1.365257740020752,进程ID:13080
由于是阻塞式下,依次从进程池中取出进程来执行任务task(),第一个任务没有完成下,不能启动第二个任务,也无法分配进程。故使用阻塞式的意义不大。
队列:Queue
在说进程间的通信之前,让我们先看看多进程模块下的Queue类吧!
put() #向队列里放入数据,可设置超时时间进行等待
get() #向队列里取出数据,可设置超时时间进行等待
put_nowait() #向队列里放入数据,不等待
get_nowait() #向队列里取出数据,不等待
empty() #判断队列是否为空
q = Queue(5) # 队列容量
# 向队列里添加元素
q.put('A')
q.put('B')
q.put('C')
q.put('D')
q.put('E')
print(q.qsize())
# timeout表示2秒后报异常
if q.full() == False: # full判断队列是否为满 empty判断队列是否为空
q.put('A', timeout=2) # 自动阻塞,当加入时超过最大容量只能等待队列里的元素被取走
else:
print('队列已满!')
# 获取队列的值
print(q.get()) # 当队列为空时还在取元素,会阻塞
print(q.get())
print(q.get())
print(q.get())
print(q.get())
输出:
5 队列已满! A B C D E
get(),put()为阻塞式,队列为空或为满时等待 get_nowait,put_nowait 为非阻塞式,队列为空或为满时不等待,直接报异常
进程通信(queue):
q = Queue(5) #创建一个容量为5的队列
p1 = Process(target = ,args = (q,))
简单的理解 Queue 实现进程间通信的方式,就是使用了操作系统给开辟的一个队列空间, 各个进程可以把数据放到该队列中,当然也可以从队列中把自己需要的信息取走。
from multiprocessing import Process
from time import sleep
def download(q):
images = ['glrl.jpg', 'boy.jpg', 'man.jpg']
for i in images:
print('正在下载{}'.format(i))
sleep(1)
q.put(i)
def getfile(q):
while True:
try:
file = q.get(timeout=5)
print('{}保存成功!'.format(file))
except:
print('全部保存成功!')
break
# 开两个进程,执行下载任务,利用队列来对两进程之间实现通信
if __name__ == '__main__':
q = Queue(5)
p1 = Process(target=download, args=(q,)) # target为待执行任务;注意只写函数名不加括号
p2 = Process(target=getfile, args=(q,)) # 利用args传入队列对象
p1.start()
p1.join() # 将任务全部执行完毕后才可执行下一步
p2.start()
p2.join()
print('!!!!!!')
输出:
正在下载glrl.jpg 正在下载boy.jpg 正在下载man.jpg glrl.jpg保存成功! boy.jpg保存成功! man.jpg保存成功! 全部保存成功! !!!!!!
注意:利用args传入队列对象,对象做参数。
线程:
#进程:Process:进程做任务速度更快
#线程:Thread:线程适合做耗时操作
对线程的简单理解:
对于经常使用电脑的我们,不难发现这样的一个现象,电脑管家里可以,一边杀毒,一边清理电脑垃圾,一边卸载软件。其实这其中就有线程的功劳,我们可以将电脑管家看作一个进程,它做的这些许多任务我们看作线程,这就是一个多线程的例子。
线程多为耗时操作,也称轻量级进程,是程序执行流的最小单元。
线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。
一个线程可以创建或撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪,阻塞,和运行三种基本状态。 每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
多线程(multithreading):是指从软件或者硬件上实现多个线程并发执行的技术,可以在单个程序中利用多个线程完成不同的工作。
线程优点:
1.使用线程可以把占据长时间的程序中的任务放到后台去处理。
2.用户界面可以更加吸引人,比如用户点击了一个按钮触发某些事件的处理,可弹出一个进度条显示处理的速度。
3.程序执行的速度可能加快。
关于线程的使用方法:
thread 和 threading 提供对线程的支持。
thread提供了低级别的,原始的线程以及简单的锁;threading 模块提供其他的方法。
threading.currentThread() #返回当前的线程变量
threading.enumerate() #返回一个包含正在运行的线程list
threading.activeCount() #返回正在运行的线程数量。与len(threading.enumerate())结果相同
如何创建并使用线程?
t1 = threading.Thread(target=函数名,name='线程名',args=(函数参数,))
t1.start()
###使用线程模仿听歌和下载功能###
import threading
from time import sleep
def download(n):
images = ['glrl.jpg','boy.jpg','man.jpg']
for i in images:
print('正在下载{}'.format(i))
sleep(n)
print('{}下载成功!'.format(i))
def listenMusic():
musics = ['大碗宽面','烤面筋','我们不一样']
for music in musics:
print('正在听歌{}'.format(music))
if __name__ == '__main__':
t1 = threading.Thread(target=download,name='jsy',args=(1,))
t1.start()
t2 = threading.Thread(target=listenMusic,name='J')
t2.start()
输出:
正在下载glrl.jpg 正在听歌大碗宽面 正在听歌烤面筋 正在听歌我们不一样 glrl.jpg下载成功! 正在下载boy.jpg boy.jpg下载成功! 正在下载man.jpg man.jpg下载成功!
利用多个线程在同一时间完成不同的任务。
线程(共享全局变量):
线程可以共享全局变量。 利用这一特性,线程可以用来做买火车票的程序。
###举例说明###
import threading
ticket = 1000
def run1():
global ticket
for i in range(100):
ticket -= 1
def run2():
global ticket
for i in range(200):
ticket += 1
if __name__ == '__main__':
# 创建线程
th1 = threading.Thread(target=run1(), name='th1')
th2 = threading.Thread(target=run1(), name='th2')
th3 = threading.Thread(target=run1(), name='th3')
th4 = threading.Thread(target=run2(), name='th4')
th1.start()
th2.start()
th3.start()
th4.start()
th1.join()
th2.join()
th3.join()
th4.join()
print('ticket:', ticket) # 两线程共享变量ticket
输出:
ticket: 900
起初,总票数为1000,经过4个线程的手之后,1000-100-100-100+200=900。由于各线程共享全局变量ticket,所以才能统一对一个变量进行操作,这一点要优于进程。
线程(锁问题):
需要注意的是:
1.python底层只要用线程就会默认加锁—GIL全局解释器锁。
2.加锁实现线程同步,保证了数据安全,但是速度降低;当数据改变量过大时,锁会自动释放,数据不安全,所以线程并不安全,此时需要手动加锁。
###举例说明###(没有手动加锁)
import threading
n = 0
def task1():
global n
for i in range(10000000):
n += 1
print('n的值:', n)
def task2():
global n
for i in range(10000000):
n += 1
print('n的值:', n)
if __name__ == '__main__':
th1 = threading.Thread(target=task1, name='th1')
th2 = threading.Thread(target=task2, name='th2')
th1.start()
th2.start()
th1.join()
th2.join()
print('最后打印{}'.format(n))
输出:
n的值: 12319874 n的值: 12652163 最后打印12652163
注意:此时由于for循环次数太多,数据量改变太大,系统默认加上的锁已然自动释放;这种清况下需要手动加锁。
###举例说明###(手动加锁后)
import threading
n = 0
def task1():
global n
with lock:
for i in range(10000000):
n += 1
print('n的值:', n)
def task2():
global n
with lock:
for i in range(10000000):
n += 1
print('n的值:', n)
if __name__ == '__main__':
lock = threading.Lock()
th1 = threading.Thread(target=task1, name='th1')
th2 = threading.Thread(target=task2, name='th2')
th1.start()
th2.start()
th1.join()
th2.join()
print('最后打印{}'.format(n))
输出:
n的值: 10000000 n的值: 20000000 最后打印20000000
使用上下文的方式加锁,with lock ,更加简便,不用去管获取锁和释放锁的问题。而且在加上锁之后,结果也是正确的。
多线程同步问题:
同步:一个一个的完成,一个做完,另一个才能进来,效率就会降低,而安全性增高。
共享数据:如果多个线程同时对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
多线程的优势在于可以同时运行多个任务(至少感觉像是这样)。 但是当线程需要共享数据时,可能存在数据不同步问题, 为了避免这种情况,引入了锁的概念。
使用Thread对象的Lock和Block可以实现简单的线程同步,这两个对象都有acquire方法和release方法, 对于那些需要每次只允许一个线程操作的数据,可以将其放到acquire和release方法之间,锁只要不释放,其他的线程都无法进入运行状态。
lock = thread.Lock()
lock.acquire() #请求得到锁
lock.release() #释放锁
###举例说明###
---> 可见上文的锁问题案例
线程(死锁问题):
开发过程中使用线程,在线程间共享多个资源的时候, 如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁;尽管死锁很少发生,但一旦发生就会造成应用的停止响应,程序不做任何事情。
避免死锁的方法:
1.重构代码
2.在acquire里加 timeout=
###举例说明###
from threading import Lock, Thread
import time
lockA = Lock()
lockB = Lock()
class MyThread(Thread):
def run(self): # start()
if lockA.acquire(): # 如果成功获取到A锁则返回Ture
print(self.name + '获取了A锁')
time.sleep(1) # 线程1在休息的时候B锁就被线程2获取了,所以无法获取B锁
if lockB.acquire(timeout=2): # 如果2秒得不到B锁,两秒后释放A锁
print(self.name + '又获取了B锁')
lockB.release()
lockA.release()
class MyThread1(Thread):
def run(self): # start()
if lockB.acquire(): # 如果成功获取到B锁则返回Ture
print(self.name + '获取了B锁')
time.sleep(1)
if lockA.acquire():
print(self.name + '又获取了A锁')
lockA.release()
lockB.release()
if __name__ == '__main__':
t1 = MyThread()
t2 = MyThread1()
t1.start()# 默认执行自定义线程里的 run()方法
t2.start()
输出:
Thread-1获取了A锁 Thread-2获取了B锁 Thread-2又获取了A锁
自定义了两个线程:MyThread和MyThread1;先执行t1里的run方法,t1获得了A锁,t1休眠1秒钟,这一秒钟里t2执行run方法,并获取了B锁,然后t2也休眠1秒钟;等到t1醒来时再想获取B锁无法获取,需要等待,等2秒后没有获得B锁,将A锁释放;而t2,对A锁一直处于等待状态,故t1一释放A锁,t2就拿到A锁,程序向下进行,成功避免了死锁。
生产者和消费者问题(两线程间的通信):
Python的queue模块中提供了同步的,线程安全的队列类,包括FIFO(先入先出)队列Queue, LIFO(后入先出-栈)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语 (可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用, 可以使用队列来实现线程间的同步。
###举例说明###
import threading
import queue
import random
import time
def produce(q):
i = 0
while i < 10:
num = random.randint(1, 100)
q.put('生产者产生数据:%d' % num)
print('生产者产生数据:%d' % num)
time.sleep(1)
i += 1
q.put(None)
#完成任务
q.task_done()
def consume(q):
while True:
item = q.get()
if item is None:
break
print('消费者获取到:%s' %item)
time.sleep(2)
#完成任务
q.task_done()
if __name__ == '__main__':
q = queue.Queue(10)
arr = []
# 创建生产者
tp = threading.Thread(target=produce, args=(q,))
tp.start()
# 创建消费者
tc = threading.Thread(target=consume, args=(q,))
tc.start()
tp.join()
tc.join()
print('END!')
输出:
生产者产生数据:95 消费者获取到:生产者产生数据:95 生产者产生数据:26 消费者获取到:生产者产生数据:26 生产者产生数据:25 生产者产生数据:43 消费者获取到:生产者产生数据:25 生产者产生数据:41 生产者产生数据:34 消费者获取到:生产者产生数据:43 生产者产生数据:73 生产者产生数据:69 消费者获取到:生产者产生数据:41 生产者产生数据:68 生产者产生数据:74 消费者获取到:生产者产生数据:34 消费者获取到:生产者产生数据:73 消费者获取到:生产者产生数据:69 消费者获取到:生产者产生数据:68 消费者获取到:生产者产生数据:74 END!
生产者生产10组数据,消费者拿出10组数据;由于是两线程对同一组数据进行操作,可认为实现了线程间的通信。
协程:
进程 > 线程 > 协程
协程理解:相当于电脑管家(进程)里的杀毒功能(线程)中同时查(协程)C,D,E盘中的文件,协程是运行在单线程间的"并发",协程相比多线程一大优势就是省去了多线程之间的切换开销,获得了更大的运行效率。
协程使用:只要出现耗时操作就会切换动作,争取时刻资源都处于利用状态,高效利用CPU
耗时操作:网络操作,网络下载(爬虫),I/O操作(文件读写),堵塞
协程的优点:
1.执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
2.不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。
协程的实现(yield):
Process---Thread---生成器完成(yield)
###举例说明###
def task1():
for i in range(3):
print('A' + str(i))
yield
time.sleep(1)
def task2():
for i in range(3):
print('B' + str(i))
yield
time.sleep(2)
if __name__ == '__main__':
g1 = task1()
g2 = task2()
while True:
try:
next(g1)
next(g2)
except:
break
输出:
A0 B0 A1 B1 A2 B2
利用yield实现暂时阻塞,再next()一个一个取出结果。
协程的实现(greenlet):
使用greenlet完成协程任务 可以更好得进行任务切换。
###举例说明###
import time
from greenlet import greenlet
def a(): # 任务a
for i in range(3):
print('A' + str(i))
gb.switch()
time.sleep(0.1)
def b(): # 任务b
for i in range(3):
print('B' + str(i))
gc.switch()
time.sleep(0.1)
def c(): # 任务c
for i in range(3):
print('C' + str(i))
ga.switch()
time.sleep(0.1)
if __name__ == '__main__':
# 通过手动控制switch,完成ABC依次打印
ga = greenlet(a)
gb = greenlet(b)
gc = greenlet(c)
ga.switch()
输出:
A0 B0 C0 A1 B1 C1 A2 B2 C2
通过设置ga,gb,gc,并对它们进行switch控制,高效实现任务的切换。
协程的实现(gevent+猴子补丁):
geeenlet已经实现了协程,但是这个人工切换,有些麻烦。 python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent。其原理是当一个greenlet遇到IO(指的是input output输入输出,比如网络,文件操作等)操作时, 比如访问网络,就自动切换到其他的greenlet,等到IO完成,在适当的时候切换回来继续执行。 由于IO操作非常耗时,经常使程序处于等待状态,有了gevent我们自动切换协程, 就保证总有greenlet在执行,而不是等待IO。
猴子补丁(Monkey pitch):实现了自动切换。
###举例说明###(不使用猴子补丁)
import gevent
import time
from gevent import monkey
def a(): # 任务a
for i in range(3):
print('A' + str(i))
time.sleep(0.1)
def b(): # 任务b
for i in range(3):
print('B' + str(i))
time.sleep(0.1)
def c(): # 任务c
for i in range(3):
print('C' + str(i))
time.sleep(0.1)
if __name__ == '__main__':
g1 = gevent.spawn(a)
g2 = gevent.spawn(b)
g3 = gevent.spawn(c)
g1.join()
g2.join()
g3.join()
print('Over !')
输出:
A0 A1 A2 B0 B1 B2 C0 C1 C2 Over !
不使用猴子补丁时,程序中任务a,b,c按顺序执行,一个执行完才能执行下一个(即便出现了耗时操作sleep),此时不能称为是协程的应用。
###举例说明###(使用了猴子补丁)
import gevent
import time
from gevent import monkey
monkey.patch_all() # 猴子补丁
def a(): # 任务a
for i in range(3):
print('A' + str(i))
time.sleep(0.1)
def b(): # 任务b
for i in range(3):
print('B' + str(i))
time.sleep(0.1)
def c(): # 任务c
for i in range(3):
print('C' + str(i))
time.sleep(0.1)
if __name__ == '__main__':
g1 = gevent.spawn(a)
g2 = gevent.spawn(b)
g3 = gevent.spawn(c)
g1.join()
g2.join()
g3.join()
print('Over !')
输出:
A0 B0 C0 A1 B1 C1 A2 B2 C2 Over !
当我们使用了猴子补丁后,任务a,b,c交替执行。所以猴子补丁的作用就是当 gevent遇到延时 时切换到其他任务。
协程模拟下载多个任务:
# 案例
from gevent import monkey
monkey.patch_all() # 此处的猴子补丁需要在导入requests包之前完成补丁操作
import requests
import gevent
import urllib.request
def download(url):
# response = requests.get(url)
response = urllib.request.urlopen(url)
content = response.read()
# content = response.text
print('下载了{}的数据,长度:{}'.format(url, len(content)))
if __name__ == '__main__':
urls = ['http://www.163.com', 'http://www.qq.com', 'http://www.baidu.com']
g1 = gevent.spawn(download, urls[0])
g2 = gevent.spawn(download, urls[1])
g3 = gevent.spawn(download, urls[2])
gevent.joinall((g1, g2, g3))
输出:
下载了http://www.baidu.com的数据,长度:321105 下载了http://www.163.com的数据,长度:613708 下载了http://www.qq.com的数据,长度:23948



