1、基本概念
GIL是CPython解释器引入的锁,GIL在解释器层面阻止了真正的并行运行。解释器在执行任何线程之前,必须等待当前正在运行的线程释放GIL,事实上,解释器会强迫想要运行的线程必须拿到GIL才能访问解释器的任何资源,例如栈或Python对象等,这也正是GIL的目的,为了阻止不同的线程并发访问Python对象。这样GIL可以保护解释器的内存,让垃圾回收工作正常。但事实上,这却造成了程序员无法通过并行执行多线程来提高程序的性能。如果我们去掉GIL,就可以实现真正的并行。GIL并没有影响多处理器并行的线程,只是限制了一个解释器只能有一个线程在运行。
2、测试用例
测试一:空函数
from threading importThreaddeffunction_to_run():pass
classthreads_object(Thread):defrun(self):
function_to_run()classnothreads_object(object):defrun(self):
function_to_run()defnon_threaded(num_iter):
funcs=[]for i inrange(int(num_iter)):
funcs.append(nothreads_object())for i infuncs:
i.run()defthreaded(num_threads):
funcs=[]for i inrange(int(num_threads)):
funcs.append(threads_object())for i infuncs:
i.start()for i infuncs:
i.join()defshow_results(func_name, results):print("%-23s %4.6f seconds" %(func_name, results))if __name__ == "__main__":importsysfrom timeit importTimer
repeat= 100number= 1num_threads= [1, 2, 4, 8]print('starting tests')for i innum_threads:
t= Timer("non_threaded(%s)" % i, "from __main__ import non_threaded")
best_result= min(t.repeat(repeat=repeat, number=number))
show_results("non_threaded (%s iters)" %i, best_result)
t= Timer("threaded(%s)" % i, "from __main__ import threaded")
best_result= min(t.repeat(repeat=repeat, number=number))
show_results("threaded (%s threads)" %i, best_result)print('Iterations complete')
下面的代码是用来评估多线程应用性能的简单代码。每一次测试都循环调用函数100次,重复执行多次,取速度最快的一次。在for循环中,调用non_threaded和threaded函数。同时,我们会不断增加调用次数和线程数来重复执行这个测试。在非线程测试中,调用函数与定义线程数一样多的次数。只需改变function_to_run的内容即可进行测试。
上面代码测试的为空函数,执行结果如下:
通过结果发现,使用线程的开销比不使用线程的开销大得多。
测试二:数字处理
将function_to_run改成计算斐波那契数列
deffunction_to_run():#pass
a, b = 0, 1
for i in range(10000):
a, b= b, a + b
结果如下:
结果:提高线程的数量并没有带来收益,因为GIL和线程管理代码的开销,多线程运行永远不可能比函数顺序执行更快。GIL只允许解释器一次执行一个线程。
测试三:数据读取
更改function_to_run如下:
deffunction_to_run():#pass
#a, b = 0, 1
#for i in range(10000):
#a, b = b, a + b
fh= open("README.md","rb")
size= 1024
for i in range(1000):
fh.read(size)
运行结果:
测试四:URL请求
from threading importThreaddeffunction_to_run():#pass
#a, b = 0, 1
#for i in range(10000):
#a, b = b, a + b
#fh = open("README.md","rb")
#size = 1024
#for i in range(1000):
#fh.read(size)
importurllib.requestfor i in range(10):
with urllib.request.urlopen("https://www.baidu.com/") as f:
f.read(1024)classthreads_object(Thread):defrun(self):
function_to_run()classnothreads_object(object):defrun(self):
function_to_run()defnon_threaded(num_iter):
funcs=[]for i inrange(int(num_iter)):
funcs.append(nothreads_object())for i infuncs:
i.run()defthreaded(num_threads):
funcs=[]for i inrange(int(num_threads)):
funcs.append(threads_object())for i infuncs:
i.start()for i infuncs:
i.join()defshow_results(func_name, results):print("%-23s %4.6f seconds" %(func_name, results))if __name__ == "__main__":importsysfrom timeit importTimer
repeat= 100number= 1num_threads= [1, 2, 4, 8]print('starting tests')for i innum_threads:
t= Timer("non_threaded(%s)" % i, "from __main__ import non_threaded")
best_result= min(t.repeat(repeat=repeat, number=number))
show_results("non_threaded (%s iters)" %i, best_result)
t= Timer("threaded(%s)" % i, "from __main__ import threaded")
best_result= min(t.repeat(repeat=repeat, number=number))
show_results("threaded (%s threads)" %i, best_result)print('Iterations complete')
运行结果:
在有I/O操作时,多线程比单线程快得多。增加线程并不会提高应用启动的时间,但是可以支持并发。例如,一次性创建一个线程池,并重用worker会很有用,这可以让我们切分一个大的数据集,用同样的函数处理不同的部分。