【问题标题】:Understanding a thread safe RwLock<Arc<T>> mechanism in Rust了解 Rust 中的线程安全 RwLock<Arc<T>> 机制
【发布时间】:2021-10-18 18:04:46
【问题描述】:

背景

我对 Rust(昨天开始)完全陌生,我正在努力确保我理解正确。我正在寻找为“游戏”编写一个配置系统,并希望它能够快速访问,但偶尔是可变的。首先,我想研究本地化,这似乎是静态配置的合理用例(因为我很欣赏这些东西通常不是“生锈”)。我想出了以下(工作)代码,部分基于this blog post(通过this question 找到)。我已将其包含在此处以供参考,但请暂时跳过它...

#[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. 呼叫Config::clone,因为步骤 3 包含在 (*...) 中 - 这是正确的方法吗?我的理解是确实现在克隆Config,生成一个可变的拥有实例,然后我可以对其进行修改。
  5. 改变新的配置设置新的debug_mode
  6. 从这个拥有的Config 创建一个新的Arc&lt;Config&gt;
  7. 更新静态 CURRENT_CONFIG。
  8. 将引用计数器释放给旧的Arc&lt;Config&gt;(如果当前没有其他内存在使用它,可能会释放内存)。
  9. 释放写锁。

如果我理解正确,那么在第 4 步中只会发生一次内存分配。对吗?第 4 步是解决此问题的正确方法吗?

了解性能影响

同样,这段代码:

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

正常使用应该很快,因为它使用了这个功能:

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

它获得一个指向当前配置的 ref-counted 指针,而不是实际复制它(clone() 应该像上面那样调用Arc::clone()),使用读锁(除非发生写入,否则速度很快)。

了解thread_local!宏的使用

如果一切都很好,那就太好了!但是,我被困在最后一段代码上:

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

这肯定是错的吗?为什么我们将 CURRENT_CONFIG 创建为 thread_local。我的理解(诚然来自其他语言,结合the limited docs)意味着当前正在执行的线程会有一个独特的版本,这是没有意义的,因为线程不能中断自己?通常我会期望一个真正静态的RwLock 跨多个线程共享?是我误解了什么还是original blog post 中的错误?

的确,下面的测试似乎证实了我的猜想:

    #[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 }`

【问题讨论】:

  • 删除thread_local 似乎确实可以修复我的测试,包括确保Config 状态在线程之间共享并且可以安全更新,下面的完整代码(尽管使用了夜间构建中的最新SyncLazy
  • (*Arc::clone(&amp;writer)).clone() 看起来像 Arc 的不必要克隆 - writer.as_ref().clone() 应该在没有内部克隆的情况下达到相同的目的。虽然克隆Arc 与复制分配的类型相比便宜,但它不是免费的,因为它在操作原子计数器时涉及内存屏障。 (计数器在创建Arc 的临时克隆时更新一次,并在它被销毁时再次更新 - 这些无法优化掉,因为它们对其他线程可见,因此编译器必须生成这两种调整。)
  • 谢谢@user4815162342,Arc::_as_ref() 是否正确增加了引用计数?
  • as_ref() 根本不增加引用计数。它为您提供了一个&amp;T,它不允许超过分发它的Arc。您可以使用&amp;T,在这种情况下调用T::clone(),而无需触及Arc 的引用计数。并且引用不能超过Arc 的事实保证了在您使用引用时对象不会被破坏。

标签: struct rust localization macros config


【解决方案1】:

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

你说得对,这里的 RwLock 是假的 - 它可以替换为 RefCell,因为它是线程本地的。

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

但是,在前面的示例中,我们引入了内部可变性。想象一下,我们有多个线程在运行,所有线程都引用相同的配置,但其中一个会翻转一个标志。并发运行的代码现在不希望标志随机翻转会怎样?

RwLock 的全部意义在于,当对象被锁定以供读取时无法进行修改(即从RwLock::read() 返回的RwLockReadGuard 是活动的)。所以Arc&lt;RwLock&lt;Config&gt;&gt; 不会在读取锁被取出时让你的标志“随机翻转”。 (当然,如果您释放锁并再次获取它,并假设标志在此期间没有更改,这可能是一个问题。)

该部分也没有具体说明如何对配置进行更新。您需要一种机制来通知其他线程发生了配置更改(例如通道),并且线程本身必须使用新配置更新自己的线程局部变量。

最终,我只会认为该部分是不好的建议,而且肯定不适合初学者。

【讨论】:

  • 我同意总体而言该博客非常笨拙且不完整。我认为对 OP 唯一需要提及的是,总的来说,这只是实现了一个非常基本的 RCU 模式。 arc_swap 板条箱或多或少可以完全处理这个问题,而不需要重型锁。
  • 谢谢@GManNickG,这看起来正是我需要的!
  • 谢谢@[三十二上校],这证实了我的怀疑。
猜你喜欢
  • 2021-11-26
  • 2012-01-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-20
  • 2022-08-15
  • 2015-07-02
  • 2014-08-04
相关资源
最近更新 更多