1. 程式人生 > 其它 >ajax的書寫方式和 陣列、map、object、list的前後臺傳參方式

ajax的書寫方式和 陣列、map、object、list的前後臺傳參方式

技術標籤:python大資料程式語言python多執行緒linux

本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,如有問題請及時聯絡我們以作處理

做 Python 開發時,想必你肯定聽過 GIL,它經常被 Python 程式設計師吐槽,說 Python 的多執行緒非常雞肋,因為 GIL 的存在,Python 無法利用多執行緒提高效能。

但事實真的如此嗎?

這篇文章,我們就來看一下 Python 的 GIL 到底是什麼?以及它的存在,究竟對我們的程式有哪些影響。

GIL是什麼?

查閱官方文件,GIL 全稱 Global Interpreter Lock,即全域性直譯器鎖

,它的官方解釋如下:

In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

翻譯成中文就是:

在 CPython 直譯器中,全域性解釋鎖 GIL 是在於執行 Python 位元組碼時,為了保護訪問 Python 物件而阻止多個執行緒執行的一把互斥鎖。這把鎖的存在主要是因為 CPython 直譯器的記憶體管理不是執行緒安全的。然而直到今天 GIL 依舊存在,現在的很多功能已經習慣於依賴它作為執行的保證。

我們從這個定義中,可以看到幾個重點:

  1. GIL 是存在於 CPython 直譯器中的,屬於直譯器層級,而並非屬於 Python 的語言特性。也就是說,如果你自己有能力實現一個 Python 直譯器,完全可以不使用 GIL
  2. GIL 是為了讓直譯器在執行 Python 程式碼時,同一時刻只有一個執行緒在執行,以此保證記憶體管理是安全的
  3. 歷史原因,現在很多 Python 專案已經習慣依賴 GIL(開發者認為 Python 就是執行緒安全的,寫程式碼時對共享資源的訪問不會加鎖)

在這裡我想強調的是,因為 Python 預設的直譯器是 CPython,GIL 是存在於 CPython 直譯器中的,我們平時說到 GIL 就認為它是 Python 語言的問題,其實這個表述是不準確的。

其實除了 CPython 直譯器,常見的 Python 直譯器還有如下幾種:

  • CPython:C 語言開發的直譯器,官方預設使用,目前使用也最為廣泛,存在 GIL
  • IPython:基於 CPython 開發的互動式直譯器,只是增強了互動功能,執行過程與 CPython 完全一樣
  • PyPy:目標是加快執行速度,採用 JIT 技術,對 Python 程式碼進行動態編譯(不是解釋),可以顯著提高程式碼的執行速度,但執行結果可能與 CPython 不同,存在 GIL
  • Jython:執行在 Java 平臺的 Python 直譯器,可以把 Python 程式碼編譯成 Java 位元組碼,依賴 Java 平臺,不存在 GIL
  • IronPython:和 Jython 類似,執行在微軟的 .Net 平臺下的 Python 直譯器,可以把 Python 程式碼編譯成 .Net 位元組碼,不存在 GIL

雖然有這麼多 Python 直譯器,但使用最廣泛的依舊是官方提供的 CPython,它預設是有 GIL 的。

那麼 GIL 會帶來什麼問題呢?為什麼開發者總是抱怨 Python 多執行緒無法提高程式效率?

GIL帶來的問題

想要了解 GIL 對 Python 多執行緒帶來的影響,我們來看一個例子。

import threading

def loop():
    count = 0
    while count <= 1000000000:
        count += 1

# 2個執行緒執行loop方法
t1 = threading.Thread(target=loop)
t2 = threading.Thread(target=loop)

t1.start()
t2.start()
t1.join()
t2.join()
複製程式碼

在這個例子中,雖然我們開啟了 2 個執行緒去執行 loop,但我們觀察 CPU 的使用情況,發現這個程式只能跑滿一個 CPU 核心,沒有利用到多核。

這就是 GIL 帶來的問題。

其原因在於,一個 Python 執行緒想要執行一段程式碼,必須先拿到 GIL 鎖後才被允許執行,也就是說,即使我們使用了多執行緒,但同一時刻卻只有一個執行緒在執行。

但我們進一步思考一下,就算有 GIL 的存在,理論來說,如果 GIL 釋放的夠快,多執行緒怎麼也要比單執行緒執行效率高吧?

但現實的結果是:多執行緒比我們想象的更糟糕。

我們再來看一個例子,還是執行一個 CPU 密集型的任務程式,我們來看單執行緒執行 2 次和 2 個執行緒同時執行,哪個效率更高?

單執行緒執行 2 次 CPU 密集型任務:

import time
import threading

def loop():
    count = 0
    while count <= 1000000000:
        count += 1


# 單執行緒執行 2 次 CPU 密集型任務
start = time.time()
loop()
loop()
end = time.time()
print("execution time: %s" % (end - start))
# execution time: 89.63111019134521
複製程式碼

從結果來看,執行耗時 89秒。

再來看 2 個執行緒同時執行 CPU 密集型任務:

import time
import threading

def loop():
    count = 0
    while count <= 1000000000:
        count += 1


# 2個執行緒同時執行CPU密集型任務
start = time.time()

t1 = threading.Thread(target=loop)
t2 = threading.Thread(target=loop)
t1.start()
t2.start()
t1.join()
t2.join()

end = time.time()
print("execution time: %s" % (end - start))
# execution time: 92.29994678497314
複製程式碼

執行耗時卻達到了 92 秒。

從執行結果來看,多執行緒的效率還不如單執行緒的執行效率高!

為什麼會導致這種情況?我們來看一下 GIL 究竟是怎麼回事。

GIL原理

其實,由於 Python 的執行緒就是 C 語言的 pthread,它是通過作業系統排程演算法排程執行的。

Python 2.x 的程式碼執行是基於 opcode 數量的排程方式,簡單來說就是每執行一定數量的位元組碼,或遇到系統 IO 時,會強制釋放 GIL,然後觸發一次作業系統的執行緒排程。

雖然在 Python 3.x 進行了優化,基於固定時間的排程方式,就是每執行固定時間的位元組碼,或遇到系統 IO 時,強制釋放 GIL,觸發系統的執行緒排程。

但這種執行緒的排程方式,都會導致同一時刻只有一個執行緒在執行。

而執行緒在排程時,又依賴系統的 CPU 環境,也就是在單核 CPU 或多核 CPU 下,多執行緒在排程切換時的成本是不同的。

如果是在單核 CPU 環境下,多執行緒在執行時,執行緒 A 釋放了 GIL 鎖,那麼被喚醒的執行緒 B 能夠立即拿到 GIL 鎖,執行緒 B 可以無縫接力繼續執行,執行流程如下圖:

而如果在在多核 CPU 環境下,當多執行緒執行時,執行緒 A 在 CPU0 執行完之後釋放 GIL 鎖,其他 CPU 上的執行緒都會進行競爭。

但 CPU0 上的執行緒 B 可能又馬上獲取到了 GIL,這就導致其他 CPU 上被喚醒的執行緒,只能眼巴巴地看著 CPU0 上的執行緒愉快地執行著,而自己只能等待,直到又被切換到待排程的狀態,這就會產生多核 CPU 頻繁進行執行緒切換,消耗資源,這種情況也被叫做「CPU顛簸」。整個執行流程如下圖:

圖中綠色部分是執行緒獲得了 GIL 並進行有效的 CPU 運算,紅色部分是被喚醒的執行緒由於沒有爭奪到 GIL,只能無效等待,無法充分利用 CPU 的並行運算能力。

這就是多執行緒在多核 CPU 下,執行效率還不如單執行緒或單核 CPU 效率高的原因。

到此,我們可以得出一個結論:如果使用多執行緒執行一個 CPU 密集型任務,那麼 Python 多執行緒是無法提高執行效率的。

別急,你以為事情就這樣結束了嗎?

我們還需要考慮另一種場景:如果多執行緒執行的不是一個 CPU 密集型任務,而是一個 IO 密集型的任務,結果又會如何呢?

答案是,多執行緒可以顯著提高執行效率!

其實原因也很簡單,因為 IO 密集型的任務,大部分時間都花在等待 IO 上,並沒有一直佔用 CPU 的資源,所以並不會像上面的程式那樣,進行無效的執行緒切換。

例如,如果我們想要下載 2 個網頁的資料,也就是發起 2 個網路請求,如果使用單執行緒的方式執行,只能是依次序列執行,其中等待的總耗時是 2 個網路請求的時間之和。

而如果採用 2 個執行緒的方式同時處理,這 2 個網路請求會同時傳送,然後同時等待資料返回(IO等待),最終等待的時間取決於耗時最久的執行緒時間,這會比序列執行效率要高得多。

所以,如果需要執行 IO 密集型任務,Python 多執行緒是可以提高執行效率的。

為什麼會有GIL?

我們已經瞭解到,GIL 對於處理 CPU 密集型任務的場景,多執行緒是無法提高執行效率的。

既然 GIL 的影響這麼大,那為什麼 Python 直譯器 CPython 在設計時要採用這種方式呢?

這就需要追溯歷史原因了。

在 2000 年以前,各個 CPU 廠商為了提高計算機的效能,其努力方向都在提升單個 CPU 的執行頻率上,但在之後的幾年遇到了天花板,單個 CPU 效能已經無法再得到大幅度提升,所以在 2000 年以後,提升計算機效能的方向便改為向多 CPU 核心方向發展。

為了更有效的利用多核心 CPU,很多程式語言就出現了多執行緒的程式設計方式,但也正是有了多執行緒的存在,隨之帶來的問題就是多執行緒之間對於維護資料和狀態一致性的困難。

Python 設計者在設計直譯器時,可能沒有想到 CPU 的效能提升會這麼快轉為多核心方向發展,所以在當時的場景下,設計一個全域性鎖是那個時代保護多執行緒資源一致性最簡單經濟的設計方案。

而隨著多核心時代來臨,當大家試圖去拆分和去除 GIL 的時候,發現大量庫的程式碼和開發者已經重度依賴 GIL(預設認為 Pythonn 內部物件是執行緒安全的,無需在開發時額外加鎖),所以這個去除 GIL 的任務變得複雜且難以實現。

所以,GIL 的存在更多的是歷史原因,在 Python 3 的版本,雖然對 GIL 做了優化,但依舊沒有去除掉,Python 設計者的解釋是,在去除 GIL 時,會破壞現有的 C 擴充套件模組,因為這些擴充套件模組都嚴重依賴於 GIL,去除 GIL 有可能會導致執行速度會比 Python 2 更慢。

Python 走到現在,已經有太多的歷史包袱,所以現在只能揹負著它們前行,如果一切推倒重來,想必 Python 設計者會設計得更加優雅一些。

解決方案

既然 GIL 的存在會導致這麼多問題,那我們在開發時,需要注意哪些地方,避免受到 GIL 的影響呢?

我總結了以下幾個方案:

  1. IO 密集型任務場景,可以使用多執行緒可以提高執行效率
  2. CPU 密集型任務場景,不使用多執行緒,推薦使用多程序方式部署執行
  3. 更換沒有 GIL 的 Python 直譯器,但需要提前評估執行結果是否與 CPython 一致
  4. 編寫 Python 的 C 擴充套件模組,把 CPU 密集型任務交給 C 模組處理,但缺點是編碼較為複雜
  5. 更換其他語言 :)

總結

這篇文章我們主要講了 Python GIL 相關的問題。

首先,我們瞭解到 GIL 屬於 Python 直譯器層面的,它並不是 Python 語言的特性,這一點我們一定不要搞混了。GIL 的存在會讓 Python 在執行程式碼時,只允許同一時刻只有一個執行緒在執行,其目的是為了保證在執行過程中記憶體管理的安全性。

之後我們通過一個例子,觀察到 Python 在多執行緒執行 CPU 密集型任務時,執行效率比單執行緒還要低,其原因是因為在多核 CPU 環境下,GIL 的存在會導致多執行緒切換時無效的資源消耗,因此會降低程式執行的效率。

但如果使用多執行緒執行 IO 密集型的任務,由於執行緒更多地是在等待 IO,所以並不會消耗 CPU 資源,這種情況下,使用多執行緒是可以提高程式執行效率的。

最後,我們分析了 GIL 存在的原因,更多是因為歷史問題導致,也正因為 GIL 的存在,很多 Python 開發者預設 Python 是執行緒安全的,這也間接增加了去除 GIL 的困難性。

基於這些前提,我們平時在部署 Python 程式時,一般更傾向於使用多程序的方式去部署,就是為了避免 GIL 的影響。

任何一種程式語言,都有其優勢和劣勢,我們需要理解它的實現機制,發揮其長處,才能更好地服務於我們的需求。

想要獲取更多Python學習資料可以加
QQ:2955637827私聊
或加Q群630390733
大家一起來學習討論吧!