当前自编代码有时受网速限制,尚未受系统资源限制,暂时用不上并发编程
概述
同一CPU在某时间点只能执行一个程序,由于速度快,容易产生多个程序同时执行的错觉
并发 | Concurrency | 多进程快速轮换 |
---|---|---|
并行 | Parallel | 同一时刻,多指令在多个处理器上同时执行 |
进程的特征
- 独立性。有独立的地址空间
- 动态性。是活动中的指令集合(程序是静态的指令集合),有自己的生命周期、状态
- 并发性。多个进程可以在同一处理器上并发执行,互不影响
线程的特征
- 同一进程下可以包含多个线程,各线程共享该进程的系统资源
- 其调度、管理由进程完成,无须经过操作系统
- 有自己的堆栈、程序计数器、局部变量
- 其运行是抢占式的;主线程与分线程并发,各线程的执行没有先后顺序
应用场景
当一个程序向不同客户端提供服务时,各客户端之间应该互不干扰。此时,可以在同一进程(程序)下运行多个线程(客户端)。与多进程相比,多线程共享内存、数据、代码,可以节约资源,提高运行效率,通信也更容易。
# 线程的创建和启动
threading模块下的方法
方法 | current_thread() | Thread(target=test, args=(,)) |
---|---|---|
作用 | 获取当前线程 | 创建线程对象 |
线程的方法
方法 | 作用 |
---|---|
getName() | 获取当前线程的名称 |
setName() | 为线程设置名称 |
is_alive() | 判断线程是否死亡。新建、死亡状态为False,其它状态为True |
- 默认名称:主线程MainThread,子线程Thread-1, …。名称相关操作也可用name属性实现
创建线程
Thread(group=None, target=None, name=None, args=(), kwargs=None, *,daemon=None)
参数 | group | target | args | kwargs | daemon |
---|---|---|---|---|---|
含义 | 线程组 | 目标方法 | 方法的位置参数 | 方法的关键词参数 | 是否为后台线程 |
import threading
def test(a):
x = threading.current_thread().getName()
print(x, a)
t1 = threading.Thread(target=test, args=(1,))
t1.start()
t2 = threading.Thread(target=test, args=(2,))
t2.start()
线程的生命周期
New | Ready | Running | Blocked | Dead |
---|---|---|---|---|
新建 | 就绪 | 运行 | 阻塞 | 死亡 |
调用start()方法后,线程获得栈、程序计数器,进入Ready状态,运行时机受线程调试器管理
注意:只能对新建的线程调用strat()方法,重复调用会报错,线程死亡后无法调用
不要调用run()方法,否则,只有主线程会被执行
运行和阻塞状态
阻塞的原因——阻塞解除时,线程恢复到Ready状态
- sleep() 主动放弃处理器资源
- 调用了阻塞式I/O方法
- 尝试获得的锁正被其它线程占用
- 等待Notify中
线程死亡
- 函数执行完成,线程正常结束
- 程序报错
控制线程
join(timeout=None)
一般情况下,主线程执行完成后就退出了,如果某分线程用了join,主线程等待该分线程结束后再结束。可以设置时间限制,到时间后分线程进入死亡状态(结束)
t1 = threading.Thread(target=test, args=(1,))
t1.start()
t1.join()
后台线程 Daemon Thread
主线程默认是前台线程。设置后台线程的方法:t1.daemon = True。后台线程随前台线程死亡
前台线程的子线程默认是前台线程;后台线程的子线程默认是后台线程
线程睡眠 sleep(secs)
来自time模块,进入blocked状态
# 线程同步
线程安全问题
由于系统的线程调度具有随机性,有时候需要让子线程锁定共享资源
例:银行账户余额1000元,同一时刻,在A、B两个客户端,登录同一账号,各取钱800元。余额变成-600。这明显是不合理的
Lock与RLock
锁:独占共享资源,访问前申请,访问后释放。加锁——修改——释放锁
Lock 基本锁,只能锁定一次
RLock 可重入锁,可锁定多次。其中,acquire()与release()成对出现
def test():
self.lock.acquire()
try:
...
finally:
self.lock.release()
死锁
两个线程互相等待对方释放锁,程序无法继续。避免死锁的方法:
- 对同一线程,避免多次锁定
- 固定加锁顺序
- 使用定时锁,到时间后自动释放 acquire(timeout=…)
- 死锁检测
阻塞与死锁的区别:阻塞——等待;死锁——报错
# 线程通信
Condition
使占有锁却无法继续执行的线程释放锁,唤醒处于等待状态的线程。总是有对应的Lock对象
方法 | 作用 |
---|---|
acquire([timeout]) | |
release() | |
wait([timeout]) | 让线程进入等待池并释放锁,等待唤醒 |
notify() | 随机唤醒等待池中的一个线程,调用acquire()尝试加锁 |
notify_all() | 唤醒等待池中的所有线程 |
Queue
三种队列 | queue.Queue() | queue.LifoQueue() | PriorityQueue() |
---|---|---|---|
特征 | 先进先出 | 后进先出 | 优先级队列 |
- 共有参数:队列的大小maxsize=0 ,<=0时不限制,达到上限时会加锁,不能再加入元素
队列的方法
方法(队列的状态) | qsize() | empty() | full() |
---|---|---|---|
作用 | 元素数量 | 队列是否为空 | 队列是否已满 |
方法 | 参数 | 作用 |
---|---|---|
put() | item, block=True, timeout=None | 放入元素 |
get() | 提取/消费元素 | |
put_nowait() | item | 放入元素,不阻塞 |
get_nowait() | item | 提取/消费元素,不阻塞 |
block:队列已满、已空时的操作。已满+True——阻塞;已空+False——报异常
timeout:阻塞时间,None表示长期,直到该队列的元素被消费
Event
不带Lock对象,想实现线程同步时,需要额外的Lock对象
is_set() 返回是否为True
set() 设置为True,并唤醒所有处于等待状态的线程
clear() 设置为False
wait([timeout=None]) 阻塞
# 线程池
用途:大量创建生存期很短的线程,可用【最大线程数】控制并发数量,保证系统性能
步骤:创建线程池——定义函数fn——提交任务submit——关闭线程池shutdown()
concurrent.futures模块下的Executor
创建线程池:ThreadPoolExecutor
创建进程池:ProcessPoolExecutor
方法 | 参数 | 作用 |
---|---|---|
submit() | fn, *args, **kwargs | 返回Future对象 |
map() | fn, *iterables, timeout=None, chunkxize=1 | 启动多个线程, 异步对iterables进行map处理, 类似全局函数map(fn, *iterables) |
shotdown() | wait=True | 关闭线程池 |
- 【args】为位置参数,【kwargs】为关键词参数
Future对象的方法
线程状态 | cancelled() | running() | done() |
---|---|---|---|
True | 已取消 | 正在运行 | 执行完成或成功取消 |
方法 | 作用 |
---|---|
cancle() | 取消线程 |
result(timeout=None) | 获取线程的最终结果 |
exception(timeout=None) | 获取线程的异常 |
add_done_callback(fn) | 为线程注册回调函数,线程完成时触发fn |
shutdown() | 关闭线程池。已经提交的任务继续执行,新任务不再接收 |
使用线程池
- 创建线程池ThreadPoolExecutor
- 定义函数fn
- 提交任务submit()
- 关闭线程池shutdown()
获取执行结果
方法 | 是否阻塞主线程 |
---|---|
result() | 是 |
add_done_callback(fn) | 否 |
线程相关类
线程局部变量
threading模块下的local()函数:返回一个(线程的)局部变量,相当于各线程各有一个可以独立地修改的副本变量。
线程的功能 | 作用 |
---|---|
同步机制 | 并发访问共享资源时,各线程进行通信 |
局部变量 | 隔离多个线程间的共享冲突 |
定时器
Thread类下的Timer子类,让指定函数在特定时间内执行一次
from threading import Timer
def test(): print(1)
t = Timer(3,test)
t.start()
注:Timer对象有cancel函数
任务调度
任务调度器:sched模块下的scheduler类
sched.scheduler(timefunc=time.monotonic, delayfunc=time.sleep)
imefunc:生成时间戳的时间函数
delayfunc阻塞程序的函数
属性:queue 返回调度器的调度队列
方法 | 作用 | |
---|---|---|
enterabs() | time, priority, action, argument=(), kwargs={} | 返回一个event,可用cancel()方法取消调度 |
enter() | delay, priority, action, argument,=(), kwargs={} | 同上 |
cancel() | event | 取消当前调试队列中的一个任务 |
empty() | 判断当前调度队列是否为空 | |
run() | blocking=True | 运行所有需调度的方法 |
time:时间点
priority:优先级:同一时间点,有多个任务时,优先级高(值小)的先执行
argument:位置参数
delay:若干秒后开始执行action任务
blocking=True:阻塞线程,直到所有被调度的方法执行完成
# 多进程
创建新进程 multiprocessing.Process
属性 | name | daemon | pid | authkey |
---|---|---|---|---|
进程的 | 名称 | 后台状态 | id | 授权key |
方法 | run() | start() | join([timeout]) | is_alive() | terminate() |
---|---|---|---|---|---|
作用 | 中断 |
Context和启动进程的方式
multiprocessing.set_start_method('spawn') #写在多进程相关代码之前
...
a = multiprocessing.get_context('spawn') #创建Context对象
q = a.Queue() #启动进程
mp = a.Process(target=test, args = (1,))
mp.start()
用进程池管理进程 multiprocessing.Pool()
进程池常用方法
方法 | 作用 |
---|---|
close() | 关闭进程池。不再接受新任务,执行完池中进程后关闭 |
terminate() | 立即中止进程池 |
join() | 等待所有进程完成 |
用进程处理函数
方法 | 参数 | 作用 | 阻塞 |
---|---|---|---|
apply() | func, [args], [kwds] | 将func交给进程池处理 | 是 |
apply_async() | func, [args], [kwds], [callback], [error_callback] | 否 | |
map() | func, iterable, [chunksize] | 用新进程对iterable的每个元素执行func | 是 |
imap() | 是 | ||
map_async() | func, iterable, [chunksize], [callback], [error_callback] | 否 |
- imap()为map()的延迟版本
进程通信
进程通信用mutiprocessing.Queue()
线程通信用queue.Queue()
get():读取数据
put(data):放入数据
创建管道 multiprocessing.Pipe()
管道的方法
发送与接收 | 作用 |
---|---|
send(obj) | 向管道的另一端发送数据,大小限制在32MB |
send_bytes(buffer, [offset], [size]) | 发送字节数据,默认发送全部,可设置起点、字节长度 |
recv() | 接收send()发送的数据 |
recv_bytes([maxlength]) | 接受send_bytes()发送的数据,可设置字数上限 |
recv_bytes_into(buffer, [offset]) | 接收到的数据放在buffer中 |
其它方法 | 作用 |
---|---|
fileno() | 连接用的文件描述器 |
poll([timeout]) | 返回连接中是否还有数据可读取 |
close() | 关闭连接 |