所有权系统(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(¤t_layer, &json_str, Some(&doc), page_height, subset_font_path.clone(), gap_width);
}
"OnixBarleyElectronicBarcodeVertical" => {
onixcomponents::OnixBarleyElectronicBarcodeVertical::draw(¤t_layer, &json_str, Some(&doc), page_height, subset_font_path.clone(), gap_height);
}
"OnixBarleyElectronicImage" => {
onixcomponents::OnixBarleyElectronicImage::draw(¤t_layer, &json_str, Some(&doc), page_height, subset_font_path.clone());
}
"OnixBarleyElectronicIsv" => {
onixcomponents::OnixBarleyElectronicIsv::draw(¤t_layer, &json_str, page_height);
}
"OnixBarleyElectronicLine" => {
onixcomponents::OnixBarleyElectronicLine::draw(¤t_layer, &json_str, page_height);
}
"OnixBarleyElectronicLineVertical" => {
onixcomponents::OnixBarleyElectronicLineVertical::draw(¤t_layer, &json_str, page_height);
}
"OnixBarleyElectronicQrcode" => {
onixcomponents::OnixBarleyElectronicQrcode::draw(¤t_layer, &json_str, page_height);
}
"OnixBarleyElectronicRectangle" => {
onixcomponents::OnixBarleyElectronicRectangle::draw(¤t_layer, &json_str, page_height);
}
"OnixBarleyElectronicText" => {
onixcomponents::OnixBarleyElectronicText::draw(¤t_layer, &json_str, Some(&doc), page_height, subset_font_path.clone());
}
"OnixBarleyElectronicTextVertical" => {
onixcomponents::OnixBarleyElectronicTextVertical::draw(¤t_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(¤t_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 等结构体的使用),但内存的管理在于并发任务、信号量和队列的管理上