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

在单线程下,线程等待了3s,运行了10次,共需要 30s。多线程模式下,每个线程都等待 3s,但线程进入了等待状态(如 io 、时间事件等),GIL 被释放出来,其它线程开始执行,所以差不多所有线程同时进入了等待状态,整体等待时间为 3s,运行10次,共需要30s。因此在 io 密集型的应用中,由于线程频繁进入等待状态,还是可以使用 python 的多线程,注意给资源加锁保护就行了。

不过,现在 python 也支持 async 编程,比起用 threading 来说更加接近于普通编程模式,将来有可能大家会到转到 aysnc 编程模式下了,像 rust 一样,async、await 模式逐步成为大家的基本技能。

posted @ 2022-04-18 22:30  drop *  阅读(117)  评论(0编辑  收藏  举报