六十二、多線程真的比單線程快?那是因爲你不知道Python中的全局解釋器鎖GIL

@Author:Runsen

@Date:2020/6/4

作者介紹:Runsen目前大三下學期,專業化學工程與工藝,大學沉迷日語,Python, Java和一系列數據分析軟件。導致翹課嚴重,專業排名中下。.在大學60%的時間,都在CSDN。決定今天比昨天要更加努力。

前面文章,點擊下面鏈接

我的Python教程,不斷整理,反覆學習

今日,我決定繼續更新Python教程,介紹的是Python 中的全局解釋器鎖GIL。已經到了六十二,還剩下區區三十八篇。長得帥就是我的動力,不對,明明就是太窮了纔是我的動力。

多線程比單線程快?

在Python中,可以通過多進程、多線程和多協程來實現多任務。這個不清楚,看看我之前的文章,難道多線程比單線程快?

你竟然敢質疑我,我太開心了。我得用一個例子證明我自己得觀點。

'''
@Author: Runsen
@微信公衆號: 潤森筆記
@博客: https://blog.csdn.net/weixin_44510615
@Date: 2020/6/4
'''

import threading, time
def my_counter():
    i = 0
    for _ in range(100000000):
        i = i+1
    return True

def main1():
    thread_ary = {}
    start_time = time.time()
    for tid in range(2):
        t = threading.Thread(target=my_counter)
        t.start()
        t.join()  # 第一次循環的時候join方法引起主線程阻塞,但第二個線程並沒有啓動,所以兩個線程是順序執行的

    print("單線程順序執行total_time: {}".format(time.time() - start_time))

def main2():
    thread_ary = {}
    start_time = time.time()
    for tid in range(2):
        t = threading.Thread(target=my_counter)
        t.start()
        thread_ary[tid] = t

    for i in range(2):
        thread_ary[i].join()  # 兩個線程均已啓動,所以兩個線程是併發的

    print("多線程執行total_time: {}".format(time.time() - start_time))

if __name__ == "__main__":
    main1()
    main2()

運行結果

單線程順序執行total_time: 17.754502773284912
多線程執行total_time: 20.01178550720215

我怕你說我亂得出來得結果,我還是截個圖看清楚點

沒錯, Python 的線程失效了,沒有起到並行計算的作用。

Python 的線程,的確封裝了底層的操作系統線程,在 Linux 系統裏是 Pthread(全稱爲 POSIX Thread),而在 Windows 系統裏是 Windows Thread。另外,Python 的線程,也完全受操作系統管理,比如協調何時執行、管理內存資源、管理中斷等等。所以,雖然 Python 的線程和 C++ 的線程本質上是不同的

GIL並不是Python的特性

GIL 的概念用簡單的一句話來解釋,就是**「任一時刻,無論線程多少,單一 CPython 解釋器只能執行一條字節碼」**。這個定義需要注意的點:

首先需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。

C++是一套語言(語法)標準,但是可以用不同的編譯器來編譯成可執行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。

Python也一樣,同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行。

其他 Python 解釋器不一定有 GIL。例如 Jython (JVM) 和 IronPython (CLR) 沒有 GIL,而 CPython,PyPy 有 GIL;

因爲CPython是大部分環境下默認的Python執行環境。所以在很多人的概念裏CPython就是Python,也就想當然的把GIL歸結爲Python語言的缺陷。所以這裏要先明確一點:GIL並不是Python的特性,Python完全可以不依賴於GIL

GIL本質就是一把互斥鎖

GIL本質就是一把互斥鎖,既然是互斥鎖,所有互斥鎖的本質都一樣,都是將併發運行變成串行,以此來控制同一時間內共享數據只能被一個任務所修改,進而保證數據安全。

可以肯定的一點是:保護不同的數據的安全,就應該加不同的鎖。

GIL 是工作原理:下面這張圖,就是一個 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 輪流執行,每一個線程在開始執行時,都會鎖住 GIL,以阻止別的線程執行;同樣的,每一個線程執行完一段後,會釋放 GIL,以允許別的線程開始利用資源。

細心的你可能會發現一個問題:爲什麼 Python 線程會去主動釋放 GIL 呢?畢竟,如果僅僅是要求 Python

CPython 使用引用計數來管理內存,所有 Python 腳本中創建的實例,都會有一個引用計數,來記錄有多少個指針指向它。當引用計數只有 0 時,則會自動釋放內存。

import sys
a = []
b = a
print(sys.getrefcount(a))
>>> 3

這個例子中,a 的引用計數是 3,因爲有 a、b 和作爲參數傳遞的 getrefcount 這三個地方,都引用了一個空列表。這樣一來,如果有兩個 Python 線程同時引用了 a,就會造成引用計數的 race condition,引用計數可能最終只增加 1,這樣就會造成內存被污染。因爲第一個線程結束時,會把引用計數減少 1,這時可能達到條件釋放內存,當第二個線程再試圖訪問 a 時,就找不到有效的內存了。

計算密集型

我們先來看一個簡單的計算密集型示例:

import time
COUNT = 50_000_000

def count_down():
   global COUNT
   while COUNT > 0:
       COUNT -= 1

s = time.perf_counter()
count_down()
c = time.perf_counter() - s
print('time taken in seconds - >:', c)

time taken in seconds - >: 9.2957003

這個是單線程, 時間是9s, 下面我們用兩個線程看看結果又如何:

import time
from threading import Thread

COUNT = 50_000_000

def count_down():
   global COUNT
   while COUNT > 0:
       COUNT -= 1

s = time.perf_counter()
t1 = Thread(target=count_down)
t2 = Thread(target=count_down)
t1.start()
t2.start()
t1.join()
t2.join()
c = time.perf_counter() - s
print('time taken in seconds - >:', c)

time taken in seconds - >: 17.110625

其實結果一點也不奇怪, 我們程序主要的操作就是在計算, cpu沒有等待, 而改爲多線程後, 增加了線程後, 在線程之間頻繁的切換,增大了時間開銷, 時間當然會增加了。

對於io密集型工作(爬蟲),多線程可以大幅提高代碼效率。對CPU計算密集型(數據分析),多線程的效率可能比單線程還略低。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章