Python 的 GIL(Global Interpreter Lock)機制限制了多執行緒在 CPU 密集型任務中的效能提升,因為它一次只允許一個執行緒持有直譯器鎖。這使得多執行緒在 CPU 密集型任務中無法真正平行執行,反而可能因為執行緒切換的額外開銷而降低效率。本文將深入探討 GIL 的影響,並以實際案例說明如何使用多程式來克服 GIL 的限制,提升 CPU 密集型任務的執行效率。同時,文章也將探討設計模式,特別是工廠模式,在 Python 中的應用,並以解析 XML 和 JSON 資料為例,展示工廠方法的實用性。

GIL 的問題

GIL 導致 Python 的並發程式無法真正實作多執行緒。GIL 有效地防止 CPU 繫結任務在多個執行緒中平行執行。為了理解 GIL 的影響,讓我們考慮一個 Python 的例子。

import time
import threading

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

start = time.time()
countdown(COUNT)
print('Sequential program finished.')
print(f'Took {time.time() - start : .2f} seconds.')

thread1 = threading.Thread(target=countdown, args=(COUNT // 2,))
thread2 = threading.Thread(target=countdown, args=(COUNT // 2,))

start = time.time()
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print('Multithreaded program finished.')
print(f'Took {time.time() - start : .2f} seconds.')

內容解密:

這個例子中,我們定義了一個 countdown 函式,該函式會將輸入的數字 n 減少到 0。然後,我們測量順序執行 countdown 函式的時間,並建立兩個執行緒,分別執行 countdown 函式,然後測量多執行緒執行的時間。

圖表翻譯:

  flowchart TD
    A[順序執行] --> B[建立執行緒]
    B --> C[執行緒 1]
    B --> D[執行緒 2]
    C --> E[執行緒 1 完成]
    D --> E[執行緒 2 完成]
    E --> F[多執行緒完成]

這個圖表展示了順序執行和多執行緒執行的流程。由於 GIL 的存在,多執行緒執行的時間可能不會比順序執行的時間短。

瞭解 GIL 及其對 Python 多執行緒的影響

在 Python 中,Global Interpreter Lock(GIL)是一個機制,防止多個執行緒同時執行 CPU 繫繫任務。這意味著,即使您使用多執行緒,CPU 繫繫任務仍將按順序執行,而不是平行執行。

GIL 的影響

GIL 的存在對 Python 多執行緒程式的效能有著顯著的影響。尤其是在 CPU 繫繫任務中,多執行緒並不能帶來理想中的速度提升。事實上,由於 GIL 的存在,多執行緒程式可能會因為執行緒切換的額外開銷而變得更慢。

實驗結果

以下是一個簡單的實驗,展示了 GIL 對 Python 多執行緒的影響:

import time
import threading

def countdown(n):
    while n > 0:
        n -= 1

# 單執行緒版本
start = time.time()
countdown(50000000)
print('單執行緒版本完成。')
print(f'耗時 {time.time() - start:.2f} 秒。')

# 多執行緒版本
start = time.time()
thread1 = threading.Thread(target=countdown, args=(25000000,))
thread2 = threading.Thread(target=countdown, args=(25000000,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print('多執行緒版本完成。')
print(f'耗時 {time.time() - start:.2f} 秒。')

結果顯示,多執行緒版本的執行時間並沒有明顯減少,甚至可能會增加。這是因為 GIL 的存在,導致多個執行緒不能同時執行 CPU 繫繫任務。

解決方案

雖然 GIL 的存在對 Python 多執行緒程式的效能有影響,但仍有多種方法可以繞過這個限制。其中最常見的方法是使用多程式(multiprocessing)代替多執行緒。由於每個程式都有自己的記憶體空間,GIL 不會對多程式程式造成影響。

import time
import multiprocessing

def countdown(n):
    while n > 0:
        n -= 1

# 單程式版本
start = time.time()
countdown(50000000)
print('單程式版本完成。')
print(f'耗時 {time.time() - start:.2f} 秒。')

# 多程式版本
start = time.time()
process1 = multiprocessing.Process(target=countdown, args=(25000000,))
process2 = multiprocessing.Process(target=countdown, args=(25000000,))
process1.start()
process2.start()
process1.join()
process2.join()
print('多程式版本完成。')
print(f'耗時 {time.time() - start:.2f} 秒。')

結果顯示,多程式版本的執行時間明顯減少,表明多程式可以有效地繞過 GIL 的限制。

多程式實作計數器

在上一章中,我們探討了使用多執行緒來實作計數器的功能。然而,多執行緒在Python中存在一些限制,尤其是由於全域解譯器鎖(Global Interpreter Lock,GIL)的存在,多執行緒在CPU密集型任務中可能無法充分發揮多核心的優勢。

多程式計數器實作

為了克服多執行緒的限制,我們可以使用多程式來實作計數器。Python的multiprocessing模組提供了建立多程式的能力。以下是使用多程式來實作計數器的示例程式碼:

import multiprocessing
import time

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    # 單程式計數
    start = time.time()
    countdown(COUNT)
    end = time.time()
    print(f"單程式計數時間:{end - start} 秒")

    # 多程式計數
    start = time.time()
    process1 = multiprocessing.Process(target=countdown, args=(COUNT // 2,))
    process2 = multiprocessing.Process(target=countdown, args=(COUNT // 2,))
    process1.start()
    process2.start()
    process1.join()
    process2.join()
    end = time.time()
    print(f"多程式計數時間:{end - start} 秒")

程式碼解釋

在上述程式碼中,我們定義了一個countdown函式,該函式負責計數。然後,在if __name__ == '__main__':塊中,我們建立了兩個程式,分別負責計數到COUNT // 2。我們使用start()方法啟動程式,然後使用join()方法等待程式完成。

結果比較

透過比較單程式和多程式計數的時間,可以看到多程式計數的時間明顯短於單程式計數的時間。這是因為多程式可以充分發揮多核心的優勢,從而提高計數的效率。

內容解密:

在這個例子中,我們使用了multiprocessing模組來建立多程式。每個程式負責計數到一半的數量,從而達到多程式計數的目的。透過比較單程式和多程式計數的時間,可以看到多程式計數的優勢。

圖表翻譯:

  flowchart TD
    A[開始] --> B[建立程式1]
    B --> C[建立程式2]
    C --> D[啟動程式1]
    D --> E[啟動程式2]
    E --> F[等待程式1完成]
    F --> G[等待程式2完成]
    G --> H[結束]

這個流程圖展示了多程式計數的過程,從建立程式到啟動程式,然後等待程式完成,最終結束。

多執行緒與多處理的效能比較

在上述程式中,我們可以看到三種不同的執行方式:順序執行、多執行緒和多處理。結果顯示,多處理的版本能夠明顯地提升執行效率,遠超於順序執行和多執行緒的版本。

順序執行

順序執行是最基本的執行方式,所有任務都按照順序一一執行。這種方式的優點是簡單易懂,但缺點是執行效率低下,尤其是在多核心處理器上。

多執行緒

多執行緒是允許多個執行緒同時執行的方式。然而,在 Python 中,由於全域性鎖機制(GIL)的限制,多執行緒的效率並不理想。GIL 會阻止多個執行緒同時執行,從而限制了多執行緒的潛在效率。

多處理

多處理是允許多個程式同時執行的方式。與多執行緒不同,多處理不受 GIL 的限制,因此可以充分利用多核心處理器的資源。結果顯示,多處理的版本能夠明顯地提升執行效率。

如何繞過 GIL 限制

有一些 Python 擴充套件是使用 C/C++ 編寫的,因此可以繞過 GIL 限制。例如,NumPy 就是一個這樣的套件。這些套件可以手動釋放 GIL,從而允許執行緒繞過鎖定。然而,這需要小心地實作和伴隨著適當的同步機制。

程式碼範例
import time
import threading
from multiprocessing import Pool

def countdown(n):
    while n > 0:
        n -= 1

def main():
    COUNT = 10000000

    start = time.time()
    countdown(COUNT)
    print("Sequential program finished.")
    print("Took {:.2f} seconds.".format(time.time() - start))

    start = time.time()
    thread1 = threading.Thread(target=countdown, args=(COUNT//2,))
    thread2 = threading.Thread(target=countdown, args=(COUNT//2,))

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()
    print("Multithreading program finished.")
    print("Took {:.2f} seconds.".format(time.time() - start))

    start = time.time()
    pool = Pool(processes=2)
    pool.apply_async(countdown, args=(COUNT//2,))
    pool.apply_async(countdown, args=(COUNT//2,))

    pool.close()
    pool.join()
    print("Multiprocessing program finished.")
    print("Took {:.2f} seconds.".format(time.time() - start))

if __name__ == "__main__":
    main()

Mermaid 圖表

  graph LR
    A[順序執行] --> B[多執行緒]
    B --> C[多處理]
    C --> D[提升執行效率]
    style A fill:#f9f,stroke:#333,stroke-width:4px
    style B fill:#f9f,stroke:#333,stroke-width:4px
    style C fill:#f9f,stroke:#333,stroke-width:4px
    style D fill:#f9f,stroke:#333,stroke-width:4px

圖表翻譯

此圖表展示了三種不同的執行方式:順序執行、多執行緒和多處理。多處理是提升執行效率的一種有效方式,尤其是在多核心處理器上。

Python 中的全域性解譯器鎖(GIL)

Python 中的全域性解譯器鎖(GIL)是一個機制,負責管理多個執行緒之間的資源存取,以避免多個執行緒同時修改相同的資料結構。然而,GIL 也會對多執行緒程式的效能產生影響,特別是在 CPU 繫繫的任務中。

GIL 的限制

GIL 只存在於 CPython 中,CPython 是 Python 的最常見的解譯器。然而,也有其他解譯器,如 Jython(用 Java 寫)和 IronPython(用 C++ 寫),可以用來避免 GIL 的限制。需要注意的是,這些解譯器不如 CPython 那樣被廣泛使用,一些套件和函式函式庫可能不相容於其中一種或兩種解譯器。

GIL 的問題

GIL 對多執行緒程式的效能產生影響,特別是在 CPU 繫繫的任務中。多次嘗試移除 GIL 都未能成功地維持非 CPU 繫繫任務的處理效能。

解決方法

雖然 GIL 會對多執行緒程式的效能產生影響,但我們可以使用一些方法來避免或減少 GIL 的限制。例如,可以使用多個程式代替多個執行緒,或者使用其他解譯器如 Jython 或 IronPython。

常見問題

  1. Python 和 C++ 之間的記憶體管理有何不同?
  2. GIL 解決了 Python 中的哪個問題?
  3. GIL 創造了哪個問題?
  4. 有哪些方法可以在 Python 程式中繞過 GIL 的限制?

進一步閱讀

  • 什麼是 Python 全域性解譯器鎖(GIL)?(gil/
  • Python GIL 視覺化(visualized
  • Python 中的複製操作(introduction
  • 從 Python 中移除 GIL 不是那麼容易(jsp?thread=214235
  • 《Python 平行程式設計》作者:玄貓,Packt Publishing Ltd (2014)
  • 《Python 並發程式設計》作者:Elliot Forbes (2017)
# 使用多個程式代替多個執行緒
import multiprocessing

def cpu_bound_task(n):
    # CPU 繫繫的任務
    result = 0
    for i in range(n):
        result += i
    return result

if __name__ == '__main__':
    # 建立多個程式
    processes = []
    for i in range(5):
        p = multiprocessing.Process(target=cpu_bound_task, args=(1000000,))
        processes.append(p)
        p.start()

    # 等待所有程式完成
    for p in processes:
        p.join()

內容解密:

上述程式碼使用多個程式代替多個執行緒來執行 CPU 繫繫的任務。這樣可以避免 GIL 的限制,提高程式的效能。需要注意的是,建立多個程式的成本比建立多個執行緒高,因此需要根據具體的情況選擇合適的方法。

圖表翻譯:

  flowchart TD
    A[主程式] --> B[建立多個程式]
    B --> C[執行 CPU 繫繫的任務]
    C --> D[等待所有程式完成]
    D --> E[結束]

上述圖表展示了使用多個程式代替多個執行緒的流程。首先,主程式建立多個程式,然後每個程式執行 CPU 繫繫的任務。最後,主程式等待所有程式完成,然後結束。

設計模式在 Python 中的應用

設計模式提供了軟體工程中各種問題的專業解決方案,使軟體更加堅固、可擴充套件和可靠。在本節中,我們將探討一系列設計模式,為您提供實際軟體工程中的解決方案。

工廠模式

設計模式是可重用的程式設計解決方案,已經在各種實際情境中被使用和證實。工廠模式是最常見的設計模式之一,它使得追蹤物件的建立變得更加容易,同時也將物件的建立和使用分離。工廠模式有兩種形式:工廠方法和抽象工廠。

工廠方法

工廠方法是一種根據單一函式的方法,根據輸入引數傳回不同的物件。這種方法使得物件的建立過程更加簡單,同時也減少了維護應用程式的複雜性。

抽象工廠

抽象工廠是一組工廠方法,用於建立相關物件的家族。這種方法使得物件的建立過程更加靈活,同時也減少了維護應用程式的複雜性。

實際應用

工廠模式在實際應用中非常常見,例如在塑膠玩具的生產中,同一種塑膠材料可以用來生產不同的玩具。這種情況下,工廠方法可以根據輸入引數傳回不同的玩具。

在軟體開發中,Django 網路框架使用工廠方法模式來建立網路表單的欄位。這種方法使得建立不同種類的欄位(例如字元欄位、電子郵件欄位等)變得更加容易。

程式碼實作

from django import forms

class PersonForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField(max_length=100)

在這個例子中,PersonForm 類別使用工廠方法模式來建立不同種類的欄位。

圖表翻譯:
  flowchart TD
    A[工廠模式] --> B[工廠方法]
    B --> C[抽象工廠]
    C --> D[建立物件]
    D --> E[傳回物件]

這個圖表展示了工廠模式的基本流程,從工廠模式到工廠方法和抽象工廠,最終建立和傳回物件。

內容解密:

工廠模式是一種設計模式,它使得物件的建立過程更加簡單,同時也減少了維護應用程式的複雜性。工廠方法和抽象工廠是工廠模式的兩種形式,前者是一種根據單一函式的方法,根據輸入引數傳回不同的物件;後者是一組工廠方法,用於建立相關物件的家族。透過工廠模式,開發人員可以建立更加靈活和可擴充套件的應用程式。

工廠模式(Factory Pattern)

工廠模式是一種建立型設計模式,提供了一種建立物件的方式,允許子類決定例項化哪一個類別。這種模式可以用於解耦物件的建立和使用,提高程式碼的靈活性和可維護性。

工廠模式的優點

工廠模式有以下優點:

  • 解耦物件的建立和使用:工廠模式可以將物件的建立和使用分離,允許子類決定例項化哪一個類別。
  • 提高程式碼的靈活性:工廠模式可以方便地新增新的類別或修改現有的類別,無需修改現有的程式碼。
  • 提高程式碼的可維護性:工廠模式可以減少程式碼的耦合度,提高程式碼的可維護性。

工廠模式的實作

以下是工廠模式的實作示例:

from abc import ABC, abstractmethod

# 抽象類別
class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

# 具體類別
class Dog(Animal):
    def speak(self):
        return "汪汪"

class Cat(Animal):
    def speak(self):
        return "喵喵"

# 工廠類別
class AnimalFactory:
    def create_animal(self, animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        else:
            raise ValueError("Invalid animal type")

# 客戶端程式碼
factory = AnimalFactory()
dog = factory.create_animal("dog")
print(dog.speak())  # Output: 汪汪

cat = factory.create_animal("cat")
print(cat.speak())  # Output: 喵喵

在這個示例中,Animal 是抽象類別,DogCat 是具體類別,AnimalFactory 是工廠類別。客戶端程式碼使用工廠類別建立物件,無需知道具體類別的實作細節。

工廠模式的應用場景

工廠模式可以應用於以下場景:

  • 物件建立複雜:當物件的建立過程複雜,需要多個步驟時,工廠模式可以簡化物件的建立過程。
  • 物件建立需要多個引數:當物件的建立需要多個引數時,工廠模式可以方便地傳遞引數。
  • 需要解耦物件的建立和使用:當需要解耦物件的建立和使用時,工廠模式可以提供一種解耦的方式。

實作工廠方法

資料有多種形式,主要分為人類可讀的檔案和二進位制檔案。人類可讀的檔案包括XML、RSS/Atom、YAML和JSON等。二進位制檔案包括SQLite的.db檔案和MP3音訊檔案等。在這個例子中,我們將關注兩種流行的人類可讀格式:XML和JSON。

雖然人類可讀的檔案通常比二進位制檔案解析速度慢,但它們使得資料交換、檢查和修改變得更加容易。因此,建議您盡可能使用人類可讀的檔案,除非有其他限制(主要是不可接受的效能和專有二進位制格式)。

在這個例子中,我們有一些輸入資料儲存在XML檔案和JSON檔案中,我們想要解析它們並檢索一些資訊。同時,我們想要集中客戶端與這些(和所有未來)外部服務的連線。我們將使用工廠方法來解決這個問題。

資料檔案

JSON檔案movies.json是一個包含美國電影資訊的資料集(標題、年份、導演名稱、型別等)。這是一個大檔案,但以下是其內容的一部分,以示範其組織結構:

[
  {
    "title": "After Dark in Central Park",
    "year": 1900,
    "director": null,
    "cast": null,
    "genre": null
  },
  {
    "title": "Boarding School Girls' Pajama Parade",
    "year": 1900,
    "director": null,
    "cast": null,
    "genre": null
  },
  ...
]

XML檔案person.xml是一個包含個人資訊的資料集(名字、姓氏、性別等)。以下是其內容的一部分:

<persons>
  <person>
    <firstName>John</firstName>
    <lastName>Smith</lastName>
    <age>25</age>
    <address>
      <streetAddress>21 2nd Street</streetAddress>
      <city>New York</city>
      <state>NY</state>
      <postalCode>10021</postalCode>
    </address>
    <phoneNumbers>
      <phoneNumber type="home">212 555-1234</phoneNumber>
      <phoneNumber type="fax">646 555-4567</phoneNumber>
    </phoneNumbers>
    <gender>
      <type>male</type>
    </gender>
  </person>
  <person>
    <firstName>Jimy</firstName>
    <lastName>Liar</lastName>
    <age>19</age>
    <address>
      <streetAddress>18 2nd Street</streetAddress>
      <city>New York</city>
      <state>NY</state>
      <postalCode>10021</postalCode>
    </address>
    <phoneNumbers>
      <phoneNumber type="home">212 555-1234</phoneNumber>
    </phoneNumbers>
    <gender>
      <type>male</type>
    </gender>
  </person>
  ...
</persons>

工廠方法實作

我們將使用Python的jsonxml.etree.ElementTree函式庫來解析JSON和XML檔案。

import json
import xml.etree.ElementTree as etree

內容解密:

上述程式碼使用json函式庫來解析JSON檔案,並使用xml.etree.ElementTree函式庫來解析XML檔案。這些函式庫提供了一種簡單的方式來解析和檢索資料。

圖表翻譯:

  graph LR
    A[JSON檔案] -->|解析|> B[JSON資料]
    B -->|檢索|> C[電影資訊]
    D[XML檔案] -->|解析|> E[XML資料]
    E -->|檢索|> F[個人資訊]
    C -->|集中客戶端|> G[工廠方法]
    F -->|集中客戶端|> G

上述圖表顯示了JSON和XML檔案的解析和檢索過程,以及集中客戶端的工廠方法。

資料提取器工廠方法實作

從技術架構視角來看,Python 的 GIL 無疑對多執行緒應用帶來了局限,尤其在 CPU 密集型任務中,多執行緒的效能提升不如預期。然而,透過多程式或其他能釋放 GIL 的技術方案,例如使用 C/C++ 擴充套件,則能有效避開 GIL 的限制,充分利用多核心處理器的優勢。更進一步,設計模式的應用,例如工廠模式,能有效提升程式碼的可維護性和擴充套件性,從而更好地管理不同資料格式(如 JSON 和 XML)的解析和處理。對於追求效能的 Python 開發者而言,深入理解 GIL 的機制並掌握多程式和設計模式的應用至關重要。玄貓認為,結合 GIL 的特性以及多程式和設計模式的靈活運用,才能在 Python 生態中打造高效能且易於維護的應用程式。