原文:https://morvanzhou.github.io/learning-steps/
多线程是加速程序计算的有效方式,Python的多线程模块threading上手快速简单
threading模块的一些基本操作,如获取线程数,添加线程等
import threading
def thread_job():
print('This is a thread of %s' % threading.current_thread()) #查看现在正在运行的线程
print(threading.active_count()) #获取已激活的线程数
print(threading.enumerate()) # see the thread list,查看所有线程信息,一个<_MainThread(...)> 带多个
def main():
thread = threading.Thread(target=thread_job,) #添加线程,接收参数target代表这个线程要完成的任务,需自行定义
thread.start() #让线程开始工作
if __name__ == '__main__':
main()
This is a thread of <Thread(Thread-6, started 3488)>
6
[<HistorySavingThread(IPythonHistorySavingThread, started 5456)>, <Thread(Thread-6, started 3488)>, <Heartbeat(Thread-5, started daemon 3396)>,
<ParentPollerWindows(Thread-3, started daemon 1000)>, <_MainThread(MainThread, started 3228)>, <Thread(Thread-4, started daemon 5980)>]
(1)不加 join( ) 的结果
我们让 T1 线程工作的耗时增加
import threading
import time
def thread_job():
print("T1 start\n")
for i in range(10):
time.sleep(0.1) # 任务间隔0.1s
print("T1 finish\n")
added_thread = threading.Thread(target=thread_job, name='T1')
added_thread.start()
print("all done\n")
预想中输出的结果是否为:
T1 start
T1 finish
all done
但实际却是:
all done
T1 start
T1 finish
(2)加入 jion( ) 的结果
线程任务还未完成便输出all done。
如果要遵循顺序,可以在启动线程后对它调用join
- 使用join对控制多个线程的执行顺序非常关键。
举个例子,假设我们现在再加一个线程T2,T2的任务量较小,会比T1更快完成
def T1_job():
print("T1 start\n")
for i in range(10):
time.sleep(0.1)
print("T1 finish\n")
def T2_job():
print("T2 start\n")
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() # 开启T1
thread_2.start() # 开启T2
print("all done\n")
T1 start
all done
T2 start
T2 finish
T1 finish
现在T1和T2都没有join,all done的出现完全取决于两个线程的执行速度, 完全有可能T2 finish出现在all done之前。这种杂乱的执行方式是我们不能忍受的,因此要使用join加以控制。
我们试试在T1启动后,T2启动前加上thread_1.join():
def T1_job():
print("T1 start\n")
for i in range(10):
time.sleep(0.1)
print("T1 finish\n")
def T2_job():
print("T2 start\n")
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() # 开启T1
thread_1.join()
thread_2.start() # 开启T2
print("all done\n")
T1 start
T1 finish
T2 start
all done
T2 finish
可以看到,T2会等待T1结束后才开始运行。
如果我们在T2启动后放上thread_1.join()会怎么样呢?
def T1_job():
print("T1 start\n")
for i in range(10):
time.sleep(0.1)
print("T1 finish\n")
def T2_job():
print("T2 start\n")
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() # 开启T1
thread_2.start() # 开启T2
thread_1.join()
print("all done\n")
T1 start
T2 start
T2 finish
T1 finish
all done
T2在T1之后启动,并且因为T2任务量小会在T1之前完成;而T1也因为加了join,all done在它完成后才显示。
你也可以添加thread_2.join()进行尝试,但为了规避不必要的麻烦,推荐如下这种1221的V型排布:
def T1_job():
print("T1 start\n")
for i in range(10):
time.sleep(0.1)
print("T1 finish\n")
def T2_job():
print("T2 start\n")
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() # 开启T1
thread_2.start() # 开启T2
thread_2.join()
thread_1.join()
print("all done\n")
T1 start
T2 start
T2 finish
T1 finish
all done
"""代码实现功能: 将数据列表中的数据传入,使用四个线程处理,将结果保存在Queue中
线程执行完后,从Queue中获取存储的结果 """
import threading
import time
from queue import Queue
"""
定义一个被多线程调用的函数
函数的参数: 一个列表l和一个队列q
函数的功能: 对列表的每个元素进行平方计算,将结果保存在队列中
"""
def job(l,q):
for i in range (len(l)):
l[i] = l[i]**2
q.put(l) #多线程调用的函数不能用return返回值
"""
定义一个多线程函数
在多线程函数中定义一个Queue,用来保存返回值,代替return
定义一个多线程列表,初始化一个多维数据列表,用来处理
"""
def multithreading():
q =Queue() #q中存放返回值,代替return的返回值
threads = []
data = [[1,2,3],[3,4,5],[4,4,4],[5,5,5]]
"""在多线程函数中定义四个线程,启动线程
将每个线程添加到多线程的列表中"""
for i in range(4):
t = threading.Thread(target=job,args=(data[i],q))
# Thread首字母要大写,被调用的job函数没有括号,只是一个索引,参数在后面
t.start() #开始线程
threads.append(t) #把每个线程append到线程列表中
"""分别join四个线程到主线程"""
for thread in threads:
thread.join()
"""定义一个空的列表results
将四个线程运行后保存在队列中的结果返回给空列表results
"""
results = []
for _ in range(4):
results.append(q.get()) #q.get()按顺序从q中拿出一个值
print(results)
if __name__=='__main__':
multithreading()
[[1, 4, 9], [9, 16, 25], [16, 16, 16], [25, 25, 25]]
这次我们来看看为什么说 python 的多线程 threading 有时候并不是特别理想
- 最主要的原因是就是, Python 的设计上, 有一个必要的环节, 就是 Global Interpreter Lock (GIL). 这个东西让 Python 还是一次性只能处理一个东西.
摘抄了一段对于 GIL 的解释
(1)尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。 实际上,解释器被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。 GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势 (比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行)。
(2)在讨论普通的GIL之前,有一点要强调的是GIL只会影响到那些严重依赖CPU的程序(比如计算型的)。 如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用多线程就很合适, 因为它们大部分时间都在等待。实际上,你完全可以放心的创建几千个Python线程, 现代操作系统运行这么多线程没有任何压力,没啥可担心的。
我们创建一个 job, 分别用 threading 和 一般的方式执行这段程序.
并且创建一个 list 来存放我们要处理的数据.
在 Normal 的时候, 我们这个 list 扩展4倍,
在 threading 的时候, 我们建立4个线程, 并对运行时间进行对比.
import threading
from queue import Queue
import copy
import time
def job(l, q):
res = sum(l)
q.put(res)
def multithreading(l):
q = Queue()
threads = []
for i in range(4):
t = threading.Thread(target=job, args=(copy.copy(l), q), name='T%i' % i)
t.start()
threads.append(t)
[t.join() for t in threads]
total = 0
for _ in range(4):
total += q.get()
print(total)
def normal(l):
total = sum(l)
print(total)
if __name__ == '__main__':
l = list(range(1000000))
s_t = time.time()
normal(l*4)
print('normal: ',time.time()-s_t)
s_t = time.time()
multithreading(l)
print('multithreading: ', time.time()-s_t)
1999998000000
normal: 0.21001219749450684
1999998000000
multithreading: 0.20301151275634766
如果你成功运行整套程序, 你大概会有这样的输出. 我们的运算结果没错, 所以程序 threading 和 Normal 运行了一样多次的运算. 但是我们发现 threading 却没有快多少, 按理来说, 我们预期会要快3-4倍, 因为有建立4个线程, 但是并没有. 这就是其中的 GIL 在作怪.
(1)不用 Lock 的情况
import threading
"""函数一:全局变量A的值每次加1,循环10次,并打印"""
def job1():
global A
for i in range(10):
A+=1
print('job1',A)
"""函数二:全局变量A的值每次加10,循环10次,并打印"""
def job2():
global A
for i in range(10):
A+=10
print('job2',A)
"""主函数:定义两个线程,分别执行函数一和函数二"""
if __name__== '__main__':
lock = threading.Lock()
A=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()
t1.join()
t2.join()
job1job2 120
job1job2 230
job1job2 340
job1job2 450
job1job2 560
job1job2 670
job1job2 780
job1job2 890
job1job2 9100
job1job2 10110
可以看出,打印的结果非常混乱
(2)使用 Lock 的情况
lock在不同线程使用同一共享内存时,能够确保线程之间互不影响,使用lock的方法是, 在每个线程执行运算修改共享内存之前,执行lock.acquire()将共享内存上锁, 确保当前线程执行时,内存不会被其他线程访问,执行运算完毕后,使用lock.release()将锁打开, 保证其他的线程可以使用该共享内存。
import threading
def job1():
global A,lock
lock.acquire()
for i in range(10):
A+=1
print('job1',A)
lock.release()
def job2():
global A,lock
lock.acquire()
for i in range(10):
A+=10
print('job2',A)
lock.release()
if __name__== '__main__':
lock=threading.Lock()
A=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()
t1.join()
t2.join()
job1 1
job1 2
job1 3
job1 4
job1 5
job1 6
job1 7
job1 8
job1 9
job1 10
job2 20
job2 30
job2 40
job2 50
job2 60
job2 70
job2 80
job2 90
job2 100
job2 110
从打印结果来看,使用lock后,一个一个线程执行完。使用lock和不使用lock,最后打印输出的结果是不同的。