最近学了下python中多线程的一些知识,在此总结一下。
概述
几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能。
进程是系统进行资源分配和调度的一个独立单位。包括三个特征:独立性,动态性,并发性。
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。
线程也被称为轻量级进程,线程是进程的执行单元。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。
操作系统可以同时执行多个任务,每一个任务就是一个进程;进程可以同时执行多个任务,每一个任务就是一个线程。
多线程优点:
- 进程之间不能共享内存,但线程之间共享内存非常容易。
- 操作系统在创建进程时,需要为进程重新分配系统资源,但创建线程的代价则小得多。
- python语言内置了多线程功能支持,而不是单纯地作为底层操作系统地调度方式,从而简化了python的多线程编程。
线程创建和启动
import threading
def action(max):
pass
#创建线程,
#target指定该线程要调度的目标方法
#args指定一个元组,以位置参数的形式为target指定的函数传入参数
#kwargs指定一个字典,以关键字参数的形式为target传入参数
#daemon指定所创建的线程是否为后台线程
t1 = threading.Thread(target=action, args=(100,))
#启动线程
t1.start()
#返回当前正在执行的线程对象
t = threading.current_thread()
#返回调用它的线程名字,同t.name
name = t.getName()
线程的生命周期
在线程的生命周期里,要经过新建、就绪、运行、阻塞和死亡5种状态。
当程序创建了一个Thread对象后,该线程处于新建状态。
start后处于就绪状态。
至于该线程何时开始运行,取决于python解释器中线程调度器的调度。
当发生如下情况时,线程将会进入阻塞状态:
- 线程调用了sleep()方法主动放弃其所占用的处理器资源
- 线程调用了一个阻塞式I/O方法,在该方法返回之前,该线程被阻塞。
- 线程试图获得一个锁对象,但该锁对象正被其他线程所持有。
- 线程在等待某个通知。
控制线程
join方法,让一个线程等待另一个线程完成的方法。当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程执行完成。
后台线程,任务是为了其他线程提供服务。例如python解释器的垃圾回收线程。
创建后台线程有两种方式:
- 主动将线程的daemon属性设置为True
- 后台线程启动的线程默认是后台线程。
线程睡眠:通过time模块的sleep(secs)函数来实现。使线程阻塞secs秒。
线程同步
Thread的run方法不具有线程安全性——程序中有多个并发程序在修改同一个对象。
threading模块提供了Lock和RLock两个类,他们都提供了如下两个方法来加锁和释放锁:
- acquire(blocking=True, timeout=-1):请求对Lock或RLock加锁,其中timeout参数指定加锁多少秒。
- release():释放锁。
Lock和RLock的区别:
- Lock是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
- RLock代表可重入锁。在同一个线程中可以对它进行多次锁定,也可以多次释放。acquire和release方法必须成对出现。
代码模板:
class X:
#定义需要保证线程安全的方法
def m(self):
#加锁
self.lock.acquire()
try:
#需要保证线程安全的代码
#。。。方法体
#使用finally来保证释放锁
finally:
#修改完成,释放锁
self.lock.release()
通过使用Lock对象可以非常方便地实现线程安全的类,线程安全的类特征:
- 该类的对象可以被多个线程安全地访问
- 每个线程在调用该对象地任意方法之后,都将得到正确的结果,该对象依然保持合理的状态
死锁:当两个线程互相等待对方释放同步监视器。
一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,所有线程都处于阻塞状态,无法继续。
避免死锁的出现:
- 避免多次锁定:尽量避免同一个线程对多个Lock进行锁定
- 具有相同的加锁顺序:如果多个线程需要对多个Lock进行锁定,则应该保证他们以相同的顺序进行加锁
- 使用定时锁
- 死锁检测
线程通信
当线程在系统中运行时,线程的调度具有一定的透明性,通常程序无法准确控制线程的轮换执行,如果有需要,可通过线程通信来保证线程的协调运行。
使用Event控制线程通信:
一个线程发出一个Event,另一个线程可通过该Event被触发。
Event本身管理一个内部旗标,程序可以通过Event的set()方法将该旗标设置为True,也可以调用clear方法设置为False。可以调用wait方法来阻塞当前线程,知道Event的旗标为True
模板:
class X:
def m1(self):
self.lock.acquire()
#如果为True
if self.event.is_set():
#...
#...
self.event.clear()
self.lock.release()
self.event.wait()
else:
self.lock.release()
self.event.wait()
def m2(self):
self.lock.acquire()
#如果为False
if not self.event.is_set():
#...
#...
self.event.set()
self.lock.release()
self.event.wait()
else:
self.lock.release()
self.event.wait()
线程池
系统启动一个新线程的成本是比较高的,因为涉及到与操作系统的交互。如果程序需要创建大量生存期很短暂的线程时,应该考虑使用线程池。
线程池在系统启动时即创建大量空闲线程,程序只需要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行。当函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成K线状态,等待下一个函数。
线程池的基类是concurrent.futures模块中的Executor,Executor提供两个子类,ThreadPoolExecutor和ProcessPoolExecutor
Executor常用方法:
- submit(fn,*args,**kwargs)
Future常用方法:
- running():如果该Future代表的线程任务正在执行、不可被取消,该方法返回True
- done():如果该Future代表的线程任务被成功取消或执行完成,该方法返回True
- result(timeout=None):获取该Future如代表的线程任务最后返回的结果。如果线程任务未完成,则会阻塞当前线程
- add_done_callback(fn):为该Future代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该fn函数
例子:
from concurrent.futures import ThreadPoolExecutor
import threading
import time
def action(max):
pass
pool = ThreadPoolExecutor(max_workers=2)
future = pool.submit(action,50)
print(future.done())
print(future.result())
pool.shutdown()