1024 笔记
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)