Python 的多執行緒受全域性直譯器鎖(GIL)限制,難以充分利用多核 CPU。子直譯器作為 Python 3.9 引入的新特性,提供了一種規避 GIL 限制的途徑,允許多個直譯器在同一程式中併發執行,各自擁有獨立的名稱空間和資源。文章提供的示例程式碼演示瞭如何使用子直譯器進行埠掃描,透過建立多個子直譯器平行處理不同埠,有效提升掃描效率。此外,多程式也是一種可行的併發方案,尤其適用於 CPU 密集型任務。文章對比了多執行緒和多程式的適用場景,並簡要介紹了非同步程式設計的應用。最後,文章深入剖析了 Python 物件模型,從 PyTypeObject 出發,講解了各種型別的結構和實作,包括 PyCellObject、PyVarObject,以及型別槽和 reprfunc 等關鍵概念,幫助讀者理解 Python 物件的底層運作機制。

多執行緒和子直譯器

在 Python 中,多執行緒和子直譯器是兩種不同的併發程式設計模型。多執行緒是指在同一個程式中同時執行多個執行緒,而子直譯器則是指在同一個程式中同時執行多個直譯器。

GIL 限制

Python 的 Global Interpreter Lock (GIL) 限制了多執行緒的併發性。GIL 是一種鎖機制,確保同一時間只有一個執行緒可以執行 Python 位元組碼。這意味著,即使有多個執行緒,Python 也只能利用一個 CPU 核心。

子直譯器

子直譯器是 Python 3.9 中引入的一種新特性。它允許在同一個程式中同時執行多個直譯器,每個直譯器都有自己的名稱空間和資源。子直譯器可以用於實作併發程式設計,避免 GIL 限制。

示例程式碼

以下是一個使用子直譯器的示例程式碼:

import _xxsubinterpreters as subinterpreters
from threading import Thread
import queue

def run(host, port, results):
    # 建立一個子直譯器
    channel_id = subinterpreters.channel_create()
    interpid = subinterpreters.create()

    # 執行一個字串作為子直譯器的程式碼
    subinterpreters.run_string(
        interpid,
        """
        import socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(1)
        result = sock.connect_ex((host, port))
        subinterpreters.channel_send(channel_id, result)
        """
    )

    # 取得子直譯器的結果
    result = subinterpreters.channel_recv(channel_id)
    results.put((host, port, result))

# 建立一個佇列來儲存結果
results = queue.Queue()

# 建立多個執行緒來執行子直譯器
threads = []
for host in ["example.com", "example.org"]:
    for port in range(1, 1024):
        thread = Thread(target=run, args=(host, port, results))
        thread.start()
        threads.append(thread)

# 等待所有執行緒完成
for thread in threads:
    thread.join()

# 列印結果
while not results.empty():
    host, port, result = results.get()
    print(f"{host}:{port} - {result}")

這個示例程式碼建立了多個子直譯器,每個子直譯器負責掃描一個不同的埠。子直譯器使用 socket 模組連線到指定的主機和埠,並將結果傳送回主執行緒。主執行緒然後列印預出所有的結果。

子直譯器是 Python 中的一種新特性,允許在同一個程式中同時執行多個直譯器。它可以用於實作併發程式設計,避免 GIL 限制。然而,子直譯器仍然是一個實驗性的特性,其 API 和實作可能會發生變化。

網路連線掃描工具

簡介

本工具使用多執行緒技術來掃描指定主機的網路連線,檢查哪些埠是開啟的。這個工具可以幫助系統管理員或網路安全人員快速地檢查網路服務的狀態。

程式碼解釋

以下是工具的核心程式碼:

import socket
import threading
from queue import Queue
import time

def run(host, port, results):
    # 建立一個socket物件
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 設定連線超時時間
    sock.settimeout(1)
    
    # 嘗試連線到指定的主機和埠
    try:
        sock.connect((host, port))
        # 如果連線成功,則將埠號碼放入結果佇列中
        results.put(port)
    except socket.error:
        # 如果連線失敗,則不做任何事情
        pass
    
    # 關閉socket物件
    sock.close()

if __name__ == '__main__':
    # 記錄開始時間
    start = time.time()
    
    # 指定主機地址
    host = "127.0.0.1"
    
    # 建立一個結果佇列
    results = Queue()
    
    # 建立多個執行緒來掃描不同的埠
    threads = []
    for port in range(80, 100):
        t = threading.Thread(target=run, args=(host, port, results))
        threads.append(t)
        t.start()
    
    # 等待所有執行緒完成
    for t in threads:
        t.join()
    
    # 列印掃描結果
    print("掃描完成,開啟的埠號碼:")
    while not results.empty():
        print(results.get())
    
    # 記錄結束時間
    end = time.time()
    
    # 列印掃描時間
    print("掃描時間:", end - start, "秒")

使用方法

  1. 執行工具:直接執行工具的Python指令碼即可開始掃描。
  2. 指定主機:修改host變數來指定要掃描的主機地址。
  3. 指定埠範圍:修改range(80, 100)來指定要掃描的埠範圍。
  4. 檢視結果:工具會列印預出開啟的埠號碼和掃描時間。

注意事項

  • 本工具僅供學習和測試使用,請勿用於非法目的。
  • 請確保您有足夠的許可權來掃描指定的主機和埠。
  • 本工具可能會對網路服務造成一定的負擔,請謹慎使用。

多執行緒和多程式的應用

在進行埠掃描時,使用多執行緒和多程式可以顯著提高掃描效率。透過建立多個執行緒或程式,可以同時掃描多個埠,從而減少掃描時間。

多執行緒的實作

Python 的 threading 模組提供了建立和管理執行緒的功能。以下是一個簡單的例子:

import threading
import time

def scan_port(port):
    # 模擬掃描埠的過程
    time.sleep(1)
    print(f"Port {port} is open")

# 建立執行緒列表
threads = []

# 建立 10 個執行緒,分別掃描 10 個埠
for i in range(10):
    t = threading.Thread(target=scan_port, args=(i,))
    threads.append(t)
    t.start()

# 等待所有執行緒完成
for t in threads:
    t.join()

多程式的實作

Python 的 multiprocessing 模組提供了建立和管理程式的功能。以下是一個簡單的例子:

import multiprocessing
import time

def scan_port(port):
    # 模擬掃描埠的過程
    time.sleep(1)
    print(f"Port {port} is open")

# 建立程式列表
processes = []

# 建立 10 個程式,分別掃描 10 個埠
for i in range(10):
    p = multiprocessing.Process(target=scan_port, args=(i,))
    processes.append(p)
    p.start()

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

比較多執行緒和多程式

多執行緒和多程式都可以用於併發執行任務,但是它們有不同的適用場景。

  • 多執行緒適用於 I/O 密集型任務,例如網路請求、檔案操作等。
  • 多程式適用於 CPU 密集型任務,例如科學計算、資料壓縮等。

在埠掃描的例子中,多執行緒可能更適合,因為掃描埠主要涉及網路 I/O 操作。

使用子直譯器

Python 3.8 及後續版本引入了子直譯器(subinterpreters)的概念,允許在同一個程式中執行多個獨立的 Python 直譯器。子直譯器可以用於併發執行任務,類別似於多執行緒和多程式。

以下是一個簡單的例子:

import asyncio

async def scan_port(port):
    # 模擬掃描埠的過程
    await asyncio.sleep(1)
    print(f"Port {port} is open")

async def main():
    tasks = []
    for i in range(10):
        task = asyncio.create_task(scan_port(i))
        tasks.append(task)
    await asyncio.gather(*tasks)

asyncio.run(main())

在 Python 中,多執行緒、多程式和子直譯器都可以用於併發執行任務。選擇合適的方法取決於任務的性質和要求。對於 I/O 密集型任務,多執行緒可能更適合;對於 CPU 密集型任務,多程式可能更適合;對於需要併發執行任務的場景,子直譯器可以提供一個新的選擇。

Python 物件模型

Python 的物件模型是根據 PyTypeObject,而函式則定義在 Objects/typeobject.c 中。每個原始檔案都有相應的標頭檔案在 Include 中。例如,Objects/rangeobject.c 有相應的標頭檔案 Include/rangeobject.h。

內建型別

以下是原始檔案和相應型別的列表:

原始檔案型別
Objects/object.c內建方法和基礎物件
Objects/boolobject.c布林型別
Objects/bytearrayobject.c位元組陣列型別
Objects/bytesobject.c位元組型別
Objects/cellobject.c細胞型別
Objects/classobject.c抽象類別型別
Objects/codeobject.c內建程式碼物件型別
Objects/complexobject.c複數型別
Objects/iterobject.c迭代器型別
Objects/listobject.c列表型別
Objects/longobject.c長整數型別
Objects/memoryobject.c基礎記憶體型別
Objects/methodobject.c類別方法型別
Objects/moduleobject.c模組型別
Objects/namespaceobject.c名稱空間型別
Objects/odictobject.c有序字典型別
Objects/rangeobject.c範圍產生器型別
Objects/setobject.c集合型別
Objects/sliceobject.c切片參考型別
Objects/structseq.c結構序列型別
Objects/tupleobject.c元組型別
Objects/typeobject.c型別物件型別
Objects/unicodeobject.c字串型別
Objects/weakrefobject.c弱參考型別

物件結構

C語言與Python不同,不是物件導向語言,因此C語言中的物件不會繼承彼此。PyObject定義了任何Python物件的原始資料段,而PyObject*代表了一個指向它的指標。

當定義Python型別時,會使用typedef和其中一種巨集:

  1. PyObject_HEAD (PyObject) 用於簡單型別。
  2. PyObject_VAR_HEAD (PyVarObject) 用於容器型別。

對於簡單型別,PyObject包含以下欄位:

欄位型別描述
ob_refcntPy_ssize_t參考計數器
ob_type_typeobject*物件型別

例如,cellobject宣告了一個額外的ob_ref欄位,除了基礎欄位之外。

瞭解 Python 物件和型別

Python 的物件和型別是程式設計的基礎。每個物件都有一個 ob_type 屬性,指向其型別的實作。型別是由 PyTypeObject 結構定義的,該結構包含了物件的行為和屬性。

PyCellObject

PyCellObject 是 Python 中的一種特殊物件,代表著一個儲存單元。它包含了以下屬性:

  • ob_ref: 儲存單元的內容,或 NULL 表示空單元
typedef struct {
    PyObject_HEAD
    PyObject *ob_ref; /* 儲存單元的內容,或 NULL 表示空單元 */
} PyCellObject;

PyVarObject

PyVarObjectPyObject 的擴充套件,增加了以下屬性:

  • ob_base: 基礎型別
  • ob_size: 儲存的元素數量

例如,整數型別 PyLongObject 的宣告如下:

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

型別和物件

Python 的物件都有一個 ob_type 屬性,指向其型別的實作。可以使用 type() 函式來取得物件的型別。

t = type("hello")
print(t)  # <class 'str'>
print(type(t))  # <class 'type'>

型別物件是 PyTypeObject 的例項,用於定義抽象基礎類別的實作。例如,所有物件都實作了 __repr__() 方法:

class example:
    x = 1

i = example()
print(repr(i))  # <__main__.example object at 0x10b418100>

__repr__() 方法的實作位於型別定義中的特定位置,稱為型別槽。

型別槽

所有型別槽都定義在 Include/cpython/object.h 中。每個型別槽都有一個名稱和函式簽名。例如,__repr__() 方法對應於 tp_repr 型別槽,其函式簽名為 reprfunc

struct PyTypeObject {
    //...
};

這些型別槽用於定義物件的行為和屬性,例如 __repr__() 方法的實作。

瞭解 Python 中的 reprfunc 及其實作

在 Python 的 C API 中,reprfunc 是一個重要的結構成員,負責傳回物件的字串表示。這個函式指標被定義為 typedef PyObject *(*reprfunc)(PyObject *);,它接受一個 PyObject* 作為引數,並傳回一個 PyObject*

reprfunc 的實作

在 Python 的實作中,reprfunc 通常被用於提供物件的字串表示。例如,當你使用 repr() 函式時,Python 會呼叫對應物件型別的 tp_repr 函式來取得其字串表示。

PyCell_Type 的實作

在給定的程式碼片段中,PyCell_Type 是一個 PyTypeObject 的例項,它代表了一種特殊的物件型別 —— cell。這個型別的 tp_repr 成員被設定為 cell_repr 函式,這意味著當你呼叫 repr() 函式在一個 cell 物件上時,Python 會執行 cell_repr 函式來取得其字串表示。

PyTypeObject PyCell_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "cell",
    sizeof(PyCellObject),
    0,
    (destructor)cell_dealloc, /* tp_dealloc */
    0, /* tp_vectorcall_offset */
    0, /* tp_getattr */
    0, /* tp_setattr */
    0, /* tp_as_async */
    (reprfunc)cell_repr, /* tp_repr */
   ...
};

cell_repr 函式

雖然給定的程式碼片段沒有提供 cell_repr 函式的具體實作,但根據其作為 reprfunc 的角色,我們可以推斷它的主要功能是傳回一個 cell 物件的字串表示。這個函式可能會根據 cell 物件的內部狀態生成一個描述其內容的字串。

PyObject* cell_repr(PyObject *self) {
    // 根據 self 的內部狀態生成字串表示
    //...
    return PyUnicode_FromFormat("Cell(%p)", self);
}

瞭解 Python 中的型別和物件

在 Python 中,型別和物件是兩個基本概念。型別(Type)是對物件的分類別和定義,而物件(Object)則是具體的例項。每個物件都有一個對應的型別,定義了它的屬性和行為。

型別的結構

在 Python 中,型別的結構是透過 PyTypeObject 這個結構體來定義的。這個結構體包含了許多成員變數,例如 tp_nametp_doctp_flags 等,用於描述型別的屬性和行為。

型別的槽位

型別的槽位(Slot)是用於儲存型別相關資料的特殊位置。每個槽位都有一個唯一的名稱和索引值,用於存取相應的資料。常見的槽位包括:

  • nb_:數值方法槽位,例如加法、減法等。
  • sq_:序列方法槽位,例如索引、切片等。
  • mp_:對映方法槽位,例如鍵值查詢等。
  • am_:非同步方法槽位,例如非同步迭代等。
  • bf_:緩衝方法槽位,例如讀寫緩衝區等。

存取型別的槽位

存取型別的槽位可以透過 PyTypeObject 的成員變數來實作。例如,要存取一個物件的 tp_repr 槽位,可以使用 o->ob_type->tp_repr 這個表示式。

實作型別的方法

實作型別的方法需要定義相應的函式指標,並將其儲存在對應的槽位中。例如,要實作一個型別的 __getitem__ 方法,可以定義一個函式指標 sq_item,並將其儲存在 tp_as_sequence 槽位中。

物件的存取

物件的存取可以透過 PyObject* 這個指標來實作。例如,要存取一個物件的屬性,可以使用 PyObject_GetAttr 這個函式。

序列物件的存取

序列物件的存取可以透過 PySequenceMethods 這個結構體來實作。例如,要存取一個序列物件的元素,可以使用 PySequence_GetItem 這個函式。

對映物件的存取

對映物件的存取可以透過 PyMappingMethods 這個結構體來實作。例如,要存取一個對映物件的鍵值對,可以使用 PyMapping_GetItemString 這個函式。

緩衝物件的存取

緩衝物件的存取可以透過 PyBufferProcs 這個結構體來實作。例如,要存取一個緩衝物件的內容,可以使用 PyBuffer_GetBuffer 這個函式。

內容解密:

上述內容介紹了 Python 中的型別和物件,包括型別的結構、型別的槽位、存取型別的槽位、實作型別的方法、物件的存取、序列物件的存取、對映物件的存取和緩衝物件的存取。其中,型別的結構透過 PyTypeObject 這個結構體來定義,型別的槽位用於儲存型別相關資料。存取型別的槽位可以透過 PyTypeObject 的成員變數來實作,實作型別的方法需要定義相應的函式指標,並將其儲存在對應的槽位中。物件的存取可以透過 PyObject* 這個指標來實作,序列物件、對映物件和緩衝物件的存取可以透過相應的結構體和函式來實作。

圖表翻譯:

上述圖表展示了 Python 中的型別和物件之間的關係,包括型別定義了物件、物件實作了序列、對映和緩衝等不同型別,以及這些型別之間的存取關係。其中,箭頭表示了定義和實作之間的關係,方塊表示了不同的型別和函式。這個圖表有助於理解 Python 中的型別和物件之間複雜的關係。

Python 中的類別和物件

Python 支援使用 class 關鍵字定義新的類別。使用者定義的類別是透過 type_new() 函式在物件類別模組中建立的。這些類別包含一個屬性字典,可以使用 __dict__() 函式存取。每當存取一個非標準類別的屬性時,預設的 __getattr__() 實作會查詢這個字典。

從技術架構視角來看,Python 的多執行緒、子直譯器、以及底層物件模型,都體現了其在併發處理和效能最佳化上的持續探索。分析 GIL 的限制以及子直譯器提供的解決方案,可以發現 Python 在平衡開發效率和執行效能上的努力。雖然子直譯器能繞過 GIL 限制,但也引入了新的複雜性和潛在的效能損耗,需要仔細評估其適用場景。深入 CPython 的原始碼,理解 PyTypeObject、PyObject、以及各種內建型別的底層實作,有助於開發者更有效地使用 Python,並針對特定應用場景進行效能調校。展望未來,隨著 Python 持續演進,預期子直譯器將更加成熟,並與多執行緒和多程式模型更好地整合,為 Python 的併發處理能力帶來顯著提升。玄貓認為,深入理解 Python 的底層機制,才能更好地駕馭這門語言的強大功能,並在實務應用中取得最佳效果。