了解 Rust 中的线程安全 RwLock<Arc<T>> 机制



背景

我是Rust(昨天开始)的新手,我正在努力确保我正确理解。我希望为"游戏"编写一个配置系统,并希望它能够快速访问,但偶尔会发生变化。首先,我想研究本地化,这似乎是静态配置的一个合理用例(因为我理解这些东西通常不是"Rusty")。我提出了以下(工作)代码,部分基于这篇博客文章(通过这个问题找到)。我在这里提供了参考,但现在可以跳过它。。。

#[macro_export]
macro_rules! localize {
(@single $($x:tt)*) => (());
(@count $($rest:expr),*) => (<[()]>::len(&[$(localize!(@single $rest)),*]));
($name:expr $(,)?) => { LOCALES.lookup(&Config::current().language, $name) };
($name:expr, $($key:expr => $value:expr,)+) => { localize!(&Config::current().language, $name, $($key => $value),+) };
($name:expr, $($key:expr => $value:expr),*) => ( localize!(&Config::current().language, $name, $($key => $value),+) );
($lang:expr, $name:expr $(,)?) => { LOCALES.lookup($lang, $name) };
($lang:expr, $name:expr, $($key:expr => $value:expr,)+) => { localize!($lang, $name, $($key => $value),+) };
($lang:expr, $name:expr, $($key:expr => $value:expr),*) => ({
let _cap = localize!(@count $($key),*);
let mut _map : ::std::collections::HashMap<String, _>  = ::std::collections::HashMap::with_capacity(_cap);
$(
let _ = _map.insert($key.into(), $value.into());
)*
LOCALES.lookup_with_args($lang, $name, &_map)
});
}
use fluent_templates::{static_loader, Loader};
use std::sync::{Arc, RwLock};
use unic_langid::{langid, LanguageIdentifier};
static_loader! {
static LOCALES = {
locales: "./resources",
fallback_language: "en-US",
core_locales: "./resources/core.ftl",
// Removes unicode isolating marks around arguments, you typically
// should only set to false when testing.
customise: |bundle| bundle.set_use_isolating(false)
};
}
#[derive(Debug, Clone)]
struct Config {
#[allow(dead_code)]
debug_mode: bool,
language: LanguageIdentifier,
}
#[allow(dead_code)]
impl Config {
pub fn current() -> Arc<Config> {
CURRENT_CONFIG.with(|c| c.read().unwrap().clone())
}
pub fn make_current(self) {
CURRENT_CONFIG.with(|c| *c.write().unwrap() = Arc::new(self))
}
pub fn set_debug(debug_mode: bool) {
CURRENT_CONFIG.with(|c| {
let mut writer = c.write().unwrap();
if writer.debug_mode != debug_mode {
let mut config = (*Arc::clone(&writer)).clone();
config.debug_mode = debug_mode;
*writer = Arc::new(config);
}
})
}
pub fn set_language(language: &str) {
CURRENT_CONFIG.with(|c| {
let l: LanguageIdentifier = language.parse().expect("Could not set language.");
let mut writer = c.write().unwrap();
if writer.language != l {
let mut config = (*Arc::clone(&writer)).clone();
config.language = l;
*writer = Arc::new(config);
}
})
}
}
impl Default for Config {
fn default() -> Self {
Config {
debug_mode: false,
language: langid!("en-US"),
}
}
}
thread_local! {
static CURRENT_CONFIG: RwLock<Arc<Config>> = RwLock::new(Default::default());
}
fn main() {
Config::set_language("en-GB");
println!("{}", localize!("apologize"));
}

为了简洁起见,我没有包括这些测试。我也欢迎对localize宏的反馈(因为我不确定我是否做对了)

问题

理解Arc克隆

然而,我的主要问题是这段代码(set_language中也有类似的例子):

pub fn set_debug(debug_mode: bool) {
CURRENT_CONFIG.with(|c| {
let mut writer = c.write().unwrap();
if writer.debug_mode != debug_mode {
let mut config = (*Arc::clone(&writer)).clone();
config.debug_mode = debug_mode;
*writer = Arc::new(config);
}
})
}

尽管这是有效的,但我想确保这是正确的方法。据我所知,它是

  1. 获取配置Arc结构的写锁
  2. 检查更改,如果更改:
  3. 在写入程序上调用Arc::clone()(这将在克隆之前自动将参数DeRefMut写入Arc)。这实际上并没有"克隆"结构,而是增加引用计数器(所以应该很快)
  4. 由于步骤3包含在(*…)中,因此调用Config::clone-这是正确的方法吗?我的理解是,现在确实克隆了Config,生成了一个可变的拥有实例,然后我可以对其进行修改
  5. 更改新配置,设置新的debug_mode
  6. 从此拥有的Config创建一个新的Arc<Config>
  7. 更新静态CURRENT_CONFIG
  8. 释放对旧Arc<Config>的引用计数器(如果当前没有其他人使用它,则可能会释放内存)
  9. 释放写锁定

如果我正确理解这一点,那么在步骤4中只会发生一次内存分配。是这样吗?第4步是正确的方法吗?

了解性能影响

类似地,这个代码:

LOCALES.lookup(&Config::current().language, $name)

在正常使用下应该是快速的,因为它使用这个功能:

pub fn current() -> Arc<Config> {
CURRENT_CONFIG.with(|c| c.read().unwrap().clone())
}

它获取一个指向当前配置的ref计数指针,而不实际复制它(clone()应该如上所述调用Arc::clone()),使用读锁(除非发生写操作,否则速度很快)。

理解thread_local!宏的使用

如果这一切都很好,那就太好了!然而,我被这最后一段代码卡住了:

thread_local! {
static CURRENT_CONFIG: RwLock<Arc<Config>> = RwLock::new(Default::default());
}

这肯定是错误的吗?为什么我们要将CURRENT_CONFIG创建为thread_local。我的理解(无可否认,来自其他语言,再加上有限的文档)意味着当前执行的线程将有一个唯一的版本,这毫无意义,因为线程不能中断自己?通常情况下,我希望在多个线程之间共享一个真正静态的RwLock?我是误解了什么,还是这是原始博客文章中的错误?

事实上,以下测试似乎证实了我的怀疑:

#[test]
fn config_thread() {
Config::set_language("en-GB");
assert_eq!(langid!("en-GB"), Config::current().language);
let tid = thread::current().id();
let new_thread =thread::spawn(move || {
assert_ne!(tid, thread::current().id());
assert_eq!(langid!("en-GB"), Config::current().language);
});
new_thread.join().unwrap();
}

Produces(演示配置不是跨线程共享的):

thread '<unnamed>' panicked at 'assertion failed: `(left == right)`
left: `LanguageIdentifier { language: Language(Some("en")), script: None, region: Some(Region("GB")), variants: None }`,
right: `LanguageIdentifier { language: Language(Some("en")), script: None, region: Some(Region("US")), variants: None }`

在我看来,你所指的博客文章的部分不是很好。

这里的RwLock是伪造的,这是正确的——它可以用RefCell替换,因为它是线程本地的。

博客文章中这种方法的理由是站不住脚的:

然而,在前面的示例中,我们引入了内部可变性。假设我们有多个线程在运行,所有线程都引用相同的配置,但其中一个线程翻转了一个标志。如果并发运行的代码现在不希望标志随机翻转,会发生什么情况?

RwLock的全部意义在于,当对象被锁定以进行读取时(即,从RwLock::read()返回的RwLockReadGuard是活动的),不能进行修改。所以CCD_ 22不会有你的标志";"随机翻转";而读取锁定被取出。(当然,如果你释放锁并再次使用,并假设标志在此期间没有改变,这可能是一个问题。)

本节也没有具体说明配置的实际更新方式。您需要一种机制来向其他线程发出配置更改发生的信号(例如通道),并且线程本身必须使用新配置更新自己的线程本地变量。

最终,我只会认为这一部分是糟糕的建议,当然不是为初学者量身定制的。

最新更新