随笔一则

  • 作者: Hantong Chen
  • 26 4月 2025
  • 2 分钟阅读
use std::{borrow::Cow, hash::Hash}; struct SniMap(HashMap<PeerKey<'static>, Peer>); impl SniMap { /// Get peer by SNI. pub(crate) fn get<'s, 'sni: 's>(&'s self, sni: &'sni str) -> Option<&'s Peer> { self.0.get(&PeerKey::Exact(Cow::Borrowed(sni))).or_else(|| { self.0.get(&PeerKey::Subdomain(Cow::Borrowed( sni.split_once('.').map(|(_, prior)| prior).unwrap_or(sni), ))) }) } } #[derive(Debug, PartialEq, Eq)] enum PeerKey<'k> { /// Match exactly the SNI. This is the default mode. /// /// Will be serialized or deserialized as `example.com`. Exact(Cow<'k, str>), /// Subdomain match /// /// Should be serialized or deserialized as `domain:example.com`. /// /// ## Notice /// /// Matches direct subdomains only; does not match nested subdomains /// (second-level or deeper). /// /// Take `domain:example.com` as an example, it will match `*.example.com` /// but not `*.*.example.com`, etc. Subdomain(Cow<'k, str>), } impl Hash for PeerKey<'_> { fn hash<H: std::hash::Hasher>(&self, state: &mut H) { match self { PeerKey::Exact(s) => s.hash(state), PeerKey::Subdomain(s) => { state.write(b"domain:"); s.hash(state) } } } } struct Peer { // ... }

非常平平无奇的代码. 于是有新的需求: 想缓存 get 的结果 (把按 domain:example.com 匹配到的 sub.example.comfull:sub.example.com 写入表内), 达到下次读哈希表一次就能把 Peer 读出来.

有这么几个方案:

  • 改用三方库 dashmap 提供的 DashMap, 类似 RwLock 但 API 层面 是 Lock-free 的.
  • 套一层 Mutex / RwLock.
  • RefCell 毙掉, 我们需要 Send + Sync, 应该用其线程安全版本: RwLock

但事实上都是不可行的, 为什么? 欢迎 Rust 初学者自行查阅资料 (问 AI 也行)~

答案 (实际上编译器已经提示了):

  • 对于具有内部可变性的 Cell 类似物, 子类型型变要求是不变 (invariant) 的1. 本例中 DashMap 内部用了 lock_api 库的 RwLock, 这玩意内部用了 UnsafeCell, 故相应字段涉及的泛型类型 K 要求是不变的.

  • 不能返回本地变量 (拿到的 &Peer 只能活得和上锁后拿到的 Guard 一样久). 起码在当前的 API 设计下不能这么干. 当然, 写出来 get_and_map 一类的奇怪 API 也不是不行.

评价: 写 Rust 的福报