背景
我完全是 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
宏的反馈(因为我不确定是否做得正确)。问题:
理解克隆
然而,我的主要问题是关于这段代码的,尤其是在以下代码中(
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);
}
})
}
虽然这样做可以,但我想确保这是正确的方法。根据我的理解,它:
- 获取配置Arc结构的写锁。
- 检查更改并在更改时:
- 调用写入器的
Arc::clone()
方法(这将自动将参数从Arc转换为DeRefMut后再克隆)。这实际上并不会“克隆”结构体,而是增加了引用计数器(因此应该很快)? - 由于步骤3被(*...)包装,因此调用
Config::clone
- 这是正确的方法吗?我的理解是,它现在会克隆Config
,产生一个可变的拥有实例,我可以在其中进行修改。 - 更改新配置设置新的
debug_mode
。 - 从此拥有的
Config
创建一个新的Arc<Config>
。 - 更新静态CURRENT_CONFIG。
- 释放旧的
Arc<Config>
的引用计数器(如果当前没有其他使用它的内容,则可能会释放内存)。 - 释放写锁。
了解性能影响
同样,这段代码:
LOCALES.lookup(&Config::current().language, $name)
正常使用时应该很快,因为它使用了这个函数:
pub fn current() -> Arc<Config> {
CURRENT_CONFIG.with(|c| c.read().unwrap().clone())
}
获取一个引用计数指针指向当前配置,而不实际复制它(`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();
}
产生(证明配置不会在线程之间共享):
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 }`
thread_local
似乎修复了我的测试,包括确保Config
状态在线程之间共享并且可以安全更新。完整的代码如下(尽管使用了最新的来自夜间构建的SyncLazy
): - undefined(*Arc::clone(&writer)).clone()
看起来是对Arc
的不必要克隆 -writer.as_ref().clone()
可以在没有内部克隆的情况下实现相同的目的。虽然克隆Arc
相对于复制分配的类型来说是廉价的,但它并不是免费的,因为它涉及到在操作原子计数器时的内存屏障。(当创建临时克隆的Arc
时,计数器会更新一次,当销毁时又会更新一次 - 这些无法被优化掉,因为它们可能对其他线程可见,所以编译器必须生成这两个调整。) - undefinedArc::_as_ref()
方法是否正确地增加了引用计数? - undefinedas_ref()
不会增加引用计数。它提供了一个&T
,但是这个引用不能超过分发它的Arc
的生命周期。你可以使用这个&T
,在这种情况下调用T::clone()
而不会影响Arc
的引用计数。而且,这个引用不能超过Arc
的生命周期,保证了在你使用引用时对象不会被销毁。 - undefined