threading介绍
这个是模块在较低级的模块 _thread 基础上建立较高级的线程接口,大多数情况我们使用threading就够用了。多线程的应用场景是进行多个任务处理时。由于线程是操作系统直接支持的执行单元,我们可以通过建立多个线程来实现多个任务的处理,使它们同步进行(宏观看起来是这样的,实际上是各个线程交替工作)。
threading用法 (文档地址)
- threading.active_count() 返回当前存活着的Tread对象个数
- threading.current_thread() 返回当前正在运行的线程的Tread对象
- threading.enumerate() 返回一个列表,列表里面是还存活的Tread对象
- threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)创建线程,直接使用Tread类这是一种方法,另一种方法思新建一个类然后继承threading.Thread。
- group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。
- target 是用于 run()方法调用的可调用对象。默认是 None,表示不需要调用任何方法。
- name 是线程名称。默认情况下,由 “Thread-N” 格式构成一个唯一的名称,其中 N 是小的十进制数。
- args 是用于调用目标函数的参数元组。默认是 ()。
- kwargs 是用于调用目标函数的关键字参数字典。默认是 {}。
- 如果不是 None,daemon 参数将显式地设置该线程是否为守护模式。 如果是 None (默认值),线程将继承当前线程的守护模式属性。
- Thread类的start()方法用来开始一个线程。
示例:
import threading,time
def thread_1():
time.sleep(0.1) # 延迟0.1秒
print(threading.active_count()) # 输出当前运行的线程
print(threading.current_thread())
def main_thread():
added_thread = threading.Thread(target=thread_1, name='thread_job') # 创建一个线程(直接使用Tread类创建线程对象)
added_thread.start() # 开始一个线程
print(threading.enumerate()) # 返回一个Tread列表
print(threading.current_thread())
if __name__ == '__main__':
main_thread()
结果
- hread类的join(timeout=None)方法会让开启线程的线程(一般指主线程)等待,阻塞这个线程,直到这个线程运行结束才结束等待。timeout的参数值为浮点数,用于设置操作超时的时间。
import threading,time
def thread_1():
time.sleep(0.1)
print('thread_1')
def thread_2():
time.sleep(0.1)
print('thread_2')
def main_thread():
added_thread_1 = threading.Thread(target=thread_1)
added_thread_2 = threading.Thread(target=thread_2)
added_thread_1.start()
added_thread_2.start()
added_thread_2.join() # 使主线程进入等待
print('main_thread')
if __name__ == '__main__':
main_thread()
结果:
将added_thread_2.join() # 使主线程进入等待注释掉
可以看到join会使主线程进入等待直到调用join的线程结束(其他线程不受影响)。
- threading.Lock 锁对象,可以通过它来创建锁被创建时为非锁定状态,原始锁有两种状态锁定和非锁定。
- Lock对象acquire(blocking=True, timeout=-1)方法,获得锁。
- 当锁的状态为非锁定时, acquire() 将锁状态改为锁定并立即返回(即执行下面的程序)。
- 当状态是锁定时, acquire() 将阻塞(将发起获得锁的线程挂起直到锁被释放获得锁),当其他线程调用 release() 将锁改为非锁定状态后(即锁被释放后), 被挂起线程的acquire() 将获得锁且重置其为锁定状态并返回(与1一致)。
- blocking 参数为bool值(默认True),可以阻塞或非阻塞地获得锁(即无法获得锁时是否阻塞线程)
- timeout 参数为浮点数(默认-1),当无法获得锁时,timeout为正决定阻塞的时间,为负数时为无限等待。,blocking为False时timeout无作用(不阻塞当然涉及不到阻塞的时间)
- Lock对象release()方法,释放锁。
- 当锁被锁定,将它重置为未锁定,并返回。如果其他线程正在等待这个锁解锁而被阻塞,只允许其中一个允许。
- 在未锁定的锁调用时,会引发 RuntimeError 异常。
- Lock对象的locked()方法,用来判断是否获得了锁。
锁,一般用在两个线程同时使用一个公共变量的情况下。为了防止两个线程同时修改变量导致的混乱。
import threading,time
thread_lock = threading.Lock() # 创建锁
share = ''
def thread_1():
thread_lock.acquire() # 锁定锁并返回
global share
for i in range(10):
share = 'hi'
print(share)
thread_lock.release()
def thread_2():
thread_lock.acquire()
global share
for i in range(10):
share = 'hello'
print(share)
thread_lock.release()
if __name__ == '__main__':
thread1=threading.Thread(target=thread_1)
thread2=threading.Thread(target=thread_2)
thread1.start()
thread2.start()
结果:
当第一个线程使用share(公共变量)时,acquire将锁锁定。第二个线程的acquire因无法获得锁而进入阻塞等待锁的释放,当hi打印完后(10次)释放锁。第二个线程获得锁,结束阻塞并返回开始打印hello直到所有线程完成。
若第二个线程不加锁的结果就混乱了如图:
为啥会这样呢,因为python的多线程是多个线程交替进行的。当第一个线程运行完一条语句后下一条语句的运行不一定还是第一个线程中的语句这个调度是由GIL(全局解释器锁)决定的,因为第二个线程没有acquire语句不会因为无法获得锁而进入阻塞,所以第二个线程是会进行的,这就导致了乱序。
- threading.Event() 创建事件对象(初始为False),这是线程之间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号。
- Event 对象的is_set()方法,当内部标志为True返回True
- Event对象的set()方法,将内部标志设置为True
- Eevet对象的wait(timeout=None)方法,如果内部标志为False,则线程进入阻塞直到内部标志改变为True。
- Event对象的clear()方法,设置内部标志为False
import threading
event = threading.Event() # 创建事件标志初始为False
def thread_1():
print("I",end=' ')
event.wait() # 为False进入阻塞
print("you",end='!')
def thread_2():
print("love",end=' ')
event.set() # 设置为True,线程1继续进行
if __name__ == '__main__':
thread_1 = threading.Thread(target=thread_1)
thread_2 = threading.Thread(target=thread_2)
thread_1.start()
thread_2.start()
结果
注意
python的多线程并不是所有的线程同时进行,而是多个线程交替进行。这是因为GIL的作用(全局解释器锁),python解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行。这就意味着python的多线程并不能进行多核运算,它最多只能完全利用一个cpu核心。它的这个特点决定了它不能胜任计算密集型的工作(因为它不能利用多核也就不能充分利用cpu提高效率),使用python的多线程来提高计算密集型程序的效率不但不可行可能还会适得其反。但对应I/O密集型的工作python多线程可以胜任,因为影响这种工作效率的因素主要是I/O操作(数据的读取),而多线程可以通过线程的调用在一个线程等待I/O时,开起另一个线程。从而提高cpu利用效率。