1. 程式人生 > >Python並行編程(十):多線程性能評估

Python並行編程(十):多線程性能評估

概念 ati 之前 %s 必須 def readme run www

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並行編程(十):多線程性能評估