python中的threading多线程模块
线程对象 锁和递归锁 事件对象 条件变量对象 信号量对象 定时器对象 栅栏对象 调用以上对象构造函数时,所有参数必须以关键字参数的形式传入
线程是程序执行流的最小单元,是由系统调度和分配CPU的基本单位,一个进程至少包括一个线程
线程在其他大部分语言中是并行运行的
但是在python中,由于CIL锁的存在,多个线程使用同一个CPU资源,且同一时间只有一个线程运行
线程之间通过频繁切换实现类似于并行的效果,且线程之间的切换是由系统决定的,不受人为控制
由于线程在同一个进程下,它们可以共享相同的上下文
线程之间的切换会有一定的系统开销,所以在CPU密集型python程序中使用多线程反而会降低运行效率
Python的标准库提供了两个模块:_thread(python2中名称是thread)和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。_thread有一些缺点,在threading得到了弥补,绝大多数情况下,我们只需要使用threading这个高级模块。
- threading相比于_thread模块来说更加高级,而_thread模块中的一些属性和threading模块有冲突。
- _thread模块只拥有一个同步原语,而threading模块则拥有很多。
- 在_thread模块中,主线程结束时,其他的线程也将强制性的结束
不过,也不要因此觉得_thread模块就需要被淘汰了,它只是不适用于我们这些简单的只是使用多线程的人,那些想访问线程更低层的专家多使用_thread模块来进行运作
threading模块的常用方法:
threading.active_count():返回当前存活的线程对象数量,等于enumerate()返回的列表长度
threading.current_thread():返回当前调用者的控制线程的Thread对象。如果调用者的控制线程不是利用threading创建,会返回一个功能受限的虚拟线程对象。
threading.get_ident():返回当前线程的非零整数“线程标识符”。它的值没有直接含义,可用于标识每个线程的资源。一个线程的标识在退出后可以被另一个线程重用。
threading.enumerate():以列表形式返回当前所有存活的线程对象。包含守护线程,current_thread()创建的虚拟线程对象和主线程。
threading.main_thread():返回主线程对象。一般情况下,主线程是Python解释器开始时创建的线程。
threading.settrace(func):为所有 threading 模块开始的线程设置追踪函数。在每个线程的run()方法被调用前,func 会被传递给 sys.settrace()
threading.setprofile(func):为所有threading模块开始的线程设置性能测试函数。在每个线程的run()方法被调用前,func会被传递给sys.setprofile()
threading.stack_size([size=0]):返回创建线程时用的堆栈大小。可选参数size指定之后新建的线程的堆栈大小,一定要是0(根据平台或者默认配置)或者最小是32,768(32KiB)的一个正整数。如果不支持改变线程堆栈大小,会抛出RuntimeError错误。如果指定的堆栈大小不合法,会抛出ValueError错误并且不会修改堆栈大小。32KiB是当前最小的能保证解释器有足够堆栈空间的堆栈大小。需要注意的是部分平台对于堆栈大小会有特定的限制,例如要求大于32KiB的堆栈大小或者需要根据系统内存页面的整数倍进行分配(4KiB页面比较普遍,在没有更具体信息的情况下,建议的方法是使用4096的倍数作为堆栈大小)。
threading模块的常量:
threading.TIMEOUT_MAX:阻塞函数( Lock.acquire(), RLock.acquire(), Condition.wait(), ...)中形参 timeout 允许的最大值。传入超过这个值的 timeout 会抛出 OverflowError 异常。
线程本地数据:是特定线程的数据。管理线程本地数据,需要创建一个local(或者一个子类型)的实例并在实例中储存属性:
mydata = threading.local()
mydata.x = 1
线程对象
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。
target 是用于 run() 方法调用的可调用对象。默认None表示不调用任何方法。
name 是线程名称。默认情况下由 "Thread-N" 格式构成一个唯一的名称,其中N是小的十进制数。
args 是用于调用目标函数的参数元组。
kwargs 是用于调用目标函数的关键字参数字典。
daemon 如果不是None/False,线程将被设置为守护模式,否则线程将继承当前线程的守护模式属性。
当线程对象一旦被创建,其活动一定会因调用线程的start()方法开始。这会在独立的控制线程调用 run() 方法。
一个线程可以被标记成一个"守护线程"。主线程结束以后会等待非守护线程都结束才会退出整个python程序而不会管守护线程是否终结,可以理解为转为后台运行。这个标志可以通过线程对象的daemon属性或者构造函数参数来设置。守护线程在程序关闭时会突然关闭,他们的资源(例如已经打开的文档,数据库事务等等)可能不会被正确释放。
线程对象的方法和属性:
start():开始线程活动。它在一个线程里最多只能被调用一次。它安排对象的run()方法在一个独立的控制进程中调用。如果同一个线程对象中调用这个方法的次数大于一次,会抛出RuntimeError。
run():代表线程活动的方法。标准的run()方法调用一个传递给对象构造函数的可调用对象,如果有,分别从 args 和 kwargs 参数中获取顺序和关键字参数。
join(timeout=None):等待,直到线程终结。这会阻塞调用这个方法的线程,直到被调用join()的线程终结,不管是正常终结还是抛出未处理异常或者直到发生超时。超时选项是可选的,因为join()总是返回None,所以要在join()后调用is_alive()才能判断是否发生超时,如果线程仍然存活,则join()超时。一个线程可以被join()很多次。如果尝试加入当前线程会导致死锁,join()会引起RuntimeError异常。如果尝试join()一个尚未开始的线程,也会抛出相同的异常。
is_alive():返回线程是否存活。一旦线程活动开始,该线程会被认为是‘存活的‘。当它的run()方法终结了(不管是正常的还是抛出未被处理的异常),就不是‘存活的‘。
name:只用于识别的字符串。它没有语义。多个线程可以赋予相同的名称。之前版本是通过getName()和setName()取值/设值
ident:这个线程的‘线程标识符‘,如果线程尚未开始则为None。这是个非零整数。当一个线程退出而另外一个线程被创建,线程标识符会被复用。即使线程退出后,仍可得到标识符。
daemon:一个表示这个线程是(True)否(False)守护线程的布尔值。一定要在调用start()前设置好,不然会抛出 RuntimeError 。之前版本是通过isDaemon()和setDaemon()取值/设值,设为True的话则主线程执行完毕后会将子线程回收掉,默认为False,主进程执行结束时不会回收子线程
锁对象和递归锁对象
锁对象:class threading.Lock,支持上下文管理协议。
一旦一个线程获得一个锁,会阻塞随后尝试获得锁的线程(包括这个获取了锁的线程自己再次尝试获取锁),直到它被释放;任何线程都可以释放它,不单指获得锁的线程。
acquire(blocking=True, timeout=-1):阻塞或非阻塞地获得锁。
当参数 blocking 设置为 True (缺省值),阻塞直到锁被释放,然后将锁锁定并返回True,如果设置了超时参数并发生超时的时候返回False。当参数 blocking 被设置为 False,将不会发生阻塞,如果成功获得锁,返回True,否则返回False。
另一个参数timeout=-1,表示等待获取锁的时间,-1(缺省值)表示一直等待。如果超过timeout设定的时间还没有获取到锁就会有获取锁失败
如何设定了timeout为正数,则不能设定blocking=False。因为设定了timeout为正数表明等待设定的时间会阻塞线程,和blocking=False是矛盾的。
release():释放一个锁,当锁被锁定,将它重置为未锁定。这个方法可以在任何线程中调用,不单指获得锁的线程。没有返回值。在未锁定的锁调用时会引发RuntimeError异常。
递归锁对象:class threading.RLock,递归锁也支持上下文管理协议。
这个锁可以由同一个线程获取多次,也就是说从一个已经获取了锁的线程中获取锁,其他线程获取的时候仍然会阻塞。
acquire()/release()方法必须是成对出现,只有最后一个release()方法才会真正的释放该线程的锁,允许其他线程获取。
递归锁对象对象拥有和锁对象相同的方法,除了递归锁的release()方法是只有当前线程拥有锁才能调用这个区别外其他用法相同。
需要注意的是Lock和RLock其实是工厂函数,返回平台支持的具体锁类中最有效的版本的实例。
事件对象
这是线程之间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号。
class threading.Event
实现事件对象的类。事件对象管理一个内部标志(初始时为false),调用 set() 方法可将其设置为true。调用 clear() 方法可将其设置为false。调用 wait() 方法将进入阻塞直到标志为true。
is_set():当且仅当内部标志为true时返回true。
set():将内部标志设置为true。所有正在等待这个事件的线程将被唤醒。当标志为true时,调用 wait() 方法的线程不会被被阻塞。
clear():将内部标志设置为false。之后调用 wait() 方法的线程将会被阻塞,直到调用 set() 方法将内部标志再次设置为true。
wait(timeout=None):阻塞线程直到内部变量为true。如果调用时内部标志为true,将立即返回。否则将阻塞线程,直到调用 set() 方法将标志设置为true或者发生可选的超时。
条件变量对象
class threading.Condition(lock=None)
一个条件变量对象允许一个或多个线程在被其它线程所通知之前进行等待。锁对象可以通过传入获得(必须为Lock或RLock对象),或者在缺省的情况下自动创建(RLock对象)。
当多个条件变量需要共享同一个锁时,传入一个锁很有用。锁是条件对象的一部分,你不必单独地跟踪它。条件变量服从上下文管理协议:使用with语句会在它包围的代码块内获取关联的锁。
这个类实际上就是继承自threading.Lock类,所以也拥有threading.Lock的方法,如acquire(blocking=True, timeout=-1)上锁和release()解锁
wait(timeout=None):该方法的运行原理
1、释放锁 2、阻塞(等待)直到其它线程调用notify()方法或notify_all()方法唤醒它,或者直到可选的超时发生。 3、一旦被唤醒,会进行排队(如果有多个线程被唤醒)按次序等待获取锁 4、当排序前一位锁被释放之后重新尝试获取锁并返回 以上4步为wait()方法在执行,在重新获取锁之后wait()方法会返回该锁,至此wait()方法才运行完毕 如果wait()方法重新获取锁时失败,将会引发 RuntimeError 异常 5、继续执行wait()方法之后的代码
当底层锁是个RLock,不会使用它的release()方法释放锁,因为当它被递归多次获取时,实际上可能无法解锁。相反,使用了RLock类的内部接口,即使多次递归获取它也能解锁它。 然后,在重新获取锁时,使用另一个内部接口来恢复递归级别。
wait_for(predicate, timeout=None)等待,直到条件计算为真。这个方法会重复地尝试调用wait()直到满足判断式或者发生超时。predicate应该是一个可调用对象而且它的返回值可被解释为一个布尔值。timeout参数给出最大等待时间,如果发生超时返回 False。
notify(n=1):唤醒n个等待这个条件的线程,如果没有线程在等待,这是一个空操作。如果调用线程在没有获得锁的情况下调用这个方法,会引发 RuntimeError 异常。依赖这个行为并不安全,优化的实现有时会唤醒超过n个线程。
notify_all():唤醒所有正在等待这个条件的线程。这个方法行为与 notify() 相似,但并不只唤醒单一线程,而是唤醒所有等待线程。如果调用线程在调用这个方法时没有获得锁,会引发 RuntimeError 异常。
注意:notify() 方法和 notify_all() 方法并不会释放锁,这意味着被唤醒的线程不会立即从它们的 wait() 方法调用中返回,而是会在调用 notify() 方法或 notify_all() 方法的线程最终放弃了锁的所有权后返回。
信号量对象
一个信号量管理一个内部计数器,该计数器因 acquire() 方法的调用而递减,因 release() 方法的调用而递增。 计数器的值永远不会小于零;当 acquire() 方法发现计数器为零时,将会阻塞,直到其它线程调用 release() 方法。
信号量对象也支持上下文管理协议 。
class threading.Semaphore(value=1)
该类实现信号量对象。信号量对象管理一个原子性的计数器,代表 release() 方法的调用次数减去 acquire() 的调用次数再加上一个初始值。如果需要, acquire() 方法将会阻塞直到可以返回而不会使得计数器变成负数。在没有显式给出 value 的值时,默认为1。
可选参数 value 赋予内部计数器初始值,默认值为 1 。如果 value 被赋予小于0的值,将会引发 ValueError 异常。
acquire(blocking=True, timeout=None):获取一个信号量。
在不带参数的情况下调用时:
如果在进入时,内部计数器的值大于0,将其减1并立即返回true。
如果在进入时,内部计数器的值为0,将会阻塞直到被 release() 方法的调用唤醒。一旦被唤醒(并且计数器值大于0),将计数器值减少1并返回true。线程会被每次 release() 方法的调用唤醒。线程被唤醒的次序是不确定的。
在参数 blocking 被设置为false的情况下调用,将不会发生阻塞。如果不带参数的调用会发生阻塞的话,带参数的调用在相同情况下将会立即返回false。否则,执行和不带参数的调用一样的操作并返回true。
当参数 timeout 不为 None 时,将最多阻塞 timeout 秒。如果未能在时间间隔内成功获取信号量,将返回false,否则返回true。
release():释放一个信号量,将内部计数器的值增加1。当计数器原先的值为0且有其它线程正在等待它再次大于0时,唤醒正在等待的线程。
class threading.BoundedSemaphore(value=1)
该类实现有界信号量。有界信号量通过检查以确保它当前的值不会超过初始值。如果超过了初始值,将会引发 ValueError 异常。在大多情况下,信号量用于保护数量有限的资源。如果信号量被释放的次数过多,则表明出现了错误。没有指定时, value 的值默认为1。
定时器对象
class threading.Timer(interval, function, args=None, kwargs=None)
创建一个定时器,表示一个操作应该在等待一定的时间之后运行,在经过 interval 秒的间隔事件后,将会用参数 args 和关键字参数 kwargs 调用 function。定时器在执行其操作之前等待的时间间隔可能与用户指定的时间间隔不完全相同。
start() 通过调用start() 方法启动定时器。
cancel():停止定时器并取消执行计时器将要执行的操作。仅当计时器仍处于等待状态时有效(在计时结束前)。
栅栏对象
栅栏类提供一个简单的同步原语,用于应对固定数量的线程需要彼此相互等待的情况。线程调用wait()方法后将阻塞,直到所有线程都调用了wait()方法。此时所有线程将被同时释放。
栅栏对象可以被多次使用,但进程的数量不能改变。
class threading.Barrier(parties, action=None, timeout=None)
创建一个需要 parties 个线程的栅栏对象。如果提供了可调用的 action 参数,它会在所有线程被释放时在其中一个线程中自动调用。 timeout 是默认的超时时间,如果没有在wait()方法中指定超时时间的话。
wait(timeout=None):冲出栅栏。当栅栏中所有线程都已经调用了这个函数,它们将同时被释放。如果提供了 timeout 参数,这里的 timeout 参数优先于创建栅栏对象时提供的 timeout 参数。
函数返回值是一个整数,取值范围在0到parties-1,在每个线程中的返回值不相同。可用于从所有线程中选择唯一的一个线程执行一些特别的工作。
如果创建栅栏对象时在构造函数中提供了 action 参数,它将在其中一个线程释放前被调用。如果此调用引发了异常,栅栏对象将进入损坏态。
如果发生了超时,栅栏对象将进入破损态。
如果栅栏对象进入破损态,或重置栅栏时仍有线程等待释放,将会引发 BrokenBarrierError 异常。
reset():重置栅栏为默认的初始态。如果栅栏中仍有线程等待释放,这些线程将会收到 BrokenBarrierError 异常。
注意使用此函数时,如果有某些线程状态未知,则可能需其它的同步来确保线程已被释放。如果栅栏进入了破损态,最好废弃它并新建一个栅栏。
abort():使栅栏进入破损态。这将导致所有已经调用和未来调用的 wait() 方法中引发 BrokenBarrierError 异常。使用这个方法的一种情况是需要中止程序以避免死锁。更好的方式是:创建栅栏时提供一个合理的超时时间,来自动避免某个线程出错。
parties:冲出栅栏所需要的线程数量。
n_waiting:当前时刻正在栅栏中阻塞的线程数量。
broken:一个布尔值,值为 True 表明栅栏为破损态。
exception threading.BrokenBarrierError:异常类,是 RuntimeError 异常的子类,在 Barrier 对象重置时仍有线程阻塞时和对象进入破损态时被引发。