线程和进程
-
进程
- 当一个程序进入内存运行时,即变成一个进程。
- 进程具有独立性,动态性,并发性
- 现代操作系统支持多进程的并发执行
-
并发(Concurrency) VS 并行(Parallel)
- 并行 在同一时刻有多条指令在多个处理器上同时执行
- 并发 在同一时刻只能有一条指令执行,多个进程指令被快速轮换执行(cpu执行速度太快了)
-
线程
- 线程的调度和管理由进程本身负责完成
- 线程不拥有系统资源,多线程共享父进程的全部资源
- 线程独立抢占式运行,一个线程可以创建和撤销另一个线程
- 多线程扩展多进程的概念,使得同一进程可以同时并发处理多个任务
- 线程在程序中是独立的,并发执行流,一个进程可以拥有多个线程,一个线程必须有一个父进程
-
操作系统 --> 多任务 --> 进程 --> 多任务 --> 线程
- 操作系统可以同时执行多个任务,每一个任务就是一个进程
- 进程可以同时执行多个任务,每个任务就是一个线程
多线程优点
- 进程中线程之间,共享内存,文件句柄和其他进程就有的状态
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存
- 同一进程的线程都有共性,多个线程共享同一进程的虚拟空间,共享进程代码段,进程公有数据等
- 结论
- 进程之间不能共享内存,但在线程之间共享内存非常容易
- 操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多
- python语言内置了多线程功能支持
创建启动线程
-
内置多线程模块
_thread
和threading
_thread
提供低级别的、原始的线程支持,以及一个简单的锁(不建议使用)threading
模块则提供了功能丰富的多线程支持
-
python创建线程的两种方式
- 使用 threading 模块的 Thread 类的构造器创建线程
- 继承 threading 模块的 Thread
-
调用
Thread
类的构造器创建线程threading.Thread
类的如下构造器创建线程__init__(self, group=None, target=None, name=None, args=(), kwargs=None, *,daemon=None)
group
:指定该线程所属的线程组target
指定该线程要调度的目标方法- args:指定一个元组,以位置参数的形式为 target 指定的函数传入参数
- Thread 类的构造器创建井启动多线程
- 调用 Thread 类的构造器创建线程对象
- 在创建线程对象时,target 参数指定的函数将作为线程执行体
- 调用线程对象的
start()
方法启动该线程
- 概念
- 线程执行体 线程的
target
函数为线程执行体,args指定参数为执行体参数 - 主线程 Python 程序运行时默认的主线程 主程序部分(没有放在任何函数中的代码)即为主线程的线程执行体
- 线程的执行没有顺序,但线程执行体有序。散落在外的代码是属于主线程执行体,主线程不需要手动启动
- 默认情况下,主线程的名字为MainThread, 用户启动的线程依次为Thread-1,2…
- 线程何时开始运行,取决于 Python 解释器中线程调度器的调度
- 线程执行体 线程的
-
继承 Thread 类创建线程类
1.定义Thread类的子类,并重写该类的run()方法。
- run方法代表线程需要完成的任务,其本质是线程的执行体
2. 创建Thread子类的实例,即创建线程对象
3. 调用线程对象的start()
方法启动该线程 -
小结
- 线程创建完毕都需要调用线程对象的start方法启动,线程执行体才可能被并发
- 使用构造器方式不需要重写run方法,但需要在构造初始化时指定线程执行体
- 对于多线程,编译器在运行时首先是收集各线程执行体信息,然后通常从主线程开始抢占式执行线程体代码,其线程执行无序
python线程的生命周期
- 新建 此时的线程对象并没有表现出任何线程的动态特征,只是一个普通的python对象
- 就绪 当线程对象调用 start() 方法之后,该线程处于就绪状态
- 处于就绪状态 Python 解释器会为其创建方法调用栈和程序计数器
- 调用 start() 方法来启动线程,系统会把该 run() 方法当成线程执行体来处理
- 若未调用start()方法,run方法也就是一个普通方法,而不是线程执行体,而普通方法只在主线程中执行
- 只能对处于新建状态的线程调用 start(),若对同一个线程重复调用 start(), 将引发 RuntimeError 异常
- 运行
- 线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略
- 对于处于可用的运行状态线程,必须由该线程主动放弃其所占用的资源
- 即当一个线程调用了它的 sleep() 或 yield() 方法后才会放弃其所占用的资源
- 阻塞
- 当发生如下情况时,线程将会进入阻塞状态
- 线程调用 sleep() 方法主动放弃其所占用的处理器资源
- 线程调用了一个阻塞式 I/O 方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个锁对象,但该锁对象正被其他线程所持有
- 线程在等待某个通知(Notify)
- 被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它
- 线程从阻塞状态只能进入就绪状态,无法直接进入运行状态
- 当发生如下情况时,线程将会进入阻塞状态
- 死亡
- 线程会以如下方式结束
- run() 方法或代表线程执行体的 target 函数执行完成,线程正常结束
- 线程抛出一个未捕获的 Exception 或 Error
- 当主线程结束时,其他线程不受任何影响,并不会随之结束
- 一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受主线程的影响
- 线程会以如下方式结束
-
线程对象
- 测试线程是否死亡 is_alive()
- 当线程处于就绪、运行、阻塞三种状态时,该方法将返回 True
- 提示
- 不要对处于死亡状态的线程调用 start() 方法,程序只能对处于新建状态的线程调用 start() 方法
- 测试线程是否死亡 is_alive()
-
让并发线程顺序执行 join方法
- Thread 提供了让一个线程等待另一个线程完成的 join() 方法
- 当在某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到被 join() 方法加入的 join 线程执行完毕
- join() 方法通常由使用线程的程序调用
- Thread 提供了让一个线程等待另一个线程完成的 join() 方法
-
关键点
- 被join的线程
- 多线程默认是抢占式获取cpu时间片
- 调用其他线程的 join() 方法的线程会被阻塞,等待被join线程执行完成
python守护线程及作用
- 守护线程
Daemon Thread
- 应用场景 Python 解释器的垃圾回收线程
- 特征
- 如果所有的前台线程都死亡了,那么后台线程会自动死亡
- 创建后台线程
- 主动将线程的
daemon
属性设置为True
,必须在start()方法调用之前进行 - 后台线程启动的线程默认是后台线程
- 主动将线程的
线程安全
- 问题产生
- 当使用多个线程来访问同一个数据时,易出现线程安全问题
- 出现偶然错误的原因在于线程调度器调度的不确定性
- 同步锁(Lock)
- 锁提供了对共享资源的独占访问
- threading模块提供了锁Lock 和RLock类
acquire(blocking=True, timeout=-1)
加锁release()
:释放锁
- Lock VS RLock
threading.Lock
它是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取threading.RLock
它代表可重入锁(Reentrant Lock),在同一个线程中可以对它进行多次锁定,也可以多次释放
- 线程安全类肯有如下特征
- 该类的对象可以被多个线程安全的访问
- 每个线程调用任意方法之后,都将得到正确结果
- 换而言之,不可变类总是线程安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全
- 并发线程在任意时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),对共享资源的修改加锁
- 流程 加锁→修改→释放锁
- 减少线程安全所带来的负面影响,可用策略如下
- 不对线程安全类的所有方法都进行同步,只对那些会改变竞争共享资源)的方法进行同步(用锁)
- 如果可变类有两种运行环境,单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本
- 在单线程环境中使用钱程不安全版本以保证性能,在多线程环境中使用线程安全版本
- 小结
- 用锁同步的范围最小化
死锁
- 产生死锁情况
- 当两个线程相互等待对方释放同步监视器时就会发生死锁
- 后果: 出现死锁,无异常无提示,所有线程都处于阻塞状态,无法继续
- 避免出现死锁
- 避免多次锁定
- 具有相同的加锁顺序
- 使用定时锁
- 死锁检测
Condition线程通信
- 现实问题
- 通常程序无法准确控制线程的轮换执行
- 但Python 可通过线程通信来保证线程协调运行
- 应用场景
- 对多个线程指定轮次交互,类似于对弈多线程一来一往,
- Condition 对象与 Lock 对象组合使用,可以为每个对象提供多个等待集(wait-set)
- 借助Condition对象来保持线程协调
- 将 Condition 对象与 Lock 对象组合使用,可以为每个对象提供多个等待集(wait-set)
- 使用 Condition 可以让那些己经得到 Lock 对象却无法继续执行的线程释放 Lock 对象
- Condition 对象也可以唤醒其他处于等待状态的线程,Condition 对象总是需要有对应的 Lock 对象
- Condition类方法
acquire([timeout])/release()
调用 Condition 关联的 Lock 的 acquire() 或 release() 方法wait([timeout])
- 导致当前线程进入 Condition 的等待池等待通知并释放锁,
- 直到其他线程调用该 Condition 的 notify() 或 notify_all() 方法来唤醒该线程
notify()
唤醒在该 Condition 等待池中的单个线程并通知它,收到通知的线程将自动调用 acquire() 方法尝试加锁notify_all()
唤醒在该 Condition 等待池中等待的所有线程并通知它们
Queue队列实现线程通信
- queue模块下的阻塞队列
queue.Queue(maxsize=0)
代表 FIFO(先进先出)的常规队列queue.LifoQueue(maxsize=0)
代表 LIFO(后进先出)的队列PriorityQueue(maxsize=0)
代表优先级队列,优先级最小的元素先出队列
- 常用属性和方法
- Queue.qsize() 返回队列的实际大小
- Queue.empty() 判断队列是否为空
- Queue.full() 判断队列是否已满
- Queue.put(item, block=True, timeout=None) 向队列中放入元素,队列己满,且 block 参数为 True(阻塞)
- Queue.put_nowait(item):向队列中放入元素,不阻塞
- Queue.get(item, block=True, timeout=None):从队列中取出元素(消费元素)
- Queue.get_nowait(item):从队列中取出元素,不阻塞
- 特性
- 当程序试图向已满队列put()放入元素时,将会阻塞线程,与此类似向已空队列get()亦会阻塞
- 应用场景 使用Queue阻塞队列特性,实现线程通信
- 本质
- 利用队列的阻塞性实现通信,因多线程本身无序抢占式执行
Python Event实现线程通信
- 事件线程通信机制
- 一个线程发出event
- 另一个线程通过Event被触发
- Event如下方法
- is_set():该方法返回 Event 的内部旗标是否为True
- set() 该方法将会把 Event 的内部旗标设置为 True,并唤醒所有处于等待状态的线程
- clear() 该方法将 Event 的内部旗标设置为 False,通常接下来会调用 wait() 方法来阻塞当前线程
- wait(timeout=None):该方法会阻塞当前线程
- 主线程调用 Event 的 set() 方法将 Event 的内部旗标设直为 True,被event.wait阻塞的线程才会进行后续
- Event优点
- 类似于 Condition 和旗标的结合体,但 Event 本身并不带 Lock 对象
python 线程池
-
作用
- 使用线程池可以有效地控制系统中并发线程的数量
- 线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数
-
线程池使用
- 线程池的基类是
concurrent.futures
模块中的Executor
ThreadPoolExecutor
用于创建程池ProcessPoolExecutor
用于创建进程池
Exectuor
常用方法submit(fn, *args, **kwargs)
分散返回结果- 将 fn 函数提交给线程池,后者为fn函数的参数, 返回Future对象
map(func, *iterables, timeout=None, chunksize=1)
统一返回结果- 为
iterables
每个元素启动线程,并收集返回每个线程的执行结果 - 该函数将会启动多个线程,以异步方式立即对
iterables
执行map
处理
- 为
shutdown(wait=True)
关闭线程池
Future
类对象(异步结果类)- Future 类主要用于获取线程任务函数的返回值
- 程序将 task 函数提交(submit)给线程池后,submit 方法会返回一个 Future 对象
- Future方法
cancel()
取消该 Future 代表的线程任务cancelled()
返回 Future 代表的线程任务是否被成功取消running()
done()
result(timeout=None)
获取该 Future 代表的线程任务最后返回的结果exception(timeout=None)
获取该 Future 代表的线程任务所引发的异常add_done_callback(fn)
为该 Future 代表的线程任务注册一个“回调函数”,当任务成功完成时,程序会自动触发该fn函数
- 线程池的基类是
-
线程池执行线程任务步骤
- 调用
ThreadPoolExecutor
类的构造器创建一个线程池 - 定义一个普通函数作为线程任务,即通俗意义上的线程执行体
- 调用
ThreadPoolExecutor
对象的 submit() 方法来提交线程任务, 得到Future类实例 - 当不想提交任何任务时,调用
ThreadPoolExecutor
对象的shutdown()
方法来关闭线程池
- 调用
-
本质
- 该线程池负责启动线程,执行作为线程执行体而提交的任务函数
- 线程池实现了上下文管理协议(Context Manage Protocol),程序可以使用 with 语句来管理线程池
线程局部变量
local()
返回线程局部变量- 简化多线程井发访问的编程处理
- 使用线程局部变量可以很简捷地隔离多线程访问的竞争资源
- 线程局部变量(Thread Local Variable)的功用
- 为每一个使用该变量的线程都提供一个变量的副本
- 从线程的角度看,就好像每一个线程都完全拥有该变量一样
- 本质
- 为了解决多线程中对共享资源的访问冲突
- 在普通的同步机制中,是通过为对象加锁来实现多个线程对共享资源的安全访问
- 线程局部变量将需要并发访问的资源复制多份
- 线程局部变量(复制) VS 同步机制 (引用)
- 同步机制是多个线程之间进行通信的有效方式,目的在于多个线程对共享资源的并发访问
- 线程局部变量是为了隔离多个线程的数据共享,从根本上避免多个钱程之间对共享资源(变量)的竞争,无需多线程同步
- 应用场景
- 多个线程之间需要共享资源,以实现线程通信,则使用同步机制
- 仅仅需要隔离多个线程之间的共享冲突,则可以使用线程局部变量
python线程定时器类Timer
-
Thread
类有一个Timer
子类- 该子类可用于控制指定函数在特定时间内执行一次, 类似于js中的 settimeout
-
schedule
任务调度器- 复杂的任务调度
sched.scheduler
类,该类代表一个任务调度器sched.scheduler(timefunc=time.monotonic, delayfunc=time.sleep)
timefunc
该参数指定生成时间戳的时间函数,默认使用time.monotonic
来生成时间戳。delayfunc
该参数指定阻塞程序的函数,默认使用time.sleep
函数来阻塞程序
sched.scheduler
调度器支持如下常用属性和方法scheduler.enterabs(time, priority, action, argument=(), kwargs={})
- 该方法返回一个 event,它可作为 cancel() 方法的参数用于取消该调度
scheduler.enter(delay, priority, action, argument=(),kwargs={})
- delay 参数用于指定多少秒之后执行 action 任务
scheduler.cancel(event)
取消任务, event 参数应是当前调度队列中的 eventscheduler.empty()
判断当前该调度器的调度队列是否为空scheduler.run(blocking=True)
运行所有需要调度的任务scheduler.queue
该只读属性返回该调度器的调度队列