Python並行編程(十):多線程性能評估
1、基本概念
GIL是CPython解釋器引入的鎖,GIL在解釋器層面阻止了真正的並行運行。解釋器在執行任何線程之前,必須等待當前正在運行的線程釋放GIL,事實上,解釋器會強迫想要運行的線程必須拿到GIL才能訪問解釋器的任何資源,例如棧或Python對象等,這也正是GIL的目的,為了阻止不同的線程並發訪問Python對象。這樣GIL可以保護解釋器的內存,讓垃圾回收工作正常。但事實上,這卻造成了程序員無法通過並行執行多線程來提高程序的性能。如果我們去掉GIL,就可以實現真正的並行。GIL並沒有影響多處理器並行的線程,只是限制了一個解釋器只能有一個線程在運行。
2、測試用例
測試一:空函數
from threading import Thread def function_to_run(): pass class threads_object(Thread): def run(self): function_to_run() class nothreads_object(object): def run(self): function_to_run() def non_threaded(num_iter): funcs = [] for i in range(int(num_iter)): funcs.append(nothreads_object())for i in funcs: i.run() def threaded(num_threads): funcs = [] for i in range(int(num_threads)): funcs.append(threads_object()) for i in funcs: i.start() for i in funcs: i.join() def show_results(func_name, results): print("%-23s %4.6f seconds" % (func_name, results))if __name__ == "__main__": import sys from timeit import Timer repeat = 100 number = 1 num_threads = [1, 2, 4, 8] print(‘starting tests‘) for i in num_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改成計算斐波那契數列
def function_to_run(): # pass a, b = 0, 1 for i in range(10000): a, b = b, a + b
結果如下:
結果:提高線程的數量並沒有帶來收益,因為GIL和線程管理代碼的開銷,多線程運行永遠不可能比函數順序執行更快。GIL只允許解釋器一次執行一個線程。
測試三:數據讀取
更改function_to_run如下:
def function_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 import Thread def function_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) import urllib.request for i in range(10): with urllib.request.urlopen("https://www.baidu.com/") as f: f.read(1024) class threads_object(Thread): def run(self): function_to_run() class nothreads_object(object): def run(self): function_to_run() def non_threaded(num_iter): funcs = [] for i in range(int(num_iter)): funcs.append(nothreads_object()) for i in funcs: i.run() def threaded(num_threads): funcs = [] for i in range(int(num_threads)): funcs.append(threads_object()) for i in funcs: i.start() for i in funcs: i.join() def show_results(func_name, results): print("%-23s %4.6f seconds" % (func_name, results)) if __name__ == "__main__": import sys from timeit import Timer repeat = 100 number = 1 num_threads = [1, 2, 4, 8] print(‘starting tests‘) for i in num_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會很有用,這可以讓我們切分一個大的數據集,用同樣的函數處理不同的部分。
Python並行編程(十):多線程性能評估