1. 程式人生 > 其它 >python 的多執行緒與 GIL

python 的多執行緒與 GIL

python 的多執行緒與 GIL

python 的多執行緒多少有點違背大家的直覺,由於 GIL 的存在和執行緒上下文的切換,多執行緒並沒有起到加快運算速度,反而更慢。以最常用的 CPython 為例,由於 GIL 的存在,以下 CPU 密集型的應用,單執行緒和多執行緒結果並沒多少差別。

python 版本的多執行緒對比

以下的示例程式碼為 CPU 密集型的計算,分為單執行緒和用 threading 實現的多執行緒(按照機器 cpu 數量確定執行緒數量)

from os import stat
from timeit import timeit
import threading
import multiprocessing

def do_time_consuming_task(m, n):
    s = 0
    for i in range(m, n):
        s = i * i

def do_split_multi_threads(n):
    cpus = multiprocessing.cpu_count() - 1
    step = n // cpus
    threads = []
    
    a = 0
    while a < n:
        b = a + step
        b = n if b > n else b
        thread = threading.Thread(target=do_time_consuming_task, args=(a, b))
        threads.append(thread)
        a = b

    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

def benchmark(n, times=10):
    single_thread_time = timeit(stmt=f'do_time_consuming_task(0, {n})',
                                number=times, setup='from __main__ import do_time_consuming_task')
    print("single thread:", single_thread_time)

    multi_thread_time = timeit(stmt=f'do_split_multi_threads({n})',
                               number=times, setup='from __main__ import do_split_multi_threads')
    print("multi threads:", single_thread_time)


if __name__ == "__main__":
    benchmark(10000000)

在 macbook m1pro 10 核上執行時,結果為

single thread: 2.697239291
multi threads: 2.697239291

雖然問題已拆分為多個執行緒,但多執行緒並沒有得到明顯更優的結果。

rust 版本的多執行緒對比

換成 rust,程式碼如下(threadings.rs)

use std::thread;
use std::time::Instant;

fn do_time_consuming_task(m: usize, n: usize) {
    let mut _s: usize = 0;
    for i in m..n {
        _s = i * i;
    }
}

fn split_multi_thread(n: usize, cpus: usize) {
    let step = n / cpus;
    let mut a = 0;
    let mut join_handles = vec![];
    while a < n {
        let b = if a + step < n { a + step } else { n };
        let (a1, b1) = (a, b);
        let thread_join_handle = thread::spawn(move || do_time_consuming_task(a1, b1));
        join_handles.push(thread_join_handle);
        a = b;
    }
    for join_handle in join_handles {
        join_handle.join().unwrap();
    }
}

fn timeit<F>(f: F, number: usize) -> f64
where
    F: Fn() -> (),
{
    let t0 = Instant::now();
    for _ in 0..number {
        f()
    }
    let t1 = Instant::now();

    let d = t1.duration_since(t0);
    d.as_micros() as f64 / 1000000.0
}

fn benchmark(n: usize, times: usize) {
    let t = timeit(|| do_time_consuming_task(0, n), times);
    println!("single thread: {}", t);

    let t = timeit(|| split_multi_thread(n, 10), times);
    println!("multi threads: {}", t);
}

fn main() {
    benchmark(10000000, 10);
}

使用 opt-level=0 進行編譯,防止被優化

rust -C opt-level=0 threadings.rs

執行結果

./threadings
single thread: 1.071244
multi threads: 0.168873

基本上,多執行緒為單執行緒的 1/61/7 左右(存在優化的空間,另外 cpu 效能不對稱),但可見多執行緒是生效的。

python 多執行緒合適用途

按照這個測試結果,在伺服器上,大部分情況下並不使用 python 多執行緒模式來執行,而通過多程序的模式來執行 python (帶 GIL 的 python 執行時)。

但這並不意味著 python 的多執行緒無用,如果 python 的程式碼改為

from time import sleep

def do_time_consuming_task(m, n):
    sleep(3)

那麼,結果為:

single thread: 30.038073875000002
multi threads: 30.038073875000002

原因在於執行緒進入了等待狀態(如 io 、時間事件等),GIL 被釋放出來。因此在 io 密集型的應用中,還是可以使用 python 的多執行緒。

不過,現在 python 也支援 async 程式設計,比起用 threading 來說更加接近於普通程式設計模式,將來有可能大家會到轉到 aysnc 程式設計模式下了,像 rust 一樣,async、await 模式逐步成為大家的基本技能。