背景
I am 全新的Rust(昨天开始),我试图确保我理解正确。我正在寻找为“游戏”编写一个配置系统,并希望它能够快速访问但偶尔可变。首先,我想研究本地化,这似乎是静态配置的合理用例(因为我意识到这些东西通常不会“生锈”)。我想出了以下(工作)代码,部分基于这篇博文 https://blog.sentry.io/2018/04/05/you-cant-rust-that(通过发现这个问题 https://stackoverflow.com/a/65200809)。我已将其包含在此处供参考,但现在请随意跳过它......
#[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
宏也是如此(因为我不确定我是否做对了)。
Question
理解Arc
cloning
然而,我的主要问题是特别是在这段代码上(有一个类似的例子set_language
too):
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 结构上的写锁。
- 检查更改,如果更改:
- Calls
Arc::clone()
在作者身上(这会自动DeRefMut
克隆之前 Arc 的参数)。这实际上并没有“克隆”结构,而是增加了引用计数器(所以应该很快)?
- Call
Config::clone
由于步骤 3 包含在 (*...) 中 - 这是正确的方法吗?我的理解是这样的does现在克隆Config
,生成一个可变的拥有实例,然后我可以对其进行修改。
- 改变新的配置设置新的
debug_mode
.
- 创建一个新的
Arc<Config>
从这个拥有的Config
.
- 更新静态 CURRENT_CONFIG。
- 将参考计数器释放到旧的
Arc<Config>
(如果当前没有其他东西使用内存,则可能会释放内存)。
- 释放写锁。
如果我理解正确的话,那么第 4 步中只会发生一次内存分配。对吗?第 4 步是解决此问题的正确方法吗?
了解性能影响
同样,这段代码:
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
。我的理解(诚然来自其他语言,结合有限的文档 https://doc.rust-lang.org/std/macro.thread_local.html)意味着当前正在执行的线程将有一个唯一的版本,这是没有意义的,因为线程不能中断自身?通常我会期望一个真正静态的RwLock
跨多线程共享?我是否误解了什么或者这是一个错误原始博客文章 https://blog.sentry.io/2018/04/05/you-cant-rust-that?
事实上,下面的测试似乎证实了我的怀疑:
#[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 }`