博主最近做一些深度学习模型,需要大批量处理图片数据,这个时候单线程操作,数据预处理很耗时,因此粗略地学习了下多线程和多进程的知识,写点简单的学习小结,章节构建如下:
目录
1 什么是线程和进程
2 实现多线程
2.1 threading 实现线程操作
2.1.1 添加线程
2.1.2 控制线程
2.2 线程锁 lock 的操作
2.3 GIL 锁
3 小结
对于操作系统而言,一个任务就是一个进程(Process),比方打开一个浏览器,打开一个笔记本。
有些进程内部同时做多件事情,即同时运行多个子任务,我们把这些子任务称为线程(Thread)。
所以一个进程至少有一个线程。
单个 CPU 可以执行多进程和多线程,即由操作系统在多个进程或者线程之间快速切换,使得每个进程或者线程短暂的交替运行。真正实现多线程需要多核 CPU 才可能实现。
当我们要执行多个任务的时候,我们可以采用多进程、多线程、多进程+多线程的模式来实现。
但是多个任务间可能有某种关联,需要相互通信和协调,比方我要完成任务1和任务2,才能开始做任务3和任务4。
Python 的标准库提供了两个模块:_thread
和 threading
,_thread
是低级模块,threading
是高级模块,对 _thread
进行了封装。通常,我们只需要使用 threading
这个高级模块。
导入线程模块:
import threading
获取已激活的线程数:
threading.active_count()
查看现在正在运行的线程:
threading.current_thread()
添加线程:
thread = threading.Thread(target=thread_job,)
注意:接收参数 target
代表这个线程要完成的任务,需自行定义
一段完成的小代码:
import threading
def t_job():
print('current_thread: %s' % threading.current_thread())
def main():
thread = threading.Thread(target=t_job,)
thread.start()
if __name__ == '__main__':
main()
current_thread:
注意:在 windows 平台下,线程(进程)的操作一定要放在 if __name__ == '__main__': 语句下执行。
线程开始运行:
thread.start()
控制多个线程的执行顺序:
thread.join()
为什么要控制多个线程的执行顺序?
以下面的例子为例介绍:
假设有 t1_job 和 t2_job 两个任务,第一个任务执行时间10s,第二个任务执行时间1s。创建以下代码:
import time
import threading
def t1_job():
print("T1 start\n")
for i in range(10):
time.sleep(1)
print("T1 finish\n")
def t2_job():
print("t2 start\n")
for i in range(10):
time.sleep(0.1)
print("t2 finish\n")
thread_1 = threading.Thread(target=t1_job, name='t1')
thread_2 = threading.Thread(target=t2_job, name='t2')
thread_1.start()
thread_2.start()
print("all jobs finished\n")
我们希望得到的输出是:
T1 start
T2 start
T2 finish
T1 finish
all jobs finished
然而实际的输出是:
T1 start
T2 start
T2 finish
all jobs finished
T1 finish
这种杂乱的执行方式是我们不能忍受的,因此要使用 join()
加以控制,推荐将每个线程对应的 join() 依次放在所有 start() 后面。
可将代码修改如下:
import time
import threading
def t1_job():
print("T1 start\n")
for i in range(10):
time.sleep(1)
print("T1 finish\n")
def t2_job():
print("t2 start\n")
for i in range(10):
time.sleep(0.1)
print("t2 finish\n")
thread_1 = threading.Thread(target=t1_job, name='t1')
thread_2 = threading.Thread(target=t2_job, name='t2')
thread_1.join()
thread_2.join()
print("all jobs finished\n")
在多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。举个栗子:
import time, threading
c = 0
def t_job(n):
global c
c += n
c -= n
def t_run(n):
for i in range(1000):
t_job(n)
t1 = threading.Thread(target=t_run, args=(5,))
t2 = threading.Thread(target=t_run, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(c)
注意:args 后面传到的参数需要加个逗号
我们定义了一个共享变量 c
,初始值为0
,并且启动两个线程,先存后取,理论上结果应该为0
,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,c
的结果就不一定是0
了。
因为当 CPU 运行时候:
c += n
相当于:
tmp = c + n
c = tmp
线程交替运行时:
初始值 c = 0
t1: tmp1 = c + 5 # tmp1 = 0 + 5 = 5
t2: tmp2 = c + 8 # tmp2 = 0 + 8 = 8
t2: c = tmp2 # c = 8
t1: c = x1 # c = 5
t1: tmp1 = c - 5 # tmp1 = 5 - 5 = 0
t1: c = tmp1 # c = 0
t2: tmp2 = c - 8 # tmp2 = 0 - 8 = -8
t2: c = tmp2 # c = -8
结果 c = -8
如果我们要确保 c
计算正确,就要给 t_job()
上一把锁,当某个线程开始执行 t_job()
时,该线程因为获得了锁,因此其他线程不能同时执行t_job()
,只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。
创建一个锁:
threading.Lock()
c = 0
lock = threading.Lock()
def t_run(n):
for i in range(10000):
lock.acquire()
try:
t_job(n)
finally:
lock.release()
当多个线程同时执行lock.acquire()
时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
注意:获得锁的线程用完后一定要释放锁,否则其他等待锁的线程将成为死线程。所以我们用try...finally
来确保锁一定会被释放。
锁的应用使得单线程能从头到尾不受干扰进行,但是也阻碍了多线程的进行。包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。另外,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
Python 的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何 Python 线程执行前,必须先获得 GIL 锁,然后,每执行100条字节码,解释器就自动释放 GIL 锁,让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在 Python 中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。Python 解释器由于设计时有 GIL 全局锁,导致了多线程无法利用多核。