返回文章列表

Python 效能提升:Rust 無縫整合執行緒與非同步程式設計

本文探討如何結合 Rust 與 Python,利用執行緒和非同步程式設計提升程式效能。文章首先闡述併發與平行概念,並以圖表分析執行緒在不同任務型別下的效能表現。接著,文章以程式碼範例說明 Rust 閉包的應用,並比較 Rust 與 Python

程式語言 效能最佳化

現代軟體開發中,效能最佳化至關重要,尤其在 I/O 密集型任務中,多執行緒能顯著提升處理速度。本文將探討如何結合 Rust 和 Python,利用執行緒和非同步程式設計提升效能。首先,我們會釐清併發和平行的概念,並分析執行緒在不同任務型別下的效能表現。接著,將透過 Rust 程式碼示範閉包的應用,並比較 Rust 與 Python 在多執行緒和多行程的實作差異,例如使用 Python 的 Thread 模組或 Process 模組。最後,我們將探討死結問題的成因、解決方案,以及如何在深度學習框架 TensorFlow 和 PyTorch 中整合 Rust 和 Python 以提升效能。

Python效能提升:Rust無縫整合:透過執行緒提升程式效能:非同步程式設計的基礎

透過執行緒提升程式效能:非同步程式設計的基礎

在現代軟體開發中,提升程式效能是一個關鍵課題。有效利用多執行緒可以顯著加快程式處理速度,特別是在 I/O 密集型任務中。本文將探討執行緒的原理、使用方式,以及如何避免常見的陷阱,並結合 Rust 的無縫整合來提升 Python 的執行效能。

併發與平行:效能提升的關鍵

併發和平行是兩個經常被混淆的概念。併發是指多個任務在同一個時間段內執行,但並非同時進行。而平行是指多個任務在同一時刻同時執行。執行緒分享資源,例如記憶體和處理能力,但也可能互相阻塞。

例如,如果一個執行緒需要持續佔用處理能力,它將會阻塞其他執行緒。以下圖示展示了兩個執行緒的執行情況:

從圖中可以看出,當執行緒 B 執行時,執行緒 A 停止執行。Pan Wu 在 2020 年關於多執行緒模擬的文章中也證實了這一點。文章中的結果總結如下圖:

CPU 密集型多執行緒任務的執行時間並不會隨著 worker 數量的增加而減少,這是因為執行緒之間的阻塞。而 I/O 密集型任務則可以從多執行緒中受益,因為在等待 I/O 操作完成的空閒時間,其他執行緒可以執行其他任務。

深入理解程式:執行緒與行程

首先了解什麼是「池」?「池」的概念類別似於一個工作中心,其中有多個 worker 同時處理輸入,然後將結果封裝成陣列。這種方法的優點是將多程式的開銷限制在程式的區域性,並能輕鬆控制 worker 的數量。在 Python 中,這意味著盡可能保持輕量級的互動。

以下圖示展示了「池」的概念:

這種方法將多程式的開銊限制在程式的區域性並且可以輕鬆控制 worker 的數量。

此外,「池」還能夠透過封裝函式與輸入陣列成一個元組來進行高效運作。以下圖示展示了這種封裝方式:

深入理解程式:閉包

閉包是一種匿名函式,它可以捕捉其所在環境的變數。以下是一個簡單的閉包範例:

fn main() {
    let example_closure = |string_input: &str| {
        println!("{}", string_input);
    };
    example_closure("這是一個閉包");
}

內容解密:

這段程式碼定義了一個名為 example_closure 的閉包,它接受一個字串引數並印出。閉包可以像普通函式一樣被呼叫。

另一個例子展示瞭如何利用閉包捕捉外部變數:

fn main() {
    let base_rate: f32 = 0.03;
    let calculate_interest = |loan_amount: &f32| {
        return loan_amount * base_rate;
    };
    println!("總利息為:{}", calculate_interest(&32567.6));
}

內容解密:

這段程式碼定義了一個 calculate_interest 閉包,它捕捉了外部變數 base_rate,用於計算貸款利息。閉包的型別是由編譯器自動推斷的。

Rust 與 Python 的多執行緒與多程式比較

在探討多程式之前,讓我們先回顧一下先前討論過的 Rust closures,並以此重現 Python 中的執行緒範例。首先,引入必要的標準模組:

use std::{thread, time};
use std::thread::JoinHandle;

這裡我們使用 thread 來產生執行緒,time 來追蹤程式執行時間,以及 JoinHandle 結構來連線執行緒。接著,我們可以建立一個簡單的執行緒函式:

fn simple_thread(seconds: i8, name: &str) -> i8 {
    println!("{} 執行", name);
    let total_seconds = time::Duration::new(seconds as u64, 0);
    thread::sleep(total_seconds);
    println!("{} 已完成", name);
    return seconds;
}

這個函式建立一個 Duration 結構 total_seconds,並使用 thread::sleep 讓函式休眠指定的秒數,最後傳回休眠的秒數。在 main 函式中,我們啟動計時器並產生三個執行緒:

let now = time::Instant::now();
let thread_one: JoinHandle<i8> = thread::spawn(|| { simple_thread(5, "A") });
let thread_two: JoinHandle<i8> = thread::spawn(|| { simple_thread(5, "B") });
let thread_three: JoinHandle<i8> = thread::spawn(|| { simple_thread(5, "C") });

我們使用 closure 將函式和引數傳遞給 thread::spawn。 Closure 內的最後一行將會是 JoinHandle 結構接收的傳回值。接著,我們使用 join 函式等待所有執行緒完成:

let result_one = thread_one.join();
let result_two = thread_two.join();
let result_three = thread_three.join();

####內容解密:

  • Result:Rust 中 Result 是一種傳回成功或失敗狀態結果。
  • Box:Box 是 Rust 中的一種智慧指標 (Smart Pointer),用於儲存大小未知或需要分配到堆積上 (heap) 的資料。
  • dyn:dyn 是 Rust 中表示動態分配型別 (Dynamic Dispatch) 的關鍵字。
  • Any:Any 是 Rust 中表示任何型別 (Any Type) 的特徵 (trait)。
  • Send:Send 是 Rust 中表示資料可安全傳遞至另一處理器 (processor) 或網路 (network) 的特徵 (trait)。

為了處理執行緒的結果,我們需要進行 downcasting(型別轉換),將特徵轉換為具體型別。以下是一個處理結果函式範例:

use std::any::Any;
use std::marker::Send;

fn process_thread(thread_result: Result<i8, Box<dyn Any + Send>>, name: &str) {
     match thread_result {
        Ok(result) => {
            println!("{} 結果 {}", name, result);
        }
        Err(result) => {
            if let Some(string) = result.downcast_ref::<String>() {
                println!("{} 錯誤 {}", name, string);
            } else {
                println!("{} 錯誤無訊息", name);
            }
        }
     }
}

最後停止計時器並處理結果:

println!("總耗時 {:?}", now.elapsed());
process_thread(result_one, "A");
process_thread(result_two, "B");
process_thread(result_three, "C");

以下圖示展示了流程:

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Python 效能提升:Rust 無縫整合執行緒與非同步程式設計

package "資料視覺化流程" {
    package "資料準備" {
        component [資料載入] as load
        component [資料清洗] as clean
        component [資料轉換] as transform
    }

    package "圖表類型" {
        component [折線圖 Line] as line
        component [長條圖 Bar] as bar
        component [散佈圖 Scatter] as scatter
        component [熱力圖 Heatmap] as heatmap
    }

    package "美化輸出" {
        component [樣式設定] as style
        component [標籤註解] as label
        component [匯出儲存] as export
    }
}

load --> clean --> transform
transform --> line
transform --> bar
transform --> scatter
transform --> heatmap
line --> style --> export
bar --> label --> export

note right of scatter
  探索變數關係
  發現異常值
end note

@enduml

內容解密: 此流程圖展示了 Rust 中如何透過主程式產生子程式、執 行及處理結果。

接下來我們看看如何將 Python 根據 Thread 改為 Process 做到跨程式運作:


from multiprocessing import Process

class ExampleProcess(Process):
      def __init__(self, seconds: int, name: str) -> None:
            super().__init__()
            self.seconds = seconds
            self.name = name

      def run(self):
            # 根據 CPU 語意進行運算

      def join(self):
            Process.join(self)

if __name__ == "__main__":
      example_process = ExampleProcess(seconds=5,name="example")
      example_process.start()
      example_process.join()

然而使用 Process 比 Thread 需要更高成本。 Python 根據 Thread 最好最佳化 I/O 資源競爭、反之使用 Process 能夠大幅度最佳化 CPU 資源競爭且可做到真正平衡運作。 而透過整合 Rust 語言精準性則更能夠精確控管資源競爭。

防止死結(Dead Lock):訊息代理、鎖機制與避免競爭條件

####死結(Dead Lock) 死結(Dead Lock)情況發生於兩或以上程式互相等待對方釋放資源而產生迴圈等待問題。 例如當 I/O 資源競爭時則會發生死結問題且需以代理控管方式解決。 當使用 Thread 模組應用時需注意死結問題常見情況即讀寫檔案模組導致死結問題。 故須透過 Rust 鎖機制及避免競爭條件達到絕對無死結設計目標。 而通常根據 Unix-like 作業系統及 Windows 作業系統都有提供鎖機制。 但根據 PHP、Python、Go、JavaScript、Rust、C# 或其他語言根據跨平台設計則可能未提供絕對支援鎖機制。 故 Play Cat 建議應依照語言特性及作業系統特性選擇最佳鎖機制來防止死結問題。

####鎖機制(Mutex) 鎖機制(Mutex)即保護分享資源不被重複存取而導致錯誤問題。 根據 Thread 模組讀寫檔案範例即可解釋此機制。 當有二或以上 Thread 想要讀寫同一部檔案時可能造成錯誤問題。 故需透過 Rust 鎖機制達到絕對安全防護分享資源問題。 以下 RUST 鎖機制範例說明:

use std::sync::{Mutex};

let counter_mutex = Mutex::new(counter);
{
   let mut counter_mutex_guard = counter_mutex.lock().unwrap();

   // 在這裡操作 counter_mutex_guard 拿到之值即可達到絕對防護安全目的.
}

####競爭條件(Race Condition) 競爭條件即當二或以上程式同時存取及修改相同分享資源則可能造成錯誤問題。 例如當二或以上程式同時存取及修改同一部檔案時可能造成錯誤問題。 故需透過適當控管分享資源防止競爭條件達到絕對安全目的. 如上述所提到以 Rust 跨程式模組實作可達到絕對安全防護分享資源目的.

####防止死結最佳實踐原則:

  1. 避免釋放分享資源前進行長時間等待操作.
  2. 避免無限迴圈操作.
  3. 避免長時間擁有鎖操作.
  4. 避免長時間佔用分享資源.

Rust 與 Python 整合技術應使用案例項分析

Rust 無縫整合Python語言設計技巧與技術應使用案例項分析

####Python整合Rust技術例項分析:

  1. Python 介面整合: 首先需安裝 PyO3 包來進行Python與Rust語言整合, 需先安裝 cargo build來編譯 PyO3 包, 接著再以 Python 語言 import PyO3 包即可達到介面整合目的。
import my_rust_package

result = my_rust_package.add_function(1,2)

print(result)
#[pyfunction]
fn add_function(a:i32,b:i32)->PyResult<i32>{
   Ok(a+b)
}

#[pymodule]
fn my_rust_package(py:Python)->PyResult<PyModule>{
   let m=py_module!(py,"my_rust_package");
   m.add_function(wrap_pyfunction!(add_function,pymodule_name="my_rust_package"))?;
   Ok(m)
}

透過以上步驟即可達成 Python 與 Rust 語言介面整合目的。

####Python整合Rust技術例項分析:

  1. Python 介面整合: 首先需安裝 PyO3 包來進行Python與Rust語言整合, 需先安裝 cargo build來編譯 PyO3 包, 接著再以 Python 語言 import PyO3 包即可達到介面整合目的。
import my_rust_package

result = my_rust_package.add_function(1,2)

print(result)
#[pyfunction]
fn add_function(a:i32,b:i32)->PyResult<i32>{
   Ok(a+b)
}

#[pymodule]
fn my_rust_package(py:Python)->PyResult<PyModule>{
   let m=py_module!(py,"my_rust_package");
   m.add_function(wrap_pyfunction!(add_function,pymodule_name="my_rust_package"))?;
   Ok(m)
}

透過以上步驟即可達成 Python 與 Rust 語言介面整合目的。

在深度學習應用中的Rust與Python 整合技術例項分析

深度學習已經成為現代人工智慧應用中不可或缺的一部分, 而在深度學翶應用上透過 Python 與 Rust 語言無縫整合則更能夠大幅度提升效能表現, 故以下 Play Cat 提供一些技術例項說明其基本原理及應用方式.

TensorFlow 與 PyTorch 框架整合例項分析:

TensorFlow 與 PyTorch 是目前最廣泛使用之深度學翶框架, 但根據 TensorFlow 與 PyTorch 本身效能限制, 故需透過跨語言開發 (cross-language development) 技術來提升效能表現, 例如透過 RUST 語言開發 CUDA 模組來加速計算, 以下範例說明 TensorFlow 與 PyTorch 框架如何透過 RUST 語言加速計算.

TensorFlow 框架整合 RUST 語言範例:

use tensorflow::{Graph, ImportGraphDefOptions};
use tensorflow::core_framework::{Tensor};

let graph_def = include_bytes!("../path/to/frozen_graph.pb");

let mut graph = Graph::new();
graph.import_graph_def(graph_def.as_ref(), &ImportGraphDefOptions::new()).unwrap();

let mut input_tensor=Tensor::<f32>::new(&[1]).unwrap();
input_tensor.copy_from_slice(&vec![0.5]);

let output_tensor=graph.run(&["output"], & [("input", &input_tensor)]).unwrap();

println!("{}",output_tensor.get_slice());

以上範例說明如何透過 TensorFlow 框架及 RUST 語言加速計算表現.

Pytorch框架整合 RUST 語言之加速計算範例:

Pytorch 框架推薦官方提供libtorch 函式庫供RUST語言開發者呼叫,

其中 libtorch 函式庫提供 C++ API 呼叫介面供RUST語言開發者呼叫, 以此達成跨語言開發(Python語言呼叫Pytorch框架,Pytorch框架呼叫libtorch函式庫),達成最佳化效能表現.

以下範例說明如何透過 Pytorch 框架及 RUST 語言加速計算表現:


#include <torch/torch.h>
#include <iostream>

int main() {

   auto tensor_a=torch::rand({4});
   auto tensor_b=torch::rand({4});

   auto tensor_sum=tensor_a+tensor_b;

   std::cout << tensor_sum << std::endl;

return 0;
}