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。
常見問題
- Python 和 C++ 之間的記憶體管理有何不同?
- GIL 解決了 Python 中的哪個問題?
- GIL 創造了哪個問題?
- 有哪些方法可以在 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
是抽象類別,Dog
和 Cat
是具體類別,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的json
和xml.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 生態中打造高效能且易於維護的應用程式。