多线程
什么是python多线程
多线程是加速程序计算的有效方式,Python的多线程模块 threading 是挺容易学习的。
线程在执行过程中与进程还是有区别的。每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
下面通过一个实例学习:
import threading
import time
import subprocess
def thread_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')
def main():
# 通过 threading.Thread 创建一个线程,
# 参数1 是线程函数;注意这里只是将函数的索引赋值给 target , 没有括号
# 参数2 是该线程的名字
added_thread = threading.Thread(target=thread_job, name='T1')
thread2 = threading.Thread(target=T2_job, name='T2')
# 启动线程
added_thread.start()
thread2.start()
# 阻塞调用线程直至线程的join() 方法被调用中止,后面的程序才会执行
added_thread.join()
thread2.join()
print('all done\n')
# 打印出当前的线程变量,打印正在运行的线程数量,打印正在运行的线程的list
print(threading.current_thread())
print(threading.active_count())
print(threading.enumerate())
if __name__ == '__main__':
main()
上述程序运行结果:
T1 start
T2 start
T2 finish
T1 finish
all done
<_MainThread(MainThread, started 38104)>
1
[<_MainThread(MainThread, started 38104)>]
Process finished with exit code 0
可以看到当两个线程的主函数执行完之后,后面的程序才开始执行。打印 all done
。
常用的线程模块及方法
Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。
-
threading 模块提供的其他方法:
-
threading.currentThread(): 返回当前的线程变量。
-
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
-
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
- run(): 用以表示线程活动的方法。
- start():启动线程活动。
- join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
存储进程结果 Queue
由于线程函数中不能使用 return
语句,于是我们考虑将线程函数中要保存的值放到队列中。
继续看一个实例:
import threading
import time
from queue import Queue
def job(l, q):
for i in range(len(l)):
l[i] = l[i]**2
# return l
# 调用线程不能用return,因此我们将它放入队列中
q.put(l)
def multithreading():
q = Queue()
# 使用一个list来存放创建的线程
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))
t.start()
threads.append(t)
# 将循环中给的当前线程附加到主线程里,即,执行完该线程才能往下执行
for thread in threads:
thread.join()
result = []
# 循坏四次取出队列中的值
for _ in range(4):
result.append(q.get())
print(result)
if __name__ == '__main__':
multithreading()
多线程执行的效率?
在人们的常识中,多进程,多线程都是通过并发的方式充分利用硬件资源提高程序的运行效率,但是在python中却不一定是这样。相反,这个功能有点鸡肋。
这是为什么呢?GIL
的存在使得多行程的运行效率真不一定高。GIL
待会说,先看多线程是不是鸡肋,我们用代码来说明。
import time
import threading
def decrement(n):
while n > 0:
n -= 1
start = time.time()
# 单线程
decrement(100000000)
cost = time.time() - start
print('single thread: ', cost)
# 多线程
start = time.time()
t1 = threading.Thread(target=decrement, args=[50000000])
t2 = threading.Thread(target=decrement, args=[50000000])
t1.start() # 启动线程,执行任务
t2.start() # 同上
t1.join() # 主线程阻塞,直到t1执行完成,主线程继续往后执行
t2.join() # 同上
cost = time.time() - start
print('multi thread:',cost)
运行结果;
single thread: 4.635588884353638
multi thread: 4.316659688949585
可以看到,使用多线程后效率并没有提升,相反,有时效率反而会下降。
GIL
是什么原因导致多线程不快反慢的呢?
原因就在于 GIL ,在 Cpython 解释器(Python语言的主流解释器)中,有一把全局解释锁(Global Interpreter Lock),在解释器解释执行 Python 代码时,先要得到这把锁,意味着,任何时候只可能有一个线程在执行代码,其它线程要想获得 CPU 执行代码指令,就必须先获得这把锁,如果锁被其它线程占用了,那么该线程就只能等待,直到占有该锁的线程释放锁才有执行代码指令的可能。
线程锁 Lock
python解释器在执行程序时,在任何时候只有一个线程在执行代码。我们定义的多个线程是在不停的切换,只是速度快到我们以为踏实多个线程并行,。那么这种情况下可能会出现一个问题:如果我们定义一个全局变量,而我们的线程函数中也使用了该变量,那么多个线程切换着执行可能会对该变量进行不同的运算。
这种情况下,就需要Python提供的进程锁 lock
了。在线程函数中调用锁,执行完在释放,使得整个过程不收其他进程干扰。
一个关于线程锁的示例:
import threading
def job1():
global A, lock
lock.acquire()
for i in range(5):
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()
# t2.join()
# t1.join()
执行结果:
job1 1
job1 2
job1 3
job1 4
job1 5
job2 15
job2 25
job2 35
job2 45
job2 55
job2 65
job2 75
job2 85
job2 95
job2 105
我们将线程函数中的结果打印出来,可以看到首先对全局变量A
进行+1
操作,循环打印5次。之后再线程函数job2
中执行+10
操作。
可以将线程锁去掉,其他部分保持不变,执行原程序,观察打印的结果是否还是如此整齐一致?
欢迎关注我的微信公众号,谈风月之余谈技术