Rust 多线程架构业务落地【知识点罗列】

207 阅读12分钟

所有权系统(Ownership System)

Rust 的所有权系统是其最核心的特性之一,它通过编译时检查来保证内存安全。

1.1 所有权与移动(Ownership and Move Semantics)

在电子面单业务中,每个任务(如绘制 PDF、处理打印请求)都有一个明确的所有者,这些任务的数据在传递时会“移动”所有权,确保同一数据不会被多个任务同时使用,避免了内存访问冲突。

例如,PdfDocument 在生成 PDF 时需要将文档所有权从创建函数传递到绘制任务中。

示例:创建 PDF 文档

在下面这段代码中:

let doc_result = PdfDocument::new(&pdf_name, page_width, page_height, "Layer 1");
let (doc, page1, layer1) = doc_result;

调用 PdfDocument::new 返回一个元组,其中包含了 PDF 文档(doc)、第一页索引(page1)以及第一页的图层索引(layer1)。这里的返回值在解构时,所有权就从函数返回值“移动”到了局部变量 doc、page1 和 layer1 上,保证了后续对 doc 的使用不会引起多重所有权问题。如果需要在多个地方使用同一个 doc,必须使用引用或者包装在 Arc(引用计数智能指针)中。

1.2. 引用与借用(References and Borrowing)

Rust 支持通过引用来“借用”数据而不获取其所有权。这对电子面单业务尤为重要,因为在打印任务的处理中,任务对象的数据可能需要被多个线程同时读取,但不需要修改,借用避免了不必要的数据拷贝。

例如,以下函数 download_and_save_image 接受不可变引用作为参数,以借用数据而不获取所有权:

pub async fn download_and_save_image(
    task_id: &str,
    url: &str,
    save_path: &PathBuf,
    file_name: &str,
) -> Result<(), Box<dyn std::error::Error>> { ... }

参数 task_id、url 和 file_name 都是通过不可变引用传入的,这样函数内部可以读取它们的数据而不取得所有权,从而避免了拷贝和所有权转移的问题。借用规则确保在引用存在期间,数据不会被销毁或修改成不符合预期的状态。

1.3. 克隆与复制(Cloning and Copying)

有时我们需要显式复制数据而不是移动所有权。例如,在处理任务队列时,DrawTask 会被克隆并传递到多个线程:

let draw_task = DrawTask {
    task_id: document_uuid.clone(),
    original_task_id: taskid.clone(),
    printer: printer_name.clone(),
    printdata: printdata.clone(),
    options: options.clone(),
    web_status: web_status.clone(),
    sequence,
};

这里使用 .clone() 将 String 和 Value 的数据复制一份,从而在传递给绘制队列时,不会因为所有权转移而导致后续对原数据的引用失效。

1.4. 智能指针与共享所有权(Smart Pointers and Shared Ownership)

在并发环境中,多个线程可能需要共享数据。Rust 提供了智能指针(如 Arc 和 Mutex)来管理共享所有权。对于电子面单系统中可能涉及的多线程任务队列,Arc<Mutex> 可以确保数据的安全共享。

lazy_static! {
    pub static ref EXECUTION_DATA: Arc<RwLock<Vec<ThreadExecution>>> = Arc::new(RwLock::new(Vec::new()));
}

这个 EXECUTION_DATA 允许多个线程共享任务执行记录,并且通过 RwLock 保证了在多个线程中可以安全地读取和写入数据。

static ref PRINT_QUEUE: Mutex<PrintTaskQueue> = Mutex::new(PrintTaskQueue::new());

PRINT_QUEUE 使用了 Mutex(这里是 tokio 的异步 Mutex)来保证在异步环境中对共享队列的互斥访问。当多个异步任务同时需要修改队列时,编译器和运行时都会确保只有一个任务可以获得可变引用,从而避免数据竞争。

1.5 生命周期与借用检查(Lifetimes and Borrow Checker)

生命周期标注是 Rust 中的一项强大功能,确保了所有引用在合法的生命周期内不会悬挂。在处理异步任务队列时,我们需要确保任务在执行过程中不会被提前销毁或引用。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {  
  if x.len() > y.len() { x } else { y }
}

在 execute_component_main 函数中,我们需要确保在任务绘制过程中,PDF 文档和图层的引用在合法的生命周期内保持有效。

longest 函数接受两个字符串切片的引用,并返回一个生命周期为 'a 的字符串切片引用。这意味着返回的引用与输入的两个引用一样长。

2. 并发编程与线程池

电子面单系统需要处理大量的打印任务。Rust 提供了强大的并发模型,利用 tokio 库,我们可以高效地管理并发任务。

2.1 异步任务队列与工作线程

initialize_task_queue 函数创建了一个任务队列,并启动多个工作线程来并发处理绘制任务:

pub async fn initialize_task_queue() {
    let (print_tx, print_rx) = std_mpsc::channel::<PrintTask>();
    // 启动绘制工作线程
    for i in 0..num_draw_workers {
        tokio::spawn(async move {
            loop {
                let task = draw_rx.recv().await;
                // 处理任务...
            }
        });
    }
}
 

通过 tokio::spawn 启动多个绘制任务线程,每个线程从任务队列中获取任务并处理,这使得系统能够并行处理多个打印任务,提高了任务的处理效率。

2.2 信号量与并发控制

为了防止任务过载和资源争用,我们使用 MonitoredSemaphore 来控制并发任务的数量:

struct MonitoredSemaphore {
    total_permits: usize,
    active_tasks: AtomicUsize,
    waiting_tasks: AtomicUsize,
}
 

3 基础知识点

3.1 特征(Traits) 特征是 Rust 中的抽象类型,它们允许为不同类型定义共同的行为。

async fn execute_component_main(
    task_id: &str,
    json: &Value,
    doc: &PdfDocumentReference,
    page_width: Mm,
    page_height: Mm,
    (page1, layer1): (PdfPageIndex, PdfLayerIndex),
    subset_font_path: String,
    gap_width: f32,
    gap_height: f32,
) {
    log_method_start(task_id, "execute_component_main");
 
    info!(
        "【{}】【execute_component_main】subset_font_path:确保进入subset_font_path有数据 {}",
        task_id, subset_font_path
    );
    let execute_component_main_start_time = Instant::now();
    let data_list = match json.as_array() {
        Some(list) => list,
        None => {
            error!("【{}】【execute_component_main】输入数据不是有效的数组", task_id);
            log_method_end(task_id, "execute_component_main");
            return;
        }
    };
 
    for (index, data) in data_list.iter().enumerate() {
        let (page, layer) = if index == 0 {
            (page1, layer1)
        } else {
            let (current_page, current_layer_index) = doc.add_page(page_width, page_height, format!("Layer {}", index + 1));
            (current_page, current_layer_index)
        };
 
        let current_layer = doc.get_page(page).get_layer(layer);
 
        if let Some(children) = data.get("children").and_then(|c| c.as_array()) {
            if children.is_empty() {
                info!("【{}】【execute_component_main】children 数组为空", task_id);
                continue;
            }
 
            for child in children {
                let component_name = match child.get("componentName").and_then(|n| n.as_str()) {
                    Some(name) => name,
                    None => {
                        error!("【{}】【execute_component_main】componentName 无效或缺失", task_id);
                        continue;
                    }
                };
 
                if let Some(props) = child.get("props") {
                    if let Some(cover) = props.get("cover") {
                        if let Some(url) = cover.get("url").and_then(|u| u.as_str()) {
                            let task_id_clone = task_id.to_string();
                            let url_clone = url.to_string();
 
                            // 生成下载任务,但不等待其��成
                            // tokio::spawn(async move {
                            //     process_image_component(&task_id_clone, &url_clone).await;
                            // });
                            process_image_component(&task_id_clone, &url_clone).await;
                        }
                    }
                }
 
                let json_str = match serde_json::to_string(child) {
                    Ok(s) => s,
                    Err(e) => {
                        error!("【{}】【execute_component_main】序列化组件失败: {}", task_id, e);
                        continue;
                    }
                };
 
                let draw_component_start_time = Instant::now();
 
                match component_name {
                    "OnixBarleyElectronicBarcode" => {
                        onixcomponents::OnixBarleyElectronicBarcode::draw(&current_layer, &json_str, Some(&doc), page_height, subset_font_path.clone(), gap_width);
                    }
                    "OnixBarleyElectronicBarcodeVertical" => {
                        onixcomponents::OnixBarleyElectronicBarcodeVertical::draw(&current_layer, &json_str, Some(&doc), page_height, subset_font_path.clone(), gap_height);
                    }
                    "OnixBarleyElectronicImage" => {
                        onixcomponents::OnixBarleyElectronicImage::draw(&current_layer, &json_str, Some(&doc), page_height, subset_font_path.clone());
                    }
                    "OnixBarleyElectronicIsv" => {
                        onixcomponents::OnixBarleyElectronicIsv::draw(&current_layer, &json_str, page_height);
                    }
                    "OnixBarleyElectronicLine" => {
                        onixcomponents::OnixBarleyElectronicLine::draw(&current_layer, &json_str, page_height);
                    }
                    "OnixBarleyElectronicLineVertical" => {
                        onixcomponents::OnixBarleyElectronicLineVertical::draw(&current_layer, &json_str, page_height);
                    }
                    "OnixBarleyElectronicQrcode" => {
                        onixcomponents::OnixBarleyElectronicQrcode::draw(&current_layer, &json_str, page_height);
                    }
                    "OnixBarleyElectronicRectangle" => {
                        onixcomponents::OnixBarleyElectronicRectangle::draw(&current_layer, &json_str, page_height);
                    }
                    "OnixBarleyElectronicText" => {
                        onixcomponents::OnixBarleyElectronicText::draw(&current_layer, &json_str, Some(&doc), page_height, subset_font_path.clone());
                    }
                    "OnixBarleyElectronicTextVertical" => {
                        onixcomponents::OnixBarleyElectronicTextVertical::draw(&current_layer, &json_str, Some(&doc), page_height, subset_font_path.clone());
                    }
                    _ => {
                        info!("【TaskID: {:?}, 线程ID: {:?}】【{}】【execute_component_main】未找到匹配的组件: {}", task_id, std::thread::current().id(), component_name, component_name);
                    }
                }
                info!("【TaskID: {:?}, 线程ID: {:?}】【{}】【execute_component_main】绘制组件耗时: {:?}", task_id, std::thread::current().id(), component_name, draw_component_start_time.elapsed());
                reset_layer_defaults(&current_layer);
            }
        }
    }
 
    info!("【TaskID: {:?}, 线程ID: {:?}】【execute_component_main】绘制组件总耗时: {:?}", task_id, std::thread::current().id(), execute_component_main_start_time.elapsed());
    log_method_end(task_id, "execute_component_main");
}

这个函数通过对不同组件的绘制进行抽象处理,类似于Rust中的特征(trait)。对于每个组件类型,你可以实现不同的行为。

3.2 泛型(Generics

fn identity<T>(value: T) -> T {    value}

identity 函数是一个泛型函数,它接受任何类型的参数并返回相同的值。

3.3迭代器(Iterators) 迭代器提供了一种处理序列数据的抽象方式。

let nums = vec![1, 2, 3, 4, 5];let even_nums: Vec<i32> = nums.into_iter().filter(|&x| x % 2 == 0).collect();

这段代码使用迭代器、闭包和 collect 方法来创建一个只包含偶数的 Vec。

3.4闭包(Closures) 在start_print_pdf等方法中使用了闭包来执行异步任务:

tokio::spawn(async move {
    // some code
});
 

闭包捕获了任务的所有权并在异步上下文中执行。

3.5 错误处理(Error Handling) 错误处理通过Result进行,如:

let result = match some_func() {
    Ok(val) => val,
    Err(e) => return Err(e.to_string()), // 错误信息封装
};

多次使用error!宏记录错误。

3.6并发编程(Concurrency) Rust 的标准库提供了多种并发编程的原语,如线程和通道。

let (print_tx, print_rx) = std_mpsc::channel::<PrintTask>();

这段代码创建了一个新的线程,并在其中打印了一条消息。join 方法等待线程完成。

3.7智能指针(Smart Pointers) 使用了Arc来实现线程安全的引用计数智能指针:

static ref PRINT_SENDER: OnceCell<Arc<std_Mutex<std_mpsc::Sender<PrintTask>>>> = OnceCell::new();
 

3.8模式匹配(Pattern Matching) 模式匹配是 Rust 中处理数据结构的一种强大工具,在处理Option和Result时使用了模式匹配:

match page_size_json.get("offsetx") {
    Some(offsetx) => ...,
    None => ...
}

这里使用 match 表达式来处理不同的消息类型。

3.9宏(Macros) 宏是 Rust 的一个强大特性,允许代码生成代码。

log!  info!
//等用于记录日志的宏是Rust中的宏机制。

3.10条件编译(Conditional Compilation) 条件编译允许根据不同的平台或配置编译不同的代码。

#[cfg(target_os = "windows")]fn is_windows() -> bool {    true}

这个属性宏仅在目标操作系统是 Windows 时启用 is_windows 函数。

3.11 裸指针(Raw Pointers) unsafe 和 winapi 的使用:

在 Windows 平台的 is_windows_7_or_newer 和 get_windows_version 函数中,代码通过 winapi 库调用 Windows 的底层 API 函数 RtlGetVersion 来获取操作系统版本。这部分代码涉及到 unsafe 操作,因为它需要直接操作和调用 Windows 的原生 API,这些操作通常会使用裸指针来访问内存中的数据结构。

unsafe 代码块的使用允许我们通过 winapi 库来调用低级别的系统函数,并直接操作一些内存结构(例如 OSVERSIONINFOW 结构体),这些操作通常会用到裸指针。

#[cfg(windows)]unsafe fn get_windows_version() -> OSVERSIONINFOW {
    let mut osvi: OSVERSIONINFOW = zeroed();  // 使用裸指针对结构体进行初始化   osvi.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOW>() as u32;
 
    // 获取 Windows API 函数指针并调用let ntdll = winapi::um::libloaderapi::GetModuleHandleA(b"ntdll.dll\0".as_ptr() as *const i8);
    let func_name = b"RtlGetVersion\0";
    let func = winapi::um::libloaderapi::GetProcAddress(ntdll, func_name.as_ptr() as *const i8);
 
    if func.is_null() {
        panic!("Failed to get RtlGetVersion function address");
    }
 
    let rtl_get_version: extern "system" fn(*mut OSVERSIONINFOW) -> i32 = 
        std::mem::transmute(func);
 
    let status = rtl_get_version(&mut osvi); // 这里使用裸指针if status == 0 {
        osvi  // 返回填充的 OSVERSIONINFOW 结构体   } else {
        panic!("Failed to get Windows version information.");
    }
}

这里,我们使用了 unsafe 和裸指针来进行 Windows API 的调用:

裸指针的使用:例如,*mut OSVERSIONINFOW 表示一个指向 OSVERSIONINFOW 结构体的可变裸指针。&mut osvi 就是将这个结构体的引用传递给 RtlGetVersion 函数,它是一个典型的裸指针使用场景。

裸指针(Raw pointers)通常用于绕过 Rust 的借用检查和所有权系统,允许开发者直接访问和操作内存。例如:

通过 *mut T 或 *const T 创建裸指针。

在 unsafe 代码块中使用裸指针,以访问底层的内存或调用外部的 C 函数。

在这段代码中,裸指针的使用集中在通过 winapi 调用 Windows 底层 API 的地方。我们通过裸指针访问和修改内存,但因为是 unsafe 代码块,所以 Rust 无法在编译时保证内存的安全性,因此需要开发者小心。

3.12 常量泛型(Const Generics)

struct Array<T, const N: usize> {    data: [T; N],}

这里定义了一个 Array 结构体,它的第二个类型参数是一个编译时常量,指定了数组的长度。

3.12 异步编程(Async Programming) Rust 的异步编程模型允许编写非阻塞的 I/O 密集型代码。

1. 异步任务调度和队列管理在 initialize_task_queue 函数中,使用了 tokio::spawn 启动多个异步工作线程来处理打印任务。task 通过 mpsc 通道传递给工作线程,工作线程会根据任务的顺序去执行绘制和打印操作。

// 启动绘制工作线程(多个并发)
for i in 0..num_draw_workers {
    tokio::spawn(async move {
        loop {
            let task_option = {
                let mut rx_guard = draw_rx.lock().await;
                rx_guard.recv().await
            };
            match task_option {
                Some(task) => {
                    // 任务处理逻辑
                    // 执行绘制任务
                    start_print_pdf_task(&task).await;
                }
                None => break, // 任务队列为空时退出
            }
        }
    });
}
 

分析

每个 tokio::spawn 启动一个异步任务来处理打印任务,并行处理多个任务。

这些任务使用异步通道 (mpsc::Receiver) 接收来自主线程或其他工作线程的任务数据,进行并行处理。

2. 使用 spawn_blocking 处理阻塞任务

在 start_print_pdf_task 中使用了 spawn_blocking 来处理可能会阻塞的操作,如文件 I/O 操作和PDF文档生成。spawn_blocking 用于在 Tokio 运行时的阻塞线程池中执行计算密集型或可能阻塞的操作。

// 使用 spawn_blocking 处理阻塞任务let blocking_result = spawn_blocking(move || {
    // 阻塞任务逻辑
});

分析

spawn_blocking 启动一个新的线程池来处理阻塞操作,确保不会阻塞 Tokio 运行时的其他任务。

这是一个优化手段,确保 I/O 操作或计算密集型任务不会影响到其他异步操作。

3. 异步 HTTP 请求和文件下载

在 download_and_save_image 函数中,使用 tokio::time::timeout 和 reqwest::Client 异步下载图片。

let response = match timeout(Duration::from_secs(200), CLIENT.get(url).send()).await {
    Ok(result) => match result {
        Ok(response) => response,
        Err(e) => {
            error!("请求失败: {}", e);
            return Err(e.into());
        }
    },
    Err(_) => {
        error!("请求超时");
        return Err("请求超时".into());
    }
};

分析

使用 tokio::time::timeout 控制请求超时,避免请求阻塞过久。

reqwest::Client 异步发送 HTTP 请求,并使用 .await 等待响应。

该方法确保 HTTP 请求和图片下载过程不会阻塞主线程,可以同时进行多个请求。

4. 异步打印任务处理

在 start_print_pdf_task 中,打印任务的处理依赖于多个异步步骤,包括 PDF 文档创建、字体处理和任务执行。

// 执行异步任务let print_result = print_pdf(
    id.to_string(),
    task.printer.clone(),
    task.task_id.clone(),
    task.pdf_path.clone(), // 使用绘制完成后的 PDF 路径   print_setting_str.clone(),
    remove_after_print,
    page_size.to_string(),
    auto_fit,
).await;

分析

使用 .await 处理异步打印任务,确保任务按顺序执行,不会阻塞其他操作。

异步调用的方式使得多个打印任务可以并行执行,而不会阻塞主线程或导致性能瓶颈。

3.13 内存分配(Memory Allocation)

/// 清除字体缓存
pub fn clear_font_cache() {
    THREAD_FONTS.with(|cache| {
        cache.borrow_mut().clear();
    });
    info!("【线程-{:?}】字体缓存已清除", std::thread::current().id());
}

尽管没有显式展示内存分配(如 Box 或 Vec 等结构体的使用),但内存的管理在于并发任务、信号量和队列的管理上