运行 Python Rust 与 rust-cpython 的并行代码

Running Python code in parallel from Rust with rust-cpython

我正在尝试使用 Rust 加速数据管道。管道包含我不想修改的 Python 代码位,因此我尝试使用 rust-cpython 和多线程从 Rust 中按原样 运行 它们。 然而,性能不是我所期望的,它实际上与在单个线程中按顺序 运行ning python 代码位相同。

阅读文档,我了解到在调用以下内容时,您实际上得到了一个指向单个 Python 解释器的指针,即使您分别从多个线程 运行 它也只能创建一次.

    let gil = Python::acquire_gil();
    let py = gil.python();

如果是这样,则意味着 Python GIL 实际上也在阻止 Rust 中的所有并行执行。有没有办法解决这个问题?

这是我的测试代码:

use cpython::Python;
use std::thread;
use std::sync::mpsc;
use std::time::Instant;

#[test]
fn python_test_parallel() {
    let start = Instant::now();

    let (tx_output, rx_output) = mpsc::channel();
    let tx_output_1 = mpsc::Sender::clone(&tx_output);
    thread::spawn(move || {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let start_thread = Instant::now();
        py.run("j=0\nfor i in range(10000000): j=j+i;", None, None).unwrap();
        println!("{:27} : {:6.1} ms", "Run time thread 1, parallel", (Instant::now() - start_thread).as_secs_f64() * 1000f64);
        tx_output_1.send(()).unwrap();
    });

    let tx_output_2 = mpsc::Sender::clone(&tx_output);
    thread::spawn(move || {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let start_thread = Instant::now();
        py.run("j=0\nfor i in range(10000000): j=j+i;", None, None).unwrap();
        println!("{:27} : {:6.1} ms", "Run time thread 2, parallel", (Instant::now() - start_thread).as_secs_f64() * 1000f64);
        tx_output_2.send(()).unwrap();
    });

    // Receivers to ensure all threads run
    let _output_1 = rx_output.recv().unwrap();
    let _output_2 = rx_output.recv().unwrap();
    println!("{:37} : {:6.1} ms", "Total time, parallel", (Instant::now() - start).as_secs_f64() * 1000f64);
}

Python 的 CPython 实现不允许同时在多个线程中执行 Python bytecode。正如您自己注意到的,全局解释器锁 (GIL) 阻止了这种情况。

我们没有关于您的 Python 代码究竟在做什么的任何信息,因此我将提供一些一般提示,告诉您如何提高代码的性能。

  • 如果您的代码是 I/O-bound,例如从网络上阅读,您通常会通过使用多线程获得不错的性能提升。阻塞 I/O 调用将在阻塞之前释放 GIL,因此其他线程可以在此期间执行。

  • 一些库,例如NumPy,在不需要访问 Python 数据结构的长期 运行ning 库调用期间在内部释放 GIL。使用这些库,即使您只使用库编写纯 Python 代码,您也可以获得多线程、CPU 绑定代码的性能改进。

  • 如果你的代码是 CPU-bound 并且大部分时间都在执行 Python 字节码,你可以经常使用 multipe processes 而不是线程来实现并行执行。 Python 标准库中的 multiprocessing 对此有所帮助。

  • 如果您的代码是 CPU 绑定的,则大部分时间都在执行 Python 字节码 不能 运行 在并行进程中,因为它访问共享数据,你不能在多个线程中并行地 运行 它——GIL 阻止了这种情况。但是,即使没有 GIL,您也不能只 运行 并行顺序代码而不更改 any 语言。由于您可以并发访问某些数据,因此需要添加锁定并可能进行算法更改以防止数据竞争;如何执行此操作的详细信息取决于您的用例。 (如果你没有并发数据访问,你应该使用进程而不是线程——见上文。)

除了并行性之外,使用 Rust 加速 Python 代码的一个好方法是 profile 您的 Python 代码,找到 热点 花费了大部分时间,并且 重写 这些位作为您从 Python 代码调用的 Rust 函数。如果这不能给你足够的加速,你可以将这种方法与并行性结合起来——防止数据竞争在 Rust 中通常比在大多数其他语言中更容易实现。