python之进程、线程、协程
进程
进程的概念
进程:
- 运行中的程序
- 是计算机中最小的资源分配单位
- 在操作系统中唯一标识符:PID
操作系统调度进程的算法:
- 短作业优先
- 先来先服务
- 时间片轮转
- 多级反馈算法
并行与并发:
- 并行:并行是指两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )
- 并发:并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率。
-
就绪(Ready)状态
当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。
-
执行/运行(Running)
状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。
-
阻塞(Blocked)状态
正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。
同步异步:
所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
在python程序中的进程操作
进程:
创建进程 时间开销大
销毁进程 时间开销大
进程之间切换 时间开销大
线程:
线程是进程的一部分,每个进程中至少有一个线程
能被CPU调度的最小单位
一个进程中的多个线程是可以共享这个进程的数据的 —— 数据共享
线程的创建、销毁、切换 开销远远小于进程 —— 开销小
进程:是计算机中最小的资源分配单位(进程是负责圈资源)
线程:是计算机中能被CPU调度的最小单位 (线程是负责执行具体代码的)
os.getpid()
:获取当前进程pid
os.getppid()
:获取父级进程pid,可以创建子进程,在pycharm中启动的所有py程序都是pycharm的子进程
import os
import time
from multiprocessing import Process
# multiprocessing多进程模块Process类
def func():
print('start',os.getpid())
time.sleep(1)
print('end',os.getpid())
if __name__ == '__main__':
p = Process(target=func) # 将函数封装到类,创建一个要开启func进程的对象
p.start() # 异步 调用开启进程的方法 但是并不等待这个进程真的开启
print('main :',os.getpid())
#main : 11436
#start 9860
#end 9860
操作系统创建进程的方式不同
print([__name__])
if __name__ == '__main__':
# 控制当这个py文件被当作脚本直接执行的时候,就执行这里面的代码
# 当这个py文件被当作模块导入的时候,就不执行这里面的代码
print('hello hello')
# __name__ == '__main__'
# 执行的文件就是__name__所在的文件
# __name__ == '文件名'
# __name__所在的文件被导入执行的时候
- windows操作系统执行开启进程的代码
实际上新的子进程需要通过import父进程的代码来完成数据的导入工作
所以有一些内容我们只希望在父进程中完成,就写在if __name__ == '__main__'
:下面
- Linux操作系统
创建进程 采用fork方式,是一种数据拷贝,不会再次执行代码
主进程和子进程之间的关系
父进程会等待着所有的子进程结束之后才结束,为了回收资源
主进程代码执行完毕:
# 主进程负责回收子进程的资源
# 如果子进程执行结束,父进程没有回收资源,那么这个子进程会变成一个僵尸进程
# 主进程的结束逻辑
# 主进程的代码结束
# 所有的子进程结束
# 给子进程回收资源
# 主进程结束
# 主进程怎么知道子进程结束了的呢?
# 基于网络、文件
join方法
阻塞父进程,直到对应子进程结束就结束
import time
from multiprocessing import Process
def send_mail():
time.sleep(3)
print('发送了一封邮件')
if __name__ == '__main__':
p = Process(target=send_mail)
p.start() # 异步 非阻塞
# time.sleep(5)
print('join start')
p.join() # 同步 阻塞 直到p对应的进程结束之后才结束阻塞
print('邮件已发送完毕')
#join start
#发送了一封邮件
#邮件已发送完毕
import time
import random
from multiprocessing import Process
def send_mail(a):
time.sleep(random.random())
print('发送了一封邮件',a)
if __name__ == '__main__':
l = []
for i in range(10):
p = Process(target=send_mail,args=(i,)) # 向子进程传参数,用元组
p.start()
l.append(p) #回收多个子进程资源,先添指列表,最后统一处理
for p in l:p.join() #对于已经结束的子进程join相当于pass
# 阻塞 直到上面的十个进程都结束
print('5000封邮件已发送完毕')
发送了一封邮件 5
发送了一封邮件 4
发送了一封邮件 3
......
5000封邮件已发送完毕
守护进程
随着主进程的代码结束而结束的,所有的子进程都必须在主进程结束之前结束,由主进程来负责回收资源
p.daemon = True
其他方法:
p.is_alive() # 判断进程是否活着
p.terminate() # 终止子进程,异步非阻塞。
def son1():
while True:
print('is alive')
time.sleep(0.5)
if __name__ == '__main__':
p = Process(target=son1)
p.start() # 异步 非阻塞
print(p.is_alive())
time.sleep(1)
p.terminate() # 异步的 非阻塞
print(p.is_alive()) # 进程还活着 因为操作系统还没来得及关闭进程
time.sleep(0.01)
print(p.is_alive()) # 操作系统已经响应了我们要关闭进程的需求,再去检测的时候,得到的结果是进程已经结束了
使用面向对象方式开启进程
import os
import time
from multiprocessing import Process
class MyProcecss2(Process): #必须继承Process
def run(self): #必须要有run方法,重写process的run,start自动调用run
while True:
print('is alive')
time.sleep(0.5)
class MyProcecss1(Process):
def __init__(self,x,y): #传参数要定义init函数
self.x = x
self.y = y
super().__init__() #要导入父类的初始化参数
def run(self):
print(self.x,self.y,os.getpid())
for i in range(5):
print('in son2')
time.sleep(1)
if __name__ == '__main__':
mp = MyProcecss1(1,2)
mp.daemon = True
mp.start()
print(mp.is_alive())
mp.terminate()
# mp2 = MyProcecss2()
# mp2.start()
# print('main :',os.getpid())
# time.sleep(1)
Process操作进程的方法总结
# p.start() 开启进程 异步非阻塞
# p.terminate() 结束进程 异步非阻塞
# p.join() 同步阻塞
# p.isalive() 获取当前进程的状态
# p.daemon = True 设置为守护进程,守护进程永远在主进程的代码结束之后自动结束
进程锁
如果在一个并发的场景下,涉及到某部分内容,是需要修改一些所有进程共享数据资源,需要加锁来维护数据的安全。在数据安全的基础上,才考虑效率问题。
加锁步骤:
-
在主进程中实例化 lock = Lock()
-
把这把锁传递给子进程
-
在子进程中 对需要加锁的代码 进行 with lock:
with lock相当于
lock.acquire()
和lock.release()
,最好采用with方式,封装了异常处理。
在进程中需要加锁的场景:
共享的数据资源(文件、数据库),对资源进行修改、删除操作
说明:加锁之后能够保证数据的安全性 但是也降低了程序的执行效率
用一个买票例子,举例:
import time
import json
from multiprocessing import Process,Lock
def search_ticket(user):
with open('ticket_count') as f:
dic = json.load(f)
print('%s查询结果 : %s张余票'%(user,dic['count']))
def buy_ticket(user,lock):
with lock:
# lock.acquire() # 给这段代码加上一把锁
time.sleep(0.02)
with open('ticket_count') as f:
dic = json.load(f)
if dic['count'] > 0:
print('%s买到票了'%(user))
dic['count'] -= 1
else:
print('%s没买到票' % (user))
time.sleep(0.02)
with open('ticket_count','w') as f:
json.dump(dic,f)
# lock.release() # 给这段代码解锁
def task(user, lock):
search_ticket(user)
with lock:
buy_ticket(user, lock)
if __name__ == '__main__':
lock = Lock()
for i in range(10):
p = Process(target=task,args=('user%s'%i,lock))
p.start()
进程之间通信 - IPC机制
进程之间的通信 - IPC机制(inter process communication)
-
python内置的模块multiprocessing提供Queue和Pipe两种
-
第三方:redis,memcache,kafka,rabbitmq
from multiprocessing import Queue,Process
# 先进先出
def func(exp,q):
ret = eval(exp)
q.put(ret)
if __name__ == '__main__':
q = Queue() #可设置大小
Process(target=func,args=('1+2+3',q)).start()
print(q.get())
Queue基于文件家族的socket 、pickle、 lock实现的消息传递,数据安全
Pipe 管道= 文件家族的socket pickle 数据不安全
队列 = 管道 + 锁
pipe用法:
from multiprocessing import Pipe
pip = Pipe()
pip.send()
pip.recv()
Queue用法:
import queue #异常捕获用
from multiprocessing import Queue
q = Queue(5)
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(5) # 当队列为满的时候再向队列中放数据 队列会阻塞
print('5555555')
try:
q.put_nowait(6) # 当队列为满的时候再向队列中放数据 会报错并且会丢失数据
except queue.Full:
pass
print('6666666')
print(q.get())
print(q.get())
print(q.get()) # 在队列为空的时候会发生阻塞
print(q.get()) # 在队列为空的时候会发生阻塞
print(q.get()) # 在队列为空的时候会发生阻塞
try:
print(q.get_nowait()) # 在队列为空的时候 直接报错
except queue.Empty:pass
进程间数据共享
mulprocessing
库中有一个Manager类,封装了所有和进程相关的 数据共享 数据传递相关的数据类型,但是对于 字典 列表这一类的数据操作的时候会产生数据不安全, 需要加锁解决问题,并且需要尽量少的使用这种方式.
from multiprocessing import Manager,Process,Lock
def func(dic,lock):
with lock:
dic['count'] -= 1
if __name__ == '__main__':
# m = Manager()
with Manager() as m:
l = Lock()
dic = m.dict({'count':100})
p_l = []
for i in range(100):
p = Process(target=func,args=(dic,l))
p.start()
p_l.append(p)
for p in p_l:p.join()
print(dic)
进程间应该尽量避免通信,即便需要通信,也应该选择进程安全的工具来避免加锁带来的问题。
生产者消费者模型
什么是生产者消费者模型?
把一个产生数据并且处理数据的过程解耦,让生产的数据的过程和处理数据的过程达到一个工作效率上的平衡,中间的容器,在多进程中我们使用队列或者可被join的队列,做到控制数据的量。
-
当数据过剩的时候,队列的大小会控制这生产者的行为
-
当数据严重不足的时候,队列会控制消费者的行为
-
并且我们还可以通过定期检查队列中元素的个数来调节生产者消费者的个数
第一种方式:
流程:消费者开启进程get,生产者开启进程put,加入队列,全部结束后(jion),队列put(None),消费者get到空终止
import time
import random
from multiprocessing import Process,Queue
def producer(q,name,food):
for i in range(10):
time.sleep(random.random())
fd = '%s%s'%(food,i)
q.put(fd)
print('%s生产了一个%s'%(name,food))
def consumer(q,name):
while True:
food = q.get()
if not food:break
time.sleep(random.randint(1,3))
print('%s吃了%s'%(name,food))
def cp(c_count,p_count):
q = Queue(10)
for i in range(c_count):
Process(target=consumer, args=(q, 'jack')).start()
p_l = []
for i in range(p_count):
p1 = Process(target=producer, args=(q, 'KFC', 'hamburger'))
p1.start()
p_l.append(p1)
for p in p_l:p.join()
for i in range(c_count):
q.put(None)
if __name__ == '__main__':
cp(2,3)
第二种方式:
JoinableQueue
:该队列有一个计数器,put计数器加1,get后标记task_done()
计数器减1,对象join后会阻塞,直到队列为空结束阻塞。
import time
import random
from multiprocessing import JoinableQueue,Process
def producer(q,name,food):
for i in range(10):
time.sleep(random.random())
fd = '%s%s'%(food,i)
q.put(fd)
print('%s生产了一个%s'%(name,food))
q.join()
def consumer(q,name):
while True:
food = q.get()
time.sleep(random.random())
print('%s吃了%s'%(name,food))
q.task_done()
if __name__ == '__main__':
jq = JoinableQueue()
p =Process(target=producer,args=(jq,'KFC','hamburger'))
p.start()
c = Process(target=consumer,args=(jq,'jack'))
c.daemon = True
c.start()
p.join()
JoinableQueue([maxsize])
:这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。 maxsize
是队列中允许最大项数,省略则无大小限制。
JoinableQueue
的实例除了与Queue对象相同的方法之外还具有:
q.task_done()
:使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
q.join()
:生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
线程
GIL锁
全局解释器锁,cpython解释器中特殊的垃圾回收机制,导致了在同一个进程中多个线程不能同时利用多核 —— python的多线程只能是并发不能是并行
所以使用所线程并不影响高io型的操作,只会对高计算型的程序由效率上的影响
主线程什么时候结束?等待所有子线程结束之后才结束
主线程如果结束了,主进程也就结束了
开启线程
# multiprocessing 是完全仿照这threading的类写的
from threading import Thread
def func():
print('start son thread')
# 启动线程 start
Thread(target=func).start()
# 开启多个子线程
def func(i):
print('start son thread',i)
time.sleep(1)
print('end son thread',i)
for i in range(10):
Thread(target=func,args=(i,)).start()
print('main')
join方法
# join方法 阻塞 直到子线程执行结束
import time
import os
from threading import Thread
def func(i):
print('start son thread',i)
time.sleep(1)
print('end son thread',i,os.getpid())
t_l = []
for i in range(10):
t = Thread(target=func,args=(i,))
t.start()
t_l.append(t)
for t in t_l:t.join()
print('子线程执行完毕')
面向对象开启多线程
# 使用面向对象的方式启动线程
class MyThread(Thread):
def __init__(self,i):
self.i = i
super().__init__()
def run(self):
print('start',self.i,self.ident)
time.sleep(1)
print('end',self.i)
for i in range(10):
t = MyThread(i)
t.start()
print(t.ident) #线程id
# 线程里的一些其他方法
from threading import current_thread,enumerate,active_count
def func(i):
t = current_thread() #当前线程对象
print('start son thread',i,t.ident)
time.sleep(1)
print('end son thread',i,os.getpid())
t = Thread(target=func,args=(1,))
t.start()
print(t.ident)
print(current_thread().ident) #在哪一个线程里,current_thread()得到的就是这个当前线程的信息
print(enumerate()) # 活着的线程列表
print(active_count()) # =len(enumerate())
在进程中有terminate 方法可以结束一个进程,在线程中不能从主线程结束一个子线程
守护线程
守护线程一直等到所有的非守护线程都结束之后才结束
除了守护了主线程的代码之外也会守护子线程,与守护进程不同,守护进程只守护主进程
# 守护线程
import time
from threading import Thread
def son1():
while True:
time.sleep(0.5)
print('in son1')
def son2():
for i in range(5):
time.sleep(1)
print('in son2')
t =Thread(target=son1)
t.daemon = True
t.start()
Thread(target=son2).start()
time.sleep(3)
线程锁
即便是线程 即便有GIL 也会出现数据不安全的问题
1.操作的是全局变量
2.做一下操作
+=
-=
*=
/=
先计算再赋值才容易出现数据不安全的问题- 包括 lst[0] += 1 dic['key']-=1
互斥锁:在同一个线程中,不能连续acquire多次
a = 0
def add_f(lock):
global a
for i in range(200000):
with lock:
a += 1
def sub_f(lock):
global a
for i in range(200000):
with lock:
a -= 1
from threading import Thread,Lock
lock = Lock()
t1 = Thread(target=add_f,args=(lock,))
t1.start()
t2 = Thread(target=sub_f,args=(lock,))
t2.start()
t1.join()
t2.join()
print(a)
递归锁
from threading import RLock
# rlock = RLock()
# rlock.acquire()
# print('*'*20)
# rlock.acquire()
# print('-'*20)
# rlock.acquire()
# print('*'*20)
优点:在同一个线程中,可以连续acuqire多次不会被锁住
缺点:占用了更多资源,效率低
死锁现象:在某一些线程中出现陷入阻塞并且永远无法结束阻塞的情况就是死锁现象
1.多把锁+交替使用
2.互斥锁在一个线程中连续acquire
避免方法:在一个线程中只有一把锁,并且每一次acquire之后都要release
解决方法:可以用递归锁解决,也可以通过优化代码逻辑解决.
import time
from threading import RLock,Thread
# noodle_lock = RLock()
# fork_lock = RLock()
noodle_lock = fork_lock = RLock()
print(noodle_lock,fork_lock)
def eat1(name,noodle_lock,fork_lock):
noodle_lock.acquire()
print('%s抢到面了'%name)
fork_lock.acquire()
print('%s抢到叉子了' % name)
print('%s吃了一口面'%name)
time.sleep(0.1)
fork_lock.release()
print('%s放下叉子了' % name)
noodle_lock.release()
print('%s放下面了' % name)
def eat2(name,noodle_lock,fork_lock):
fork_lock.acquire()
print('%s抢到叉子了' % name)
noodle_lock.acquire()
print('%s抢到面了'%name)
print('%s吃了一口面'%name)
time.sleep(0.1)
noodle_lock.release()
print('%s放下面了' % name)
fork_lock.release()
print('%s放下叉子了' % name)
lst = ['A','B','C','D']
Thread(target=eat1,args=(lst[0],noodle_lock,fork_lock)).start()
Thread(target=eat2,args=(lst[1],noodle_lock,fork_lock)).start()
Thread(target=eat1,args=(lst[2],noodle_lock,fork_lock)).start()
Thread(target=eat2,args=(lst[3],noodle_lock,fork_lock)).start()
互斥锁解决
import time
from threading import Lock,Thread
lock = Lock()
def eat1(name,noodle_lock,fork_lock):
lock.acquire()
print('%s抢到面了'%name)
print('%s抢到叉子了' % name)
print('%s吃了一口面'%name)
time.sleep(0.1)
print('%s放下叉子了' % name)
print('%s放下面了' % name)
lock.release()
def eat2(name,noodle_lock,fork_lock):
lock.acquire()
print('%s抢到叉子了' % name)
print('%s抢到面了'%name)
print('%s吃了一口面'%name)
time.sleep(0.1)
print('%s放下面了' % name)
print('%s放下叉子了' % name)
lock.release()
lst = ['A','B','C','D']
Thread(target=eat1,args=(lst[0],noodle_lock,fork_lock)).start()
Thread(target=eat2,args=(lst[1],noodle_lock,fork_lock)).start()
Thread(target=eat1,args=(lst[2],noodle_lock,fork_lock)).start()
Thread(target=eat2,args=(lst[3],noodle_lock,fork_lock)).start()
线程队列queue模块
对于线程之间的通信,采用队列,内部已实现锁, 线程安全
- 先进先出队列
from queue import Queue
先来先服务的思想
- 后进先出队列---栈
from queue import LifoQueue
- 优先级队列
from queue import PriorityQueue
pq = PriorityQueue()
pq.put((10,'A'))
pq.put((6,'B'))
pq.put((20,'C'))
print(pq.get())
print(pq.get())
print(pq.get())
# (6, 'B')
# (10, 'A')
# (20, 'C')
带锁的单例模式🔶
import time
from threading import Lock
class A:
__instance = None
lock = Lock()
def __new__(cls, *args, **kwargs):
with cls.lock:
if not cls.__instance:
time.sleep(0.1)
cls.__instance = super().__new__(cls)
return cls.__instance
def __init__(self,name,age):
self.name = name0
self.age = age
def func():
a = A('YHP', 22)
print(a)
from threading import Thread
for i in range(10):
t = Thread(target=func)
t.start()
在多进程里启动多线程
import os
from multiprocessing import Process
from threading import Thread
def tfunc():
print(os.getpid())
def pfunc():
print('pfunc-->',os.getpid())
Thread(target=tfunc).start()
if __name__ == '__main__':
Process(target=pfunc).start()
池的使用
预先的开启固定个数的进程数,当任务来临的时候,直接提交给已经开好的进程,让这个进程去执行就可以了,节省了进程,线程的开启关闭的切换时间,并且减轻了操作系统调度的负担.
开启步骤
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
# 创建一个池子
tp = ThreadPoolExecutor(池中线程/进程的个数)
# 异步提交任务
ret = tp.submit(函数,参数1,参数2....)
# 获取返回值
ret.result()
# 在异步的执行完所有任务之后,主线程/主进程才开始执行的代码
tp.shutdown() # 阻塞 直到所有的任务都执行完毕
# 关闭池之后就不能继续提交任务,并且会阻塞,直到已经提交的任务完成
# map方法
ret = tp.map(func,iterable) #迭代获取iterable中的内容,作为func的参数,让子线程来执行对应的任务
for i in ret: 每一个都是任务的返回值,不用 result
# 回调函数
ret.add_done_callback(函数名)
# 要在ret对应的任务执行完毕之后,直接继续执行add_done_callback绑定的函数中的内容,并且ret的结果会作为参数返回给绑定的函数
带参数及返回值
def func(i,name):
print('start',os.getpid())
time.sleep(random.randint(1,3))
print('end', os.getpid())
return '%s * %s'%(i,os.getpid())
if __name__ == '__main__':
p = ProcessPoolExecutor(5)
ret_l = []
for i in range(10):
ret = p.submit(func,i,'JJJ')
ret_l.append(ret)
for ret in ret_l:
print('ret-->',ret.result()) # ret.result() 同步阻塞
print('main',os.getpid())
回调函数
import requests
from concurrent.futures import ThreadPoolExecutor
def get_page(url):
res = requests.get(url)
return {'url':url,'content':res.text}
def parserpage(ret): #必须有参数
dic = ret.result()
print(dic)
tp = ThreadPoolExecutor(5)
url_lst = [
'http://www.baidu.com', # 3
'http://www.cnblogs.com', # 1
'http://www.douban.com', # 1
'http://www.tencent.com',
]
ret_l = []
for url in url_lst:
ret = tp.submit(get_page,url)
ret_l.append(ret)
ret.add_done_callback(parserpage)
回调函数add_done_callback:
-
执行完子线程任务之后直接调用对应的回调函数
-
爬取网页 需要等待数据传输和网络上的响应高IO的 -- 子线程
-
分析网页 没有什么IO操作 -- 这个操作没必要在子线程完成,交给回调函数
是单独开启线程进程还是池?
-
如果只是开启一个子线程做一件事情,就可以单独开线程
-
有大量的任务等待程序去做,要达到一定的并发数,开启线程池
-
根据你程序的io操作也可以判定是用池还是不用池
如:socket的server 大量的阻塞io recv recvfrom socketserver,爬虫的时候 池
池的总结
ThreadPoolExecutor中的几个常用方法
tp = ThreadPoolExecutor(cpu*5) # 进程一般不超过cpu个数,线程一般不超过5倍cpu个数
# ret = tp.map(任务函数,iterable)
# 1.迭代ret,总是能得到所有的返回值
# shutdown
# tp.shutdown()
obj = tp.submit(任务函数,参数1,参数2。。。)
# obj
# 1.获取返回值 obj.result() 是一个阻塞方法
# 2.绑定回调函数 obj.add_done_callback(子线程执行完毕之后要执行的代码对应的函数)
协程
协程:用户级别的,由我们自己写的python代码来控制切换的,是操作系统不可见的
在Cpython解释器下 - 协程和线程都不能利用多核,都是在一个CPU上轮流执行,由于多线程本身就不能利用多核,所以即便是开启了多个线程也只能轮流在一个CPU上执行,协程如果把所有任务的IO操作都规避掉,只剩下需要使用CPU的操作,就意味着协程就可以做到题高CPU利用率的效果。
多线程和协程对比
-
线程: 切换需要操作系统,开销大,操作系统不可控,给操作系统的压力大,操作系统对IO操作的感知更加灵敏
-
协程: 切换需要python代码,开销小,用户操作可控,完全不会增加操作系统的压力,用户级别能够对IO操作的感知比较低
两种切换方式
-
原生python完成 yield ->asyncio-> aiohttp、sanic
-
C语言完成的python模块 greenlet -> gevent
gevent模块
import time
import gevent
from gevent import monkey
monkey.patch_all()
def eat():
print('Jack is eating')
print('in eat: ')
time.sleep(1) #遇到阻塞让出CPU
return 'Jack finished eat'
def sleep():
print('YHP is sleeping')
time.sleep(1)
print('YHP finished sleep')
g_l=[]
for i in range(10): # 创造十个协程任务
g1 = gevent.spawn(eat)
g_l.append(g1)
g2 = gevent.spawn(sleep) # 创造一个协程任务
g2.join() # 阻塞 直到g1任务完成为止
gevent.joinall(g_l) #jionall后面加包含gevent对象的列表
for i in g_l:
print(i.value) #value取值
asyncio模块
python原生的底层的协程模块
在爬虫 webserver框架中,提高网络编程的效率和并发效果
import asyncio
# 1.起一个任务
async def demo(): # 协程方法
print('start')
await asyncio.sleep(1) # 阻塞
print('end')
loop = asyncio.get_event_loop() # 创建一个事件循环
loop.run_until_complete(demo()) # 把demo任务丢到事件循环中去执行
# 2.启动多个任务,并且没有返回值
async def demo(): # 协程方法
print('start')
await asyncio.sleep(1) # 阻塞
print('end')
loop = asyncio.get_event_loop() # 创建一个事件循环
wait_obj = asyncio.wait([demo(),demo(),demo()])
loop.run_until_complete(wait_obj)
# 3.启动多个任务并且有返回值
async def demo(): # 协程方法
print('start')
await asyncio.sleep(1) # 阻塞
print('end')
return 123
loop = asyncio.get_event_loop()
t1 = loop.create_task(demo())
t2 = loop.create_task(demo())
tasks = [t1,t2]
wait_obj = asyncio.wait([t1,t2])
loop.run_until_complete(wait_obj)
for t in tasks:
print(t.result())
# 4.谁先回来先取谁的结果
import asyncio
async def demo(i): # 协程方法
print('start')
await asyncio.sleep(10-i) # 阻塞
print('end')
return i,123
async def main():
task_l = []
for i in range(10):
task = asyncio.ensure_future(demo(i))
task_l.append(task)
for ret in asyncio.as_completed(task_l):
res = await ret
print(res)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
总结:
-
await 阻塞 协程函数这里要切换出去,还能保证一会儿再切回来
-
await 必须写在async函数里,async函数是协程函数
-
loop 事件循环 所有的协程的执行 调度 都离不开这个loop