使用“thread_local!”和“OnceCell”在多个线程之间共享包含“Rc”引用的静态延迟初始化对象

Share static lazy-initialized object containing `Rc` refs among multiple threads with `thread_local!` and `OnceCell`

我已将我的测试分成几个类似的部分。在每个部分中,将结果与静态测试字符串进行比较,该字符串以专用测试语言(此处称为 dum)编写并使用 pest.

进行解析

这是我的 MWE 的全局结构。

$ tree
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── tests
    ├── dum.pest
    ├── section_1.rs
    └ .. imagine more similar sections here.
[package]
...
edition = "2018"

[dev-dependencies]
pest = "*"
pest_derive = "*"
once_cell = "*"
lazy_static = "*"

use pest_derive::Parser;
use pest::{iterators::Pairs, Parser};

// Compile dedicated grammar.
#[derive(Parser)]
#[grammar = "../tests/dum.pest"]
pub struct DumParser;

// Here is the static test string to run section 1 against.
static SECTION_1: &'static str = "Content to parse for section 1.";

// Type of the result expected to be globally available in the whole test section.
type ParseResult = Pairs<'static, Rule>;

现在,我第一个让所有测试函数都可以使用解析结果的幼稚尝试是:

// Naive lazy_static! attempt:
use lazy_static::lazy_static;
lazy_static! {
    static ref PARSED: ParseResult = {
        DumParser::parse(Rule::any, &*SECTION_1).expect("Parse failed.")
    };
}
#[test]
fn first() {
    println!("1: {:?} parsed to {:?}", &*SECTION_1, *PARSED);
}
#[test]
fn second() {
    println!("2: {:?} parsed to {:?}", &*SECTION_1, *PARSED);
}

这不编译。根据 pest,这是因为它们使用了无法在线程之间安全共享的内部 Rc 引用,我认为 cargo test 确实为每个 #[test] 函数创建了一个新线程。

建议的解决方案涉及使用 thread_local!OnceCell,但我无法弄清楚。以下两次尝试:

// Naive thread_local! attempt:
thread_local! {
    static PARSED: ParseResult = {
        println!(" + + + + + + + PARSING! + + + + + + + "); // /!\ SHOULD APPEAR ONLY ONCE!
        DumParser::parse(Rule::any, &*SECTION_1).expect("Parse failed.")
    };
}
#[test]
fn first() {
    PARSED.with(|p| println!("1: {:?} parsed to {:?}", &*SECTION_1, p));
}
#[test]
fn second() {
    PARSED.with(|p| println!("2: {:?} parsed to {:?}", &*SECTION_1, p));
}

// Naive OnceCell attempt:
use once_cell::sync::OnceCell;
thread_local! {
static PARSED: OnceCell<ParseResult> = {
    println!(" + + + + + + + PARSING! + + + + + + + "); // /!\ SHOULD APPEAR ONLY ONCE!
        let once = OnceCell::new();
        once.set(DumParser::parse(Rule::any, &*SECTION_1).expect("Parse failed."))
        .expect("Already set.");
        once
    };
}
#[test]
fn first() {
    PARSED.with(|p| println!("1: {:?} parsed_to {:?}", &*SECTION_1, p.get().unwrap()));
}
#[test]
fn second() {
    PARSED.with(|p| println!("2: {:?} parsed_to {:?}", &*SECTION_1, p.get().unwrap()));
}

都可以编译 运行 很好。但是 cargo test -- --nocapture 的输出 建议实际上对每个测试函数进行一次解析:

running 2 tests
 + + + + + + + PARSING! + + + + + + +
 + + + + + + + PARSING! + + + + + + +
1: "Content to parse for section 1." parsed_to [Pair { rule: any, span: Span { str: "Content to parse for section 1.", start: 0, end: 31 }, inner: [] }]
2: "Content to parse for section 1." parsed_to [Pair { rule: any, span: Span { str: "Content to parse for section 1.", start: 0, end: 31 }, inner: [] }]
test first ... ok
test second ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

这表明我的两次尝试都失败了。

这些方法有什么问题?
如何使每个部分只进行一次解析?

为什么 lazy_static! 不合适?

是否 cargo test 每次测试都启动一个新线程实际上是无关紧要的。

一个 static 变量是全局的,因此 可能 在线程之间共享,因此即使没有线程被派生,它也必须是 Sync

并且由于 Rc 不是 Sync(不能在线程之间共享),所以这行不通。

为什么 thread_local! 不合适?

顾名思义,每个线程有一个 thread_local! 变量。

thread_local! 中的代码实际上并不是 运行 紧接着 thread-creation;因为变量是在第一次访问时延迟实例化的。

如何让每个部分只进行一次解析?

不要直接使用pest的输出。

如果你 post-process pest 的输出并从中创建一个 Sync 的结构,那么你可以用 lazy_static 存储它,它只会被解析一次。

实际上,您可以走得更远,完全避免 lazy_static。如果您可以用纯粹的 const 方式表达结构,那么您可以使用 build.rs 脚本或程序宏将字符串转换为 compile-time 处的模型。但是对于测试来说,这可能不值得付出努力。