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

3

背景

我完全是 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);
            }
        })
    }

虽然这样做可以,但我想确保这是正确的方法。根据我的理解,它:
  1. 获取配置Arc结构的写锁。
  2. 检查更改并在更改时:
  3. 调用写入器的 Arc::clone() 方法(这将自动将参数从Arc转换为DeRefMut后再克隆)。这实际上并不会“克隆”结构体,而是增加了引用计数器(因此应该很快)?
  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())
    }

获取一个引用计数指针指向当前配置,而不实际复制它(`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
完整的代码在此处可获取(https://gist.github.com/thargy/f64e26c3c3321be10de1705ea3f47ef2)。请返回仅翻译的文本。 - undefined
1
(*Arc::clone(&writer)).clone() 看起来是对 Arc 的不必要克隆 - writer.as_ref().clone() 可以在没有内部克隆的情况下实现相同的目的。虽然克隆 Arc 相对于复制分配的类型来说是廉价的,但它并不是免费的,因为它涉及到在操作原子计数器时的内存屏障。(当创建临时克隆的 Arc 时,计数器会更新一次,当销毁时又会更新一次 - 这些无法被优化掉,因为它们可能对其他线程可见,所以编译器必须生成这两个调整。) - undefined
谢谢 @user4815162342,Arc::_as_ref() 方法是否正确地增加了引用计数? - undefined
1
as_ref()不会增加引用计数。它提供了一个&T,但是这个引用不能超过分发它的Arc的生命周期。你可以使用这个&T,在这种情况下调用T::clone()而不会影响Arc的引用计数。而且,这个引用不能超过Arc的生命周期,保证了在你使用引用时对象不会被销毁。 - undefined
1个回答

3
你提到的博客文章部分,在我看来并不是很好。
你说的这个 RwLock 是虚假的,它可以被一个线程本地的 RefCell 替换。
对于博客文章中所采用方法的理由不足:
“然而,在先前的示例中,我们引入了内部可变性。想象一下,我们有多个运行的线程,都引用同一个配置,但其中一个翻转了一个标志。现在,正在并发运行的代码会发生什么情况,因为该标志随机翻转了呢?”
整个使用 RwLock 的意义在于,在读取时(即从 RwLock::read() 返回的 RwLockReadGuard 存在期间),不能进行修改。因此,Arc<RwLock<Config>> 不会在读锁定时随机更改标志。(当然,如果你释放锁并再次获取它,并认为标志在此期间未更改,则可能会出现问题。)
该部分也没有指定如何实际更新配置。你需要一种机制来通知其他线程配置已更改(例如通道),并且线程本身必须使用新配置更新其本地变量。
最终,我认为这个部分是错误的建议,而且肯定不适合初学者。

1
我总体上同意这篇博客非常笨拙而且不完整。我认为唯一需要补充的有用信息是,总体来说,这只是实现了一个非常基本的RCU模式。arc_swap crate几乎完全处理了这个问题,而无需使用重量级锁。 - undefined
谢谢,@GManNickG,这正是我所需要的! - undefined
谢谢 @[Colonel Thirty Two],这证实了我的怀疑。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接