引言

你是否遇到过 Rust 并发场景下的资源竞争、性能瓶颈?

当多个线程同时抓取网页导致 IP 被封、多线程读写本地数据引发一致性问题时,如何优雅地实现线程安全?

本文结合开源项目 Saga Reader 的真实开发场景,深度解析 Arc/Mutex/RwLock 的实战技巧,带你从 “踩坑” 到 “优化”,掌握 Rust 并发编程的核心方法论,文末附项目地址,欢迎 star 交流!

关于开源项目Saga Reader(中文麒睿智库),之前我在博客园中有详细介绍,新朋友可以先阅读这篇文章

技术背景

在 Rust 编程的世界里,并发编程是一个既强大又充满挑战的领域。为了实现高效、安全的并发操作,Rust 提供了一系列实用的工具,其中 Arc(原子引用计数指针)和 Mutex(互斥锁)、RwLock(读写锁)是非常关键的组件。本文将结合 Saga Reader 项目中的实际应用案例,深入探讨 Arc、Mutex、RwLock 的使用场景、技术要点,并结合我们的 Saga Reader 项目中的实际案例,分享它们在并发场景下的使用技巧和设计哲学。

什么是Saga Reader

基于Tauri开发的开源AI驱动的智库式阅读器(前端部分使用Web框架),能根据用户指定的主题和偏好关键词自动从互联网上检索信息。它使用云端或本地大型模型进行总结和提供指导,并包括一个AI驱动的互动阅读伴读功能,你可以与AI讨论和交换阅读内容的想法。

这个项目我5月刚放到Github上(Github - Saga Reader),欢迎大家关注分享。‍码农‍开源不易,各位好人路过请给个小星星Star

核心技术栈:Rust + Tauri(跨平台)+ Svelte(前端)+ LLM(大语言模型集成),支持本地 / 云端双模式

关键词:端智能,边缘大模型;Tauri 2.0;桌面端安装包 < 5MB,内存占用 < 20MB。

运行截图

项目核心模块

问题初现:无 Arc 与 Mutex 的困境

因为是本地桌面端,涉及到本地数据的并发读写以及数据抓取的并发限流控制。以网页内容抓取模块为例,多个线程同时进行网页抓取操作,代码如下:

// 早期网页抓取示例
use reqwest; async fn scrap_text_by_url(url: &str) -> anyhow::Result<String> {
let response = reqwest::get(url).await?;
let text = response.text().await?;
// 处理网页内容
Ok(text)
}

由于没有任何同步机制,多个线程可能会同时访问同一个网页资源,服务器可能会将这些请求视为恶意攻击,从而对 IP 进行封禁。同时,多个线程同时处理抓取到的内容,可能会导致数据处理混乱,影响最终结果的准确性。

再比如对本地数据的读取,无并发控制会引起数据不一致问题。

引入 Arc 与 Mutex:柳暗花明

Arc

Arc 是 Rust 标准库中的一个智能指针,全称为 Atomic Reference Counting。在多线程环境中,多个线程可能需要同时访问同一个资源,Arc 可以让多个线程安全地共享同一个数据实例。它通过原子操作来管理引用计数,当引用计数降为 0 时,数据会被自动释放,从而避免了数据竞争和内存泄漏的问题。

Mutex

Mutex 即互斥锁,是一种用于实现线程同步的机制。在多线程编程中,多个线程可能会同时访问和修改共享资源,这可能会导致数据不一致或其他竞态条件。Mutex 可以确保在同一时间只有一个线程能够访问被保护的资源,从而保证数据的一致性和线程安全。

Saga Reader 中的 Mutex 实战

源码:scrap/src/simulator.rs

在 Saga Reader 项目中,我们有一个模拟浏览器行为来抓取网页内容的功能,位于 。由于创建和管理模拟的 Webview 窗口是资源密集型操作,并且可能涉及到一些全局状态或限制(例如,不能同时打开多个同名的模拟窗口),我们需要确保这部分操作的串行化执行。

为什么选择 Mutex 而非其他锁?

> 模拟 Webview 窗口创建是资源密集型操作,且需保证同一时刻仅允许一个实例运行(避免内存泄漏和窗口句柄冲突)。此时写操作(创建窗口)是核心操作,读操作极少,因此选择 Mutex 保证独占性,而非引入 RwLock 的复杂度。

// ... existing code ...
use tokio::sync::{oneshot, Mutex}; // 引入 Tokio 的异步 Mutex
// ... existing code ... // 使用 once_cell 的 Lazy 来延迟初始化一个全局的、带 Arc 的 Mutex
// Arc<Mutex<()>> 中的 () 表示我们用这个 Mutex 保护的不是具体数据,
// 而是保护一段代码逻辑的独占执行权。
static MUTEX: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(()))); pub async fn scrap_text_by_url<R: Runtime>(
app_handle: AppHandle<R>,
url: &str,
) -> anyhow::Result<String> {
// 在关键代码段开始前,异步获取锁
// _lock 是一个 RAII 守护(guard),当它离开作用域时,锁会自动释放
let _lock = MUTEX.lock().await;
match app_handle.get_webview_window(WINDOW_SCRAP_HOST) {
Some(_) => {
error!("The scrap host for simulator was busy to use, scrap pages at the same time was not support currently!");
Err(anyhow::anyhow!("Scrap host is busy"))
}
None => {
// ... 创建和操作 Webview 窗口的代码 ...
// 这部分代码在持有锁的期间执行,保证了同一时间只有一个任务能执行到这里
let window = WebviewWindowBuilder::new(
// ... existing code ...
Ok(result)
}
}
// _lock 在这里离开作用域,Mutex 自动释放
}

在这个例子中,static MUTEX: Lazy<Arc<Mutex<()>>> 定义了一个全局静态的互斥锁。Arc 使得这个 Mutex 可以在多个异步任务之间安全共享。Lazy 确保 Mutex 只在第一次被访问时初始化。Mutex<()> 表示这个锁并不直接保护某个具体的数据,而是用来控制对一段代码逻辑(即创建和使用 WINDOW_SCRAP_HOST 窗口的过程)的独占访问。通过 MUTEX.lock().await,任何尝试执行 scrap_text_by_url 的任务都必须先获得这个锁,从而保证了模拟器资源的串行使用,避免了潜在的冲突和错误。

读多写少场景的性能利器:RwLock

虽然 Mutex 提供了强大的数据保护能力,但它的独占性在某些场景下可能会成为性能瓶颈。想象一个场景:我们有一个共享的配置对象,它很少被修改(写操作),但会被非常频繁地读取(读操作)。如果使用 Mutex,即使是多个读操作也不得不排队等待,这显然不是最优的。

RwLock<T> (Read-Write Lock) 正是为了解决这类“读多写少”的场景而设计的。它允许多个读取者同时访问共享数据,或者一个写入者独占访问共享数据。规则如下:

  • 共享读:可以有任意数量的读取者同时持有读锁。
  • 独占写:当有写入者持有写锁时,其他所有读取者和写入者都必须等待。
  • 读写互斥:当有任何读取者持有读锁时,写入者必须等待;反之亦然。

RwLock 的核心特性:

  • 提高读并发:在读取操作远多于写入操作时,RwLock 能显著提高并发性能。
  • 写操作依然独占:保证了数据修改时的安全性。

Saga Reader 中的 RwLock 实战

源码:feed_api_rs/src/features/impl_default.rs

在 Saga Reader 的核心功能模块 中,FeaturesAPIImpl 结构体持有一个 ApplicationContext,这个上下文中包含了用户配置 (UserConfig) 和应用配置 (AppConfig) 等共享状态。这些配置信息会被多个 API 调用读取,而修改配置的操作相对较少。

// ... existing code ...
use tokio::sync::RwLock; // 引入 Tokio 的异步 RwLock
// ... existing code ... pub struct FeaturesAPIImpl {
// ApplicationContext 被 Arc 和 RwLock 包裹,以便在异步任务间安全共享和并发访问
context: Arc<RwLock<ApplicationContext>>,
scrap_provider: ScrapProviderEnums,
article_recorder_service: ArticleRecorderService,
} impl FeaturesAPIImpl {
pub async fn new(ctx: ApplicationContext) -> anyhow::Result<Self> {
// ... 初始化代码 ...
let context = Arc::new(RwLock::new(ctx)); // 创建 RwLock 实例
// ...
Ok(FeaturesAPIImpl {
context,
scrap_provider,
article_recorder_service,
})
} // 示例:读取配置 (读操作)
async fn update_feed_contents<R: Runtime>(
&self,
package_id: &str,
feed_id: &str,
app_handle: Option<AppHandle<R>>,
) -> anyhow::Result<()> {
let user_config;
let llm_section;
{
// 获取读锁,允许多个任务同时读取 context
let context_guarded = self.context.read().await;
user_config = context_guarded.user_config.clone();
llm_section = context_guarded.app_config.llm.clone();
} // 读锁在此处释放
// ... 后续逻辑使用 user_config 和 llm_section ...
Ok(())
} // 示例:修改用户配置 (写操作)
async fn add_feeds_package(&self, feeds_package: FeedsPackage) -> anyhow::Result<()> {
// 获取写锁,独占访问 context
let context_guarded = &mut self.context.write().await;
let user_config = &mut context_guarded.user_config;
if user_config.add_feeds_packages(feeds_package) {
return self.sync_user_profile(user_config).await;
}
// ...
Err(anyhow::Error::msg(
"add_feeds_package failure, may be the feeds package already existed",
))
// 写锁在此处释放
}
}

context: Arc<RwLock<ApplicationContext>> 使得 ApplicationContext 可以在多个异步的 API 请求处理任务之间安全地共享。当一个任务需要读取配置,它会调用 self.context.read().await 来获取一个读锁。多个任务可以同时持有读锁并访问 ApplicationContext。当一个任务需要修改配置,它会调用 self.context.write().await 来获取一个写锁。此时,其他任何尝试获取读锁或写锁的任务都会被阻塞,直到写锁被释放。这种机制极大地提高了读取密集型操作的并发性能,同时保证了写操作的原子性和数据一致性。

关于 tauri::StateArc

我们经常看到 Tauri 命令的参数形如 state: State<'_, Arc<HybridRuntimeState>>。这里的 Arc<HybridRuntimeState> 表明 HybridRuntimeState 是一个被多所有权共享的状态对象。Tauri 的 State 管理器本身会确保以线程安全的方式将这个状态注入到命令处理函数中。如果 HybridRuntimeState 内部的数据需要细粒度的并发控制,那么它内部可能就会使用 MutexRwLock。例如,我们的 FeaturesAPIImpl 实例(它内部使用了 RwLock)就是通过 HybridRuntimeState 共享给各个 Tauri 命令的。

// ... existing code ...
// 在插件初始化时,创建 FeaturesAPIImpl 实例并放入 Arc 中
// 然后通过 app_handle.manage() 交给 Tauri 的状态管理器
.setup(|app_handle, _plugin| {
let features_api = tauri::async_runtime::block_on(async {
let context_host = Startup::launch().await.unwrap();
let context = context_host.copy_context();
FeaturesAPIImpl::new(context).await.expect("tauri-plugin-feed-api setup the features instance failure")
}); app_handle.manage(Arc::new(HybridRuntimeState { features_api })); // features_api 内部有 RwLock
Ok(())
})
// ... existing code ...
// ... existing code ...
// Tauri 命令通过 State 获取共享的 HybridRuntimeState
#[tauri::command(rename_all = "snake_case")]
pub(crate) async fn get_feeds_packages(
state: State<'_, Arc<HybridRuntimeState>>,
) -> Result<Vec<FeedsPackage>, ()> {
// features_api 内部的 RwLock 会在这里发挥作用
let features_api = &state.features_api;
Ok(features_api.get_feeds_packages().await)
}
// ... existing code ...

Mutex vs. RwLock:如何选择?

特性 Mutex RwLock
基本原理 独占访问 共享读,独占写
适用场景 写操作频繁,或读写操作均衡,或逻辑简单 读操作远多于写操作,且读操作耗时较长
锁的粒度 通常较粗,保护整个数据结构或代码块 可以更细粒度,但通常也保护整个数据结构
性能(读多) 可能成为瓶颈 显著优于 Mutex
性能(写多) 与 RwLock 类似,或略优(因逻辑更简单) 可能不如 Mutex(因内部状态管理更复杂)
死锁风险 存在(如ABBA死锁) 存在,且可能更复杂(如写锁饥饿读锁)

选择建议:

  • 优先简单:如果不确定,或者共享数据的访问模式不清晰,可以从 Mutex 开始,因为它的语义更简单,更不容易出错。
  • 分析瓶颈:如果性能分析表明某个 Mutex 成为了瓶颈,并且该场景符合“读多写少”的特点,那么可以考虑替换为 RwLock
  • 警惕写锁饥饿RwLock 的一个潜在问题是写锁饥饿。如果读请求非常频繁,写操作可能长时间无法获得锁。一些 RwLock 的实现可能提供公平性策略来缓解这个问题,但仍需注意。
  • 锁的持有时间:无论使用 Mutex 还是 RwLock,都应尽可能缩短锁的持有时间,以减少线程阻塞和提高并发度。将耗时操作移出临界区(持有锁的代码段)。

总结与展望

MutexRwLock 是 Rust 并发编程中不可或缺的同步原语。它们以不同的策略平衡了数据安全和并发性能的需求。在 Saga Reader 项目中,我们根据具体的业务场景和数据访问模式,恰当地选择了 Mutex 来保证资源操作的串行化,以及 RwLock 来优化共享配置的并发读取性能。

理解并熟练运用这些并发工具,是构建高效、健壮的 Rust 应用的基石。随着项目的发展,我们也将持续关注并发性能,并在必要时对锁的使用策略进行调优,以确保 Saga Reader 能够为用户带来流畅、稳定的阅读体验。

参与开源,一起构建高效阅读器!

Saga Reader 是一个完全开源的跨平台项目,目前正在快速迭代中,急需以下方向的贡献者:

  • Rust 开发:优化并发逻辑、扩展本地大模型支持;
  • 前端开发:基于 Svelte 优化用户交互;
  • AI 算法:改进文本总结与伴读功能的语义理解;

如何参与?

  1. ‍码农‍开源不易,各位好人路过请给个小星星Star。 → GitHub - Saga Reader
  2. 加入 Issues 讨论:提出功能建议或参与 Bug 修复;
  3. 提交 PR:我们会提供详细的开发文档与技术支持!

福利:活跃贡献者可获得项目代码署名!

【实战】深入浅出 Rust 并发:RwLock 与 Mutex 在 Tauri 项目中的实践的更多相关文章

  1. 2017.6.30 用shiro实现并发登录人数控制(实际项目中的实现)

    之前的学习总结:http://www.cnblogs.com/lyh421/p/6698871.html 1.kickout功能描述 如果将配置文件中的kickout设置为true,则在另处再次登录时 ...

  2. 【实战Java高并发程序设计 7】让线程之间互相帮助--SynchronousQueue的实现

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  3. 【实战Java高并发程序设计6】挑战无锁算法:无锁的Vector实现

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  4. 【实战Java高并发程序设计 5】让普通变量也享受原子操作

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  5. 【实战Java高并发程序设计 4】数组也能无锁:AtomicIntegerArray

    除了提供基本数据类型外,JDK还为我们准备了数组等复合结构.当前可用的原子数组有:AtomicIntegerArray.AtomicLongArray和AtomicReferenceArray,分别表 ...

  6. 【实战Java高并发程序设计 3】带有时间戳的对象引用:AtomicStampedReference

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference AtomicReference无法解决上述问题的根 ...

  7. 【实战Java高并发程序设计 1】Java中的指针:Unsafe类

    是<实战Java高并发程序设计>第4章的几点. 如果你对技术有着不折不挠的追求,应该还会特别在意incrementAndGet() 方法中compareAndSet()的实现.现在,就让我 ...

  8. 《实战java高并发程序设计》源码整理及读书笔记

    日常啰嗦 不要被标题吓到,虽然书籍是<实战java高并发程序设计>,但是这篇文章不会讲高并发.线程安全.锁啊这些比较恼人的知识点,甚至都不会谈相关的技术,只是写一写本人的一点读书感受,顺便 ...

  9. 《实战Java高并发程序设计》读书笔记

    文章目录 第二章 Java并行程序基础 2.1 线程的基本操作 2.1.1 线程中断 2.1.2 等待(wait)和通知(notify) 2.1.3 等待线程结束(join)和谦让(yield) 2. ...

  10. C++11并发——多线程std::mutex (二)

    https://www.cnblogs.com/haippy/p/3237213.html Mutex 又称互斥量,C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在 <mute ...

随机推荐

  1. 使用Bioaider进行本地blast

    系统环境为windows11 1. 下载blast程序 https://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/LATEST/ 双击安装,记住自己的 ...

  2. Flink学习(十二) Sink到JDBC(可扩展到任何关系型数据库)

    导入依赖 <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java& ...

  3. 2024.11.19随笔&联考总结

    联考 看到 T1 就知道一定是简单计数题然后发现 \(O(n)\) 可以过于是就大概写了写式子就开写.写的过程中犯了一些低级错误,代码重构了一次才过.耽误的时间比较久.然后开 T2,一眼有一个 \(O ...

  4. 【由技及道】量子跃迁部署术:docker+jenkins+Harbor+SSH的十一维交付矩阵【人工智障AI2077的开发日志011】

    摘要: SSH密钥对构建的十一维安全通道 × Harbor镜像星门 × 错误吞噬者语法糖 = 在CI/CD的量子观测中实现熵减永动机,使容器在部署前保持开发与生产维度的叠加态 量子纠缠现状(技术背景) ...

  5. js里一些实在想不通的问题合集

    The global NaN property is a value representing Not-A-Number. --MDN NaN 是用来表示一个非数字的值得全局属性, 但是typeof之 ...

  6. mysql 2003远程访问失败 mysql8配置远程访问

    # mysql -uroot -p #进入数据库 > use mysql;#进入数据库 > select host, user, authentication_string, plugin ...

  7. IvorySQL v4 逻辑复制槽同步功能解析:高可用场景下的数据连续性保障

    功能简介 IvorySQL v4 基于 PostgreSQL 17,引入了逻辑复制槽同步至热备份数据库的功能.这一改进有效解决了旧版本中主数据库与备份数据库切换后逻辑复制中断的问题.对于那些追求数据高 ...

  8. 【Linux】5.6 Shell打印输出指令

    Shell打印输出命令 1. echo命令 Shell 的 echo 指令与 PHP 的 echo 指令类似,都是用于字符串的输出.命令格式:echo string 您可以使用echo实现更复杂的输出 ...

  9. AI可解释性 II | Saliency Maps-based 归因方法(Attribution)论文导读(持续更新)

    AI可解释性 II | Saliency Maps-based 归因方法(Attribution)论文导读(持续更新) 导言 本文作为AI可解释性系列的第二部分,旨在以汉语整理并阅读归因方法(Attr ...

  10. Postman+Newman生成接口测试报告

    1.安装node 安装完后进入cmd输入node检验版本 2.安装newman 打开cmd-->输入npm install -g newman,然后输入newman -v验证版本 3.安装htm ...