<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么 GIL 讓多線程變得如此雞肋?

          共 6328字,需瀏覽 13分鐘

           ·

          2021-03-12 10:03

          做 Python 開發(fā)時,想必你肯定聽過 GIL,它經(jīng)常被 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 是在于執(zhí)行 Python 字節(jié)碼時,為了保護訪問 Python 對象而阻止多個線程執(zhí)行的一把互斥鎖。這把鎖的存在主要是因為 CPython 解釋器的內(nèi)存管理不是線程安全的。然而直到今天 GIL 依舊存在,現(xiàn)在的很多功能已經(jīng)習慣于依賴它作為執(zhí)行的保證。

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

          1. GIL 是存在于 CPython 解釋器中的,屬于解釋器層級,而并非屬于 Python 的語言特性。也就是說,如果你自己有能力實現(xiàn)一個 Python 解釋器,完全可以不使用 GIL
          2. GIL 是為了讓解釋器在執(zhí)行 Python 代碼時,同一時刻只有一個線程在運行,以此保證內(nèi)存管理是安全的
          3. 歷史原因,現(xiàn)在很多 Python 項目已經(jīng)習慣依賴 GIL(開發(fā)者認為 Python 就是線程安全的,寫代碼時對共享資源的訪問不會加鎖)

          在這里我想強調(diào)的是,因為 Python 默認的解釋器是 CPython,GIL 是存在于 CPython 解釋器中的,我們平時說到 GIL 就認為它是 Python 語言的問題,其實這個表述是不準確的。

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

          • CPython:C 語言開發(fā)的解釋器,官方默認使用,目前使用也最為廣泛,存在 GIL
          • IPython:基于 CPython 開發(fā)的交互式解釋器,只是增強了交互功能,執(zhí)行過程與 CPython 完全一樣
          • PyPy:目標是加快執(zhí)行速度,采用 JIT 技術(shù),對 Python 代碼進行動態(tài)編譯(不是解釋),可以顯著提高代碼的執(zhí)行速度,但執(zhí)行結(jié)果可能與 CPython 不同,存在 GIL
          • Jython:運行在 Java 平臺的 Python 解釋器,可以把 Python 代碼編譯成 Java 字節(jié)碼,依賴 Java 平臺,不存在 GIL
          • IronPython:和 Jython 類似,運行在微軟的 .Net 平臺下的 Python 解釋器,可以把 Python 代碼編譯成 .Net 字節(jié)碼,不存在 GIL

          雖然有這么多 Python 解釋器,但使用最廣泛的依舊是官方提供的 CPython,它默認是有 GIL 的。

          那么 GIL 會帶來什么問題呢?為什么開發(fā)者總是抱怨 Python 多線程無法提高程序效率?

          GIL帶來的問題

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

          import threading

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

          # 2個線程執(zhí)行l(wèi)oop方法
          t1 = threading.Thread(target=loop)
          t2 = threading.Thread(target=loop)

          t1.start()
          t2.start()
          t1.join()
          t2.join()

          在這個例子中,雖然我們開啟了 2 個線程去執(zhí)行 loop,但我們觀察 CPU 的使用情況,發(fā)現(xiàn)這個程序只能跑滿一個 CPU 核心,沒有利用到多核。

          這就是 GIL 帶來的問題。

          其原因在于,一個 Python 線程想要執(zhí)行一段代碼,必須先拿到 GIL 鎖后才被允許執(zhí)行,也就是說,即使我們使用了多線程,但同一時刻卻只有一個線程在執(zhí)行。

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

          但現(xiàn)實的結(jié)果是:多線程比我們想象的更糟糕。

          我們再來看一個例子,還是運行一個 CPU 密集型的任務(wù)程序,我們來看單線程執(zhí)行 2 次和 2 個線程同時執(zhí)行,哪個效率更高?

          單線程執(zhí)行 2 次 CPU 密集型任務(wù):

          import time
          import threading

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


          # 單線程執(zhí)行 2 次 CPU 密集型任務(wù)
          start = time.time()
          loop()
          loop()
          end = time.time()
          print("execution time: %s" % (end - start))
          # execution time: 89.63111019134521

          從結(jié)果來看,執(zhí)行耗時 89秒。

          再來看 2 個線程同時執(zhí)行 CPU 密集型任務(wù):

          import time
          import threading

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


          # 2個線程同時執(zhí)行CPU密集型任務(wù)
          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

          執(zhí)行耗時卻達到了 92 秒。

          從執(zhí)行結(jié)果來看,多線程的效率還不如單線程的執(zhí)行效率高!

          為什么會導(dǎo)致這種情況?我們來看一下 GIL 究竟是怎么回事。

          GIL原理

          其實,由于 Python 的線程就是 C 語言的 pthread,它是通過操作系統(tǒng)調(diào)度算法調(diào)度執(zhí)行的。

          Python 2.x 的代碼執(zhí)行是基于 opcode 數(shù)量的調(diào)度方式,簡單來說就是每執(zhí)行一定數(shù)量的字節(jié)碼,或遇到系統(tǒng) IO 時,會強制釋放 GIL,然后觸發(fā)一次操作系統(tǒng)的線程調(diào)度。

          雖然在 Python 3.x 進行了優(yōu)化,基于固定時間的調(diào)度方式,就是每執(zhí)行固定時間的字節(jié)碼,或遇到系統(tǒng) IO 時,強制釋放 GIL,觸發(fā)系統(tǒng)的線程調(diào)度。

          但這種線程的調(diào)度方式,都會導(dǎo)致同一時刻只有一個線程在運行。

          而線程在調(diào)度時,又依賴系統(tǒng)的 CPU 環(huán)境,也就是在單核 CPU 或多核 CPU 下,多線程在調(diào)度切換時的成本是不同的。

          如果是在單核 CPU 環(huán)境下,多線程在執(zhí)行時,線程 A 釋放了 GIL 鎖,那么被喚醒的線程 B 能夠立即拿到 GIL 鎖,線程 B 可以無縫接力繼續(xù)執(zhí)行,執(zhí)行流程如下圖:



          而如果在在多核 CPU 環(huán)境下,當多線程執(zhí)行時,線程 A 在 CPU0 執(zhí)行完之后釋放 GIL 鎖,其他 CPU 上的線程都會進行競爭。

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



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

          這就是多線程在多核 CPU 下,執(zhí)行效率還不如單線程或單核 CPU 效率高的原因。

          到此,我們可以得出一個結(jié)論:如果使用多線程運行一個 CPU 密集型任務(wù),那么 Python 多線程是無法提高運行效率的。

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

          我們還需要考慮另一種場景:如果多線程運行的不是一個 CPU 密集型任務(wù),而是一個 IO 密集型的任務(wù),結(jié)果又會如何呢?

          答案是,多線程可以顯著提高運行效率!

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

          例如,如果我們想要下載 2 個網(wǎng)頁的數(shù)據(jù),也就是發(fā)起 2 個網(wǎng)絡(luò)請求,如果使用單線程的方式運行,只能是依次串行執(zhí)行,其中等待的總耗時是 2 個網(wǎng)絡(luò)請求的時間之和。

          而如果采用 2 個線程的方式同時處理,這 2 個網(wǎng)絡(luò)請求會同時發(fā)送,然后同時等待數(shù)據(jù)返回(IO等待),最終等待的時間取決于耗時最久的線程時間,這會比串行執(zhí)行效率要高得多。

          所以,如果需要運行 IO 密集型任務(wù),Python 多線程是可以提高運行效率的。

          為什么會有GIL

          我們已經(jīng)了解到,GIL 對于處理 CPU 密集型任務(wù)的場景,多線程是無法提高運行效率的。

          既然 GIL 的影響這么大,那為什么 Python 解釋器 CPython 在設(shè)計時要采用這種方式呢?

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

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

          為了更有效的利用多核心 CPU,很多編程語言就出現(xiàn)了多線程的編程方式,但也正是有了多線程的存在,隨之帶來的問題就是多線程之間對于維護數(shù)據(jù)和狀態(tài)一致性的困難。

          Python 設(shè)計者在設(shè)計解釋器時,可能沒有想到 CPU 的性能提升會這么快轉(zhuǎn)為多核心方向發(fā)展,所以在當時的場景下,設(shè)計一個全局鎖是那個時代保護多線程資源一致性最簡單經(jīng)濟的設(shè)計方案。

          而隨著多核心時代來臨,當大家試圖去拆分和去除 GIL 的時候,發(fā)現(xiàn)大量庫的代碼和開發(fā)者已經(jīng)重度依賴 GIL(默認認為 Pythonn 內(nèi)部對象是線程安全的,無需在開發(fā)時額外加鎖),所以這個去除 GIL 的任務(wù)變得復(fù)雜且難以實現(xiàn)。

          所以,GIL 的存在更多的是歷史原因,在 Python 3 的版本,雖然對 GIL 做了優(yōu)化,但依舊沒有去除掉,Python 設(shè)計者的解釋是,在去除 GIL 時,會破壞現(xiàn)有的 C 擴展模塊,因為這些擴展模塊都嚴重依賴于 GIL,去除 GIL 有可能會導(dǎo)致運行速度會比 Python 2 更慢。

          Python 走到現(xiàn)在,已經(jīng)有太多的歷史包袱,所以現(xiàn)在只能背負著它們前行,如果一切推倒重來,想必 Python 設(shè)計者會設(shè)計得更加優(yōu)雅一些。

          解決方案

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

          我總結(jié)了以下幾個方案:

          1. IO 密集型任務(wù)場景,可以使用多線程可以提高運行效率
          2. CPU 密集型任務(wù)場景,不使用多線程,推薦使用多進程方式部署運行
          3. 更換沒有 GIL 的 Python 解釋器,但需要提前評估運行結(jié)果是否與 CPython 一致
          4. 編寫 Python 的 C 擴展模塊,把 CPU 密集型任務(wù)交給 C 模塊處理,但缺點是編碼較為復(fù)雜
          5. 更換其他語言 :)

          總結(jié)

          這篇文章我們主要講了 Python GIL 相關(guān)的問題。

          首先,我們了解到 GIL 屬于 Python 解釋器層面的,它并不是 Python 語言的特性,這一點我們一定不要搞混了。GIL 的存在會讓 Python 在執(zhí)行代碼時,只允許同一時刻只有一個線程在執(zhí)行,其目的是為了保證在執(zhí)行過程中內(nèi)存管理的安全性。

          之后我們通過一個例子,觀察到 Python 在多線程運行 CPU 密集型任務(wù)時,執(zhí)行效率比單線程還要低,其原因是因為在多核 CPU 環(huán)境下,GIL 的存在會導(dǎo)致多線程切換時無效的資源消耗,因此會降低程序運行的效率。

          但如果使用多線程運行 IO 密集型的任務(wù),由于線程更多地是在等待 IO,所以并不會消耗 CPU 資源,這種情況下,使用多線程是可以提高程序運行效率的。

          最后,我們分析了 GIL 存在的原因,更多是因為歷史問題導(dǎo)致,也正因為 GIL 的存在,很多 Python 開發(fā)者默認 Python 是線程安全的,這也間接增加了去除 GIL 的困難性。

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

          任何一種編程語言,都有其優(yōu)勢和劣勢,我們需要理解它的實現(xiàn)機制,發(fā)揮其長處,才能更好地服務(wù)于我們的需求。

          更多閱讀



          2020 年最佳流行 Python 庫 Top 10


          2020 Python中文社區(qū)熱門文章 Top 10


          5分鐘快速掌握 Python 定時任務(wù)框架

          特別推薦




          點擊下方閱讀原文加入社區(qū)會員

          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产91看片婬黄大片 | 国产精品正在播放 | 极品国产3区 | 操比免费视频 | 黄视频大几吧操逼 |