1. Event事件
定义
Event事件的作用: 用来控制线程
事件处理的机制:全局定义了一个内置标志Flag,如果Flag值为 False,那么当程序执行 event.wait方法时就会阻塞,如果Flag值为True,那么event.wait 方法时便不再阻塞。
方法
set()
: 将标志设为True,使处于阻塞状态的线程恢复运行状态。wait()
: 如果标志为True将立即返回,否则线程阻塞(可传时间)- clear(): 将标志设为False。
isSet()
: 获取内置标志状态,返回True或False。
from threading import Event import time from threading import Thread # 调用Event类实例化一个对象 e = Event() def light(): print('红灯亮...') time.sleep(5) # 发送信号true,其他所有进程准备执行 e.set() # flag变为true ,阻塞态变为运行 print('绿灯亮...') def car(name): print('正在等红灯...') # 所有汽车🚕处于阻塞态 e.wait() # 为false是阻塞,直到为true print(f'{name}出发...') # 让一个线程任务控制多个car任务 t = Thread(target=light) t.start() for i in range(10): s = Thread(target=car,args=(f'{i}号赛车',)) s.start()
当light线程执行时,打印'红灯亮',IO操作切换线程执行car线程,打印'正在等红灯',wait时flag是false所以阻塞,遇到IO切换,执行第二个car打印'正在等红灯',..... 直到5秒结束,set让flag变为true,打印'绿灯亮',IO操作
2. 进程池与线程池
定义
进程池与线程池是用来控制当前程序允许创建(进程/线程)的数量.
当我们需要开启多个线程或进程时,难道只能一个个去开吗? 这时我们可以开辟一个进程池或线程池,可以一次就开启多个进程或线程,池的简单实现也可以用生产者与消费者模式来实现。 主线程: 相当于生产者,只管向线程池提交任务。并不关心线程池是如何执行任务的。因此,并不关心是哪一个线程执行的这个任务。 线程池: 相当于消费者,负责接收任务,并将任务分配到一个空闲的线程中去执行。
线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
使用线程池可以有效地控制系统中并发线程的数量
作用
保证在硬件允许的范围内创建(进程/线程)的数量.
使用
进程池
from concurrent.futures import ProcessPoolExecutor来调用 ProcessPoolExecutor(5) # 代表只能开启5个进程 ProcessPoolExecutor() # 不写默认以cpu的个数限制进程数
线程池
from concurrent.futures import ThreadPoolExecutor pool = ThreadPoolExecutor(5) # 代表只能开启5个进程 pool = ThreadPoolExecutor() # 不写默认以'cpu个数*5'限制进程数
.submit()
.submit('传函数地址')
异步提交任务,与t = Thread() t.start()
一样
from concurrent.futures import ThreadPoolExecutor import time pool = ThreadPoolExecutor(5) # 同时提交5个线程任务 def task(): print('线程任务开始...') time.sleep(1) print('线程任务结束...') for i in range(5): pool.submit(task)
回调函数add_done_callback()
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数
异步回调指的是:在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数。
pool.submit('传函数地址').add_done_callback('添加回调函数')
'''submit(task)执行task函数拿到task返回值,通过add_done_callback将返回值传入回调函数,回调函数需传形参,得到的是对象通过`对象.result()`得到线程任务返回的结果'''
from concurrent.futures import ThreadPoolExecutor import time pool = ThreadPoolExecutor(5) # 同时提交5个线程任务 def task(): print('任务开始...') time.sleep(1) print('线程任务结束...') return 123 # 回调函数 def call_b(res): # 渎职操作不要与接收的res重名 data = res.result() # .result()去取出res对象中的值 print(data) for i in range(5): pool.submit(task).add_done_callback(call_b)
3. 协程
定义
线程是系统级别的,他们有操作系统调度 协程是程序级别的,由程序根据需要自己调度 在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行
一个程序中有很多函数,称之为子程序.在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可中断回来继续执行此子程序,这个过程就称之为协程, 类似于yield操作,但yield不会检测IO操作.
--协程-- 通过手动模拟操作系统的'多道技术',实现切换+保存 状态 手动实现 遇到IO切换,欺骗操作系统误以为没有IO操作 单线程 遇到IO, 切换 + 保存状态 单线程 计算密集, 来回切换 + 保存状态,反而效率更低 优点: 在IO密集型的情况下,会提高效率 缺点: 计算密集型,来回切换,反而效率较低
Python通过yield
提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持
代码实现
验证计算密集型的情况下
串行的情况下 import time def func1(): for i in range(10000000): i + 1 def func2(): for i in range(10000000): i + 1 start = time.time() func1() func2() end = time.time() print(f'用时{end-start}') # 用时1.9524507522583008 ========================================================== 切换+保存实现并发 import time def func1(): while True: 10000000 + 1 yield def func2(): # g 是生成器对象 g = func1() for i in range(10000000): i + 1 next(g) # 每次执行next相当于切换到func1下面 start = time.time() func2() stop = time.time() print(stop-start) # 3.2243001461029053
IO密集型的情况下
如何检测到IO操作,引出gevent
第三方库实现并发同步或异步编程,遇到IO阻塞会自动切换任务
pip install gevent # 安装第三方库 from gevent import monkey # 进行导入monkey monkey.patch_all() # 可以监听该程序下的所有IO操作 from gevent import spawn # 用于做切换+保存状态
from gevent import monkey monkey.patch_all() # 可以监听该程序下的所有IO操作 from gevent import spawn # 用于做切换+保存状态 import time def func1(): print('1') # IO操作 time.sleep(3) def func2(): print('2') # IO操作 time.sleep(1) def func3(): print('3') # IO操作 time.sleep(5) start = time.time() # 调用spawn(任务函数) 让其单线程下实行并发 s1 = spawn(func1) s2 = spawn(func2) s3 = spawn(func3) # join发送信号,相当于等待自己结束再全结束(单线程) s1.join() s2.join() s3.join() stop = time.time() print(stop-start) # 5.014296770095825 # 实现并发执行,func123会并发一起执行
4. TCP服务端实现协程
'''服务端''' from gevent import monkey monkey.patch_all() # 检测IO import socket from gevent import spawn s = socket.socket() s.bind( ('127.0.0.1',8848) ) s.listen(5) print('等待客户端连接') def work(conn): while True: try: data = conn.recv(1024) print(data.decode('utf-8')) conn.send(data.upper()) except Exception as e: print(e) break conn.close() def server(): while True: conn,addr= s.accept() print(f'用户{addr}已连接') # 监听work函数,进行并发 spawn(work,conn) if __name__ == '__main__': # 监听server函数,进行并发 s1 = spawn(server) s1.join() ========================================================== '''客户端''' import socket from threading import Thread, current_thread def client(): c = socket.socket() c.connect( ('127.0.0.1',8848) ) print('启动客户端...') num = 0 while True: data = f'{current_thread().name}{num}' c.send(data.encode('utf-8')) da = c.recv(1024) print(da.decode('utf-8')) num += 1 # 模拟30个用户并发去访问客户端 for i in range(30): t = Thread(target=client) t.start()
5. IO模型
Python中的io模块是用来处理各种类型的I/O操作流。主要有三种类型的I/O类型:文本I/O(Text I/O),二进制I/O(Binary I/O)和原始I/O(Raw I/O)。它们都是通用类别,每一种都有不同的后备存储。属于这些类别中的任何一个的具体对象称为文件对象,其他常用的术语为流或者类文件对象。
IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段: 1)等待数据准备 (Waiting for the data to be ready) 2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
常用四种IO模型
- 阻塞IO(blocking IO)
- 非阻塞IO(non-blocking IO)
- 多路复用IO(IO multiplexing)
- 异步IO(Asynchronous I/O)