- Python 线程threading
- Python多线程适用于I/O密集型
- Python多线程的工作过程
- 构造函数
- 线程函数
- start()
- join(timeout=None)
- 创建线程
- 线程加锁
- 5种线程锁
- join函数
- 第一种:Python多线程默认情况(非守护线程)
- 第二种:守护线程
- 第三种:加入join方法设置同步(非守护线程,且join不设置超时)
- 第四种:加入join方法设置同步(非守护线程,join设置超时)
- 第五种:加入join方法设置同步(*守护线程*,join设置超时)
GIL的全称是Global Interpreter Lock(全局解释器锁),为了数据安全,GIL保证同一时间只能有一个线程拿到数据。所以,在python中,同时只能执行一个线程。
而IO密集型,多线程能够有效提升效率( 单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率 )。所以python多线程对IO密集型代码比较友好。
而CPU密集型( 各种循环处理、计算等等 ),由于计算工作多,计时器很快就会达到阈值,然后触发GIL的释放与再竞争( 多个线程来回切换当然是需要消耗资源的 ),所以python多线程对CPU密集型代码并不友好。
Python多线程的工作过程Python在使用多线程的时候,调用的是c语言的原生线程。
- 拿到公共数据
- 申请GIL(全局解释器锁)
- python解释器调用os原生线程
- os操作cpu执行运算
- 当该线程执行时间到后,无论运算是否已经执行完,GIL都被要求释放
- 由其他进程重复上面的过程
- 等其他进程执行完后,又会切换到之前的线程(从他记录的上下文继续执行),整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)。
笔记:有些没看懂?
构造函数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,将显式地设置该线程是否为守护模式。 如果是 None (默认值),线程将继承当前线程的守护模式属性。
3.3 版及以上才具有该属性。
注意:一定要在调用 start() 前设置好,不然会抛出 RuntimeError 。
初始值继承于创建线程;主线程不是守护线程,因此主线程创建的所有线程默认都是 daemon = False。
当没有存活的非守护线程时,整个Python程序才会退出。 -
如果子类型**重载了构造函数,**它一定要确保在做任何事前,先发起调用基类构造器(Thread.init())。
名词解释:守护模式
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”。Python 解释器的垃圾回收线程就是典型的后台线程。
后台线程有一个特征,如果所有的前台线程都死亡了,那么后台线程会自动死亡。
开始线程活动。
它在一个线程里最多只能被调用一次。它安排对象的 run() 方法在一个独立的控制进程中调用。
如果同一个线程对象中调用这个方法的次数大于一次,会抛出 RuntimeError 。
join(timeout=None)等待,直到线程终结。
这会阻塞调用这个方法的线程,直到被调用 join() 的线程终结。
- 不管是正常终结
- 还是抛出未处理异常
- 或者直到发生超时,超时选项是可选的。
当 timeout 参数存在而且不是 None 时,它应该是一个用于指定操作超时的以秒为单位的浮点数(或者分数)。
因为 join() 总是返回 None ,所以你一定要在 join() 后调用is_alive()才能判断是否发生超时 ,如果线程仍然存活,则 join() 超时。
当 timeout 参数不存在或者是 None ,这个操作会阻塞直到线程终结。
一个线程可以被 join() 很多次。
- 如果尝试加入当前线程会导致死锁, join() 会引起 RuntimeError 异常。
- 如果尝试 join() 一个尚未开始的线程,也会抛出相同的异常。
import threading # 线程模块
import time
def run(n):
print("task", n)
time.sleep(1)
print('2s')
time.sleep(1)
print('3s')
if __name__ == '__main__':
th = threading.Thread(target=run,name="thread_1" args=("thread 1",), daemon=True) # 创建线程
# 把子进程设置为守护线程,**必须在start()之前设置
th.setDaemon(True) # 设置守护线程,其实在创建时已经 设置了 daemon=True
th.start()
# 设置主线程等待子线程结束
th.join()
print("end")
线程加锁
由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据(笔记:Python基础数据类型、列表、元组、字典都是线程安全的,因此不会导致程序崩溃,但会导致数据出现未知值,即脏数据),
所以出现了线程锁,即同一时刻允许一个线程执行操作。线程锁用于锁定资源,可以定义多个锁,像下面的代码,当需要独占某一个资源时,任何一个锁都可以锁定这个资源,就好比你用不同的锁都可以把这个相同的门锁住一样。
由于线程之间是进行随机调度的,如果有多个线程同时操作一个对象,并且没有很好地保护该对象,会造成程序结果的不可预期,我们因此也称为“线程不安全”。
为了防止上面情况的发生,就出现了锁。
- 同步锁
- 递归锁
- 条件锁
- 事件锁
- 信号量锁
我在5种Python线程锁中有详细讲述。
join函数
概念补充:
- 在Python多线程编程中,join方法的作用是线程同步
- 守护线程,是为守护别人而存在,当设置为守护线程后,被守护的主线程不存在后,守护线程也自然不存在
以下分5种不同的形式解释join在多线程编程中的用处
第一种:Python多线程默认情况(非守护线程)Python多线程的默认情况(设置线程setDaemon(False)),主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束
笔记:setDaemon(False) 即 该线程被设置为非守护线程;主程序退出后,子线程不会自动退出。
import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(3)
print("线程1奉命报道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(8)
print("线程2奉命报道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "n")
tsk = [] # 线程列表
# 创建并开启线程1
thread1 = threading.Thread(target = doWaiting1)
thread1.start() # start()函数 实际调用 RUN()函数(Python调用的是 C语言的线程)
tsk.append(thread1)
# 创建并开启线程2
thread2 = threading.Thread(target = doWaiting2)
thread2.start()
tsk.append(thread2)
# 计时程序
print('start join: ' + time.strftime('%H:%M:%S') )
print('end join: ' + time.strftime('%H:%M:%S') )
运行结果
start waiting1: 20:03:30 start waiting2: 20:03:30 start join: 20:03:30 end join: 20:03:30 # 此处主线程已经结束,而子线程还在继续工作 线程1奉命报道 # 子线程1 继续在工作 stop waiting1: 20:03:33 线程2奉命报道 stop waiting2: 20:03:38
结论:
- 计时程序属于主线程,整个主线程在开启线程1和线程2后,进入计时模块,主线程结束
- 主线程结束,但并没有影响线程1和线程2的运行,故后面线程1和线程2仍然跑来报道,至此整个程序才完全结束
开启线程的**setDaemon(True),**设置子线程为守护线程,实现主程序结束,子程序立马全部结束功能
import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(3)
print("线程1奉命报道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(8)
print("线程2奉命报道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "n")
tsk = []
# 创建并开启线程1
thread1 = threading.Thread(target = doWaiting1)
thread1.setDaemon(True)
thread1.start()
tsk.append(thread1)
# 创建并开启线程2
thread2 = threading.Thread(target = doWaiting2)
thread2.setDaemon(True)
thread2.start()
tsk.append(thread2)
print('start join: ' + time.strftime('%H:%M:%S') )
print('end join: ' + time.strftime('%H:%M:%S') )
运行结果:
start waiting1: 20:10:04 start waiting2: 20:10:04 start join: 20:10:04 end join: 20:10:04 # 主线程结束后,子线程立刻结束,无论处于什么状态。
结论:
- 主线程结束后,无论子线程1,2是否运行完成,都结束不再往下继续运行
非守护线程,主程序将一直等待子程序全部运行完成才结束
import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(3)
print("线程1奉命报道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(8)
print("线程2奉命报道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "n")
tsk = []
# 创建并开启线程1
thread1 = threading.Thread(target = doWaiting1)
thread1.start()
tsk.append(thread1)
# 创建并开启线程2
thread2 = threading.Thread(target = doWaiting2)
thread2.start()
tsk.append(thread2)
print('start join: ' + time.strftime('%H:%M:%S') )
for t in tsk:
print('%s线程到了'%t)
t.join() # 线程join() 即:两个线程再次回合,全部完成后才能继续进行下一行
print('end join: ' + time.strftime('%H:%M:%S') )
运行结果:
start waiting1: 20:14:35 start waiting2: 20:14:35 start join: 20:14:35线程到了 线程1奉命报道 stop waiting1: 20:14:38 线程到了 线程2奉命报道 stop waiting2: 20:14:43 end join: 20:14:43 # 两个线程执行完毕后,才能运行至此
结论:
- 使用join函数,主线程将被阻塞(即:主线程,指定是创建子线程的线程),一直等待被使用了join方法的线程运行完成
- start join 是在35秒,stop waiting1在38秒,刚好sleep了3秒,stop waiting2是43秒,刚好sleep了8秒,这也说明,线程1和2是基本同时运行的,但由于执行所消耗的时间不一致,所以阻塞所用的时间也是不一样的,最终end join时间是最后线程运行完,整个程序就中止在43秒
- 将所有的线程放入一个列表,通过循环对列表中的所有线程使用join方法判断,也是为了保证全部子线程都能全部运行完成,主线程才退出
给join设置timeout数值,判断等待多后子线程还没有完成,则主线程不再等待
笔记:join设置超时后,判断依据为 子线程执行完毕 | 超时 (逻辑或的关系),即两个条件谁先为真,就向下执行。
import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(2)
print("线程1奉命报道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(8)
print("线程2奉命报道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "n")
tsk = []
# 创建并开启线程1
thread1 = threading.Thread(target = doWaiting1)
thread1.start()
tsk.append(thread1)
# 创建并开启线程2
thread2 = threading.Thread(target = doWaiting2)
thread2.start()
tsk.append(thread2)
print('start join: ' + time.strftime('%H:%M:%S') )
for t in tsk:
print("开始:"+time.strftime('%H:%M:%S'))
print('%s线程到了'%t)
t.join(5)
print("结束:" + time.strftime('%H:%M:%S'))
print('end join: ' + time.strftime('%H:%M:%S') )
运行结果:
start waiting1: 21:14:25 start waiting2: 21:14:25 start join: 21:14:25 开始:21:14:25线程到了 线程1奉命报道 stop waiting1: 21:14:27 结束:21:14:27 开始:21:14:27 线程到了 结束:21:14:32 end join: 21:14:32 线程2奉命报道 stop waiting2: 21:14:33
结论:
- 给join设置等待时间后,超过了等待时间后,主线程终止,但不影响子线程继续运行,等子线程全部运行完毕整个程序终止
- 所有线程可能出现的最大等待时间 timeout_total ≤ timeout * 线程数量
- 虽然timeout设置的是5s,但是线程1只需要2s,所以循环从开始到结束,只需消耗2s(21:14:27 - 21:14:25),到此循环就进入第二次,第二次等待仍可以分配5s(21:14:32 - 21:14:27),所以两次总共的等待是时间2+5=7s,但是线程2运行所需要时间是8s,而且8s是从21:14:25开始的,结束时间是21:14:33,因为join等待时间完了主程序结束了,但不影响线程2继续运行,所以在end join后,线程2仍然输出了报道结果(是因为没有开启守护线程)
- 个别文章解释join这个时间为:主线程会等待多个线程的timeout累加和,这个说法不准确,由3的推理可以得出,并非会一定等待“线程数* timeout”这么多时间,而是≤“线程数*timeout”,
超时且未处理完毕的子线程将被直接终止(符合守护线程的特性)
import threading, time
def doWaiting1():
print('start waiting1: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(2)
print("线程1奉命报道")
print('stop waiting1: ' + time.strftime('%H:%M:%S') + "n")
def doWaiting2():
print('start waiting2: ' + time.strftime('%H:%M:%S') + "n")
time.sleep(8)
print("线程2奉命报道")
print('stop waiting2: ', time.strftime('%H:%M:%S') + "n")
tsk = []
# 创建并开启线程1
thread1 = threading.Thread(target = doWaiting1)
thread1.setDaemon(True) # 守护线程
thread1.start()
tsk.append(thread1)
# 创建并开启线程2
thread2 = threading.Thread(target = doWaiting2)
thread2.setDaemon(True) # 守护线程
thread2.start()
tsk.append(thread2)
print('start join: ' + time.strftime('%H:%M:%S') )
for t in tsk:
print("开始:"+time.strftime('%H:%M:%S'))
print('%s线程到了'%t)
t.join(5)
print("结束:" + time.strftime('%H:%M:%S'))
print('end join: ' + time.strftime('%H:%M:%S') )
运行结果:
start waiting1: 21:24:14 start waiting2: 21:24:14 start join: 21:24:14 开始:21:24:14线程到了 线程1奉命报道 stop waiting1: 21:24:16 结束:21:24:16 开始:21:24:16 线程到了 结束:21:24:21 end join: 21:24:21
结论:
- 相比第四种,超时后主线程运行到end join则结束了,子线程2已经被终止停止运行



