基於Python的效能最佳化

changwan發表於2024-05-24

一、多執行緒

在CPU不密集、IO密集的任務下,多執行緒可以一定程度的提升執行效率。

import threading
import time
import requests

def fetch_url(url: str)->None:
    '''根據地址發起請求,獲取響應
    - url: 請求地址'''
    response = requests.get(url)
    print(f"{url}: {response.status_code}")

def fetch_urls_sequential(urls:list)->None:
    start_time = time.time()
    for url in urls:
        fetch_url(url)
    end_time = time.time()
    print(f"使用單執行緒時間為: {end_time - start_time} 秒\n")

def fetch_urls_concurrent(urls:list)->None:
    start_time = time.time()
    threads = []
    for url in urls:
        thread = threading.Thread(target=fetch_url, args=(url,))
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()
    end_time = time.time()
    print(f"使用多執行緒時間為: {end_time - start_time} 秒")

if __name__ == "__main__":
    urls = ["http://www.example.com"]*10
    fetch_urls_sequential(urls)
    fetch_urls_concurrent(urls)

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

使用單執行緒時間為: 10.178432703018188 秒

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

http://www.example.com: 200

使用多執行緒時間為: 0.5794060230255127 秒

可以看到在IO密集型任務時,排除極端情況,使用多執行緒可以很大的提升程式的效能。例如在這個例子中,響應時間就相差了8倍多。

雖然在Python中有GIL保護機制,但是依然需要注意執行緒安全。例如(共享資料、共享裝置、非原子性操作等)。可以使用鎖機制、訊號機制、佇列、管道等等。

二、協程

協程也叫輕量級執行緒,協程是一種在單一執行緒內實現併發程式設計的技術。它們允許函式在執行過程中暫停,並在稍後恢復,從而使得多個任務能夠交替執行,而不需要多個作業系統執行緒的開銷。協程透過讓出控制權來暫停執行,等待其他協程執行,然後在適當的時候恢復執行。

區別 執行緒 協程
上下文切換 執行緒上下文切換由作業系統決定,消耗更大 協程的上下文切換由使用者自己決定,消耗更小
併發 執行緒是搶佔式的,作業系統可以隨時中斷執行緒排程了一個執行緒 協程是協程式的,需要主動讓出控制權時,才會進行任務切換
開銷 建立執行緒和銷燬執行緒,造成很大的開銷 基於單執行緒的,並且協程是輕量級的,不會消耗大量資源。

協程的優勢在於能夠更高效地利用系統資源,在執行多個任務時能夠充分利用CPU的效能。相比之下,併發執行的多個協程可以在單個執行緒內非阻塞地交替執行,從而減少了執行緒切換和上下文切換的開銷,提高了整體的執行效率。

所以協程本身並不會直接提升單個任務的執行時間,但是,如果一個任務可以分解為多個步驟,並且這些步驟之間存在依賴關係,那麼使用協程來執行這些步驟會更快。因為在等待I/O操作或其他非同步操作完成時,協程可以讓出CPU控制權,允許其他協程繼續執行,從而最大程度地減少了等待時間。

例如從網站下載頁面內容,並且計算頁面內容。這就是單任務多步驟,這種情況就可以體現出協程的優勢(效能、執行時間都會提升)。

import asyncio
import threading
import aiohttp
import time
import requests

async def fetch_page(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()
        
async def compute_length(url):
    page = await fetch_page(url)
    return len(page)

async def async_main():
    urls = ["http://www.example.com"]*10
    tasks = [compute_length(url) for url in urls]
    results = await asyncio.gather(*tasks)
    print("內容長度為:", results)

def thread_fetch_page(url):
    response = requests.get(url)
    return response.text
def thread_compute_length(url):
    page = thread_fetch_page(url)
    return len(page)
def thread_main():
    urls = ["http://www.example.com"]*10    
    threads = []
    results = []
    start_time = time.time()
    for url in urls:
        thread = threading.Thread(target=lambda u: results.append(thread_compute_length(u)), args=(url,))
        threads.append(thread)
        thread.start()
    for thread in threads:
        thread.join()
    end_time = time.time()
    print("內容長度為:", results)
    print(f"使用多執行緒時間為: {end_time - start_time} 秒")    

if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(async_main())
    end_time = time.time()
    print(f"使用協程時間為: {end_time - start_time} 秒")
    thread_main()

內容長度為: [1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256]

使用協程時間為: 0.5775842666625977 秒

內容長度為: [1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256]

使用多執行緒時間為: 5.595600128173828 秒

可以看到這裡的協程的執行時間提升了很多,因為是單任務多步驟,類似於流水線的方式,所以協程的速度會快很多。並且這裡使用協程是單執行緒的,開銷更小;而多執行緒這裡使用了10個執行緒,開銷更大。

三、多程序

如果任務主要由 CPU 運算組成(CPU密集型任務),而不涉及太多的 I/O 操作,那麼多程序通常比多執行緒更適合,因為多程序能夠利用多核處理器的全部效能,每個程序獨立執行在自己的地址空間中,避開了 GIL 的限制。

例如大規模的計算,這種耗時的計算也是CPU密集型任務,使用多程序能明顯的提升效能。

這裡舉例計算大規模積分

import multiprocessing
import threading
import numpy as np
import time

def integrate(f, a, b, N):
    """使用梯形法則計算f在區間[a, b]上的積分,N為分割數"""
    x = np.linspace(a, b, N)
    y = f(x)
    dx = (b - a) / (N - 1)
    return np.trapz(y, dx=dx)

def f(x):
    '''計算積分'''
    return np.sin(x) * np.exp(-x)

def integrate_range(start, end, result, index):
    result[index] = integrate(f, start, end, 100000000) 

def thread_main():
    result = [None] * 4
    threads = []
    ranges = [(0, 5), (5, 10), (10, 15), (15, 20)]  

    start_time = time.time()

    for i, (start, end) in enumerate(ranges):
        thread = threading.Thread(target=integrate_range, args=(start, end, result, i))
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

    end_time = time.time()
    print(f"多執行緒使用時間為: {end_time - start_time} 秒")
    print(f"積分結果: {result}\n")
def multiprocess_main():
    manager = multiprocessing.Manager()
    result = manager.list([None] * 4)
    processes = []
    ranges = [(0, 5), (5, 10), (10, 15), (15, 20)]  
    start_time = time.time()

    for i, (start, end) in enumerate(ranges):
        process = multiprocessing.Process(target=integrate_range, args=(start, end, result, i))
        processes.append(process)
        process.start()

    for process in processes:
        process.join()

    end_time = time.time()
    print(f"多程序使用時間為: {end_time - start_time} 秒")
    print(f"積分結果: {result}")

if __name__ == "__main__":
    thread_main()    
    multiprocess_main()    

多執行緒使用時間為: 7.396134853363037 秒

積分結果: [0.5022749400837572, -0.0022435439294056455, -3.1379421486677834e-05, -1.809428816655182e-08]

多程序使用時間為: 4.97518515586853 秒

積分結果: [0.5022749400837572, -0.0022435439294056455, -3.1379421486677834e-05, -1.809428816655182e-08]

可以看到這裡的區別還是很大的,如果資料量更大,那麼程序的優勢會更明顯。因為如果計算的時間過快,那麼執行緒可以很快的進行切換。所以在大規模計算時,才可以體現出程序的優勢。

四、總結

特性 程序 執行緒 協程
建立開銷 極小
切換開銷 極小
記憶體共享 不共享 共享 共享
通訊方式 管道、佇列等 共享記憶體 直接呼叫
多核利用 受GIL影響
使用場景 CPU密集型任務 IO密集型任務 高併發IO密集型任務
複雜度 較高 較低 依賴非同步程式設計。較高

1、程序(Process)

  • 定義:程序是作業系統分配資源和排程的基本單位。每個程序擁有獨立的記憶體空間、檔案描述符和其他資源。

  • 優點:

    • 獨立性:程序之間相互獨立,不會直接影響彼此的執行,崩潰一個程序不會影響其他程序。
    • 利用多核:能夠充分利用多核 CPU 的優勢,每個程序可以在不同的 CPU 核心上並行執行。
  • 缺點:

    • 開銷大:程序建立和銷燬的開銷較大,包括記憶體空間、檔案控制代碼等資源。
    • 通訊複雜:程序間通訊(IPC)比較複雜,常用的 IPC 機制包括管道、訊息佇列、共享記憶體等。
  • 使用場景:

    • CPU 密集型任務,計算量大且需要充分利用多核 CPU 效能。
    • 需要高可靠性的任務,程序隔離可以防止任務間相互影響。

2、執行緒(Thread)

  • 定義:執行緒是程序中的一個執行流,是 CPU 排程和執行的基本單位。執行緒共享程序的記憶體和資源。

  • 優點:

    • 輕量級:建立和銷燬執行緒的開銷較小,執行緒之間的上下文切換開銷比程序小。
    • 共享記憶體:同一程序的執行緒共享記憶體和資源,資料交換和通訊更方便。
  • 缺點:

    • GIL 限制:在 Python 中,由於全域性直譯器鎖(GIL),多執行緒在同一時間只能有一個執行緒執行 Python 位元組碼,限制了多執行緒在 CPU 密集型任務中的效能提升。
    • 執行緒安全:共享資料時需要小心處理執行緒同步問題,避免資料競爭、死鎖等問題。
  • 使用場景:

    • I/O 密集型任務,如檔案讀寫、網路請求等,可以在等待 I/O 完成時切換執行緒,提升效率。
    • 任務之間需要頻繁的資料共享和通訊的場景。

3、協程(Coroutine)

  • 定義:協程是一種更輕量級的併發執行方式,協程在使用者空間內實現切換,由程式自身控制,不依賴作業系統的排程。

  • 優點:

    • 極輕量:協程的建立和切換開銷極小,適合大量併發任務。
    • 無鎖程式設計:協程之間通常不需要鎖機制,因為協程在同一個執行緒中執行,不存在多執行緒的競爭問題。
    • 高效利用 I/O 等待:協程特別適合 I/O 密集型任務,可以在 I/O 等待時切換到其他協程執行。
  • 缺點:

    • 單執行緒限制:協程在單執行緒中執行,無法利用多核 CPU 的優勢。
    • 需要非同步程式設計支援:需要語言和框架的非同步支援,編寫非同步程式碼較為複雜。
  • 使用場景:

    • 高併發的 I/O 密集型任務,如大量的網路請求處理、Web 伺服器等。
    • 需要大量併發但任務之間獨立性較高的場景。

相關文章