惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿
H
Help Net Security
Last Week in AI
Last Week in AI
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
IT之家
IT之家
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
Google DeepMind News
Google DeepMind News
B
Blog
C
Check Point Blog
T
Tailwind CSS Blog
云风的 BLOG
云风的 BLOG
D
Docker
Recent Announcements
Recent Announcements
Vercel News
Vercel News
博客园 - 聂微东
阮一峰的网络日志
阮一峰的网络日志
MyScale Blog
MyScale Blog
The GitHub Blog
The GitHub Blog
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
月光博客
月光博客
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
B
Blog RSS Feed
The Register - Security
The Register - Security
V
Visual Studio Blog
F
Full Disclosure
Hugging Face - Blog
Hugging Face - Blog
T
Threat Research - Cisco Blogs
Latest news
Latest news
PCI Perspectives
PCI Perspectives
Cisco Talos Blog
Cisco Talos Blog
博客园 - Franky
D
DataBreaches.Net
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
P
Palo Alto Networks Blog
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
T
Tenable Blog
L
LINUX DO - 热门话题
Spread Privacy
Spread Privacy

I'm OWenT

国产大模型(GLM 5.1、Kimi K2.6)真实场景效果和 Coding Plan 额度测试 新版本libatapp的连接管理——从etcd服务发现到拓扑驱动的自动重连 新版本libatbus的设计变更——从树形路由到拓扑驱动 Protobuf又一坑 - C++标准和ABI兼容性 AI真好用-给Blog主题统一加mermaid,chart.js,excalidraw,draw.io的多种引入方式支持 给内网部署Squid-通用HTTP下载缓存 UE使用CodeChecker和clang-tidy生成静态分析报告 找出UE的循环依赖 C++小协程栈和临时变量及作用域的栈溢出问题分析 游戏服务的可观测性能力建设(C++生态) 指标上报的多线程优化和多拉取源点优化 协程(libcopp)的Channel功能和CPU命中率优化 通用RPC代码生成器 实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针) 手夯一个STL allocator和对象内存分析组件 std::condition_variable 的信号丢失问题 踩坑一处(GCC)STL std::async 实现BUG导致的crash问题 GCC 14的一个warning to error BUG 给xresloader(Excel导表工具)增强UE读表支持(包含蓝图,Blueprint) Opentelemetry社区在gRPC的几个链接问题(静态库和动态库混用,musl工具链,符号裁剪) Excel转表工具(xresloader)的新验证器(验证外部Excel和文本数据,唯一性和自定义规则) protobuf v22和gRPC v1.55版本升级的依赖变化和upb适配 关于protobuf近期版本(v20/v3.20+)和 gRPC v1.54版本在某些编译环境下的一些链接和编译问题 xresloader-Excel导表工具链的近期变更汇总 打通游戏服务端框架的C++20协程改造的最后一环 Opentelemetry-cpp的Logs模块标准更新(涉及近期版本:1.8-1.9的BREAK CHANGES) 给cmake-toolset和工具链(curl等)加HTTP/2和HTTP/3支持 又开新坑之 coredns 插件: nftables和filter 关于opentelemetry-cpp社区对于C++ Head Only组件单例和符号可见性的讨论小记 填个转表工具 xresloader 去年的坑(数组尾部裁剪) 集成 upb 和 lua binding 的踩坑小记 libcopp对C++20协程的接入和接口设计 再度优化GCC、LLVM、Clang、libc++、libc++abi等套件的构建脚本 游戏服务的分布式事务优化(二)- 事务管理 游戏服务的分布式事务优化(一)- Write Ahead Log(WAL) 模块 记录一些bazel适配用编译选项 测试现代化硬件C++浮点数性能和一致性 适配Boringssl和OpenSSL 3.0 近期cmake-toolset的一些适配问题 C++20 Text Formatting/fmtlib 适配问题小记 再次重构LLVM+Clang+libcxx+libc++abi+其他相关工具的构建流程 重构基于CMake的构建工具链 新版GCC和LLVM+Clang终于Release啦 折腾一下nftables下的双拨 [C++20] Module partitions和符号交叉引用(声明和实现分离) 基于protobuf的代码生成 几个使用protobuf中C++接口的Arena的坑 Amazon Aurora DB存储引擎论文阅读小记 近期对libatapp的一些优化调整(增加服务发现和连接管理,支持yaml等) xresloader转表工具链增加了一些新功能(map,oneof支持,输出矩阵,基于模板引擎的加载代码生成等) 在游戏服务器中使用分布式事务 libcopp接入C++20 Coroutine和一些过渡期的设计 libatbus 的大幅优化 nftables初体验 容器配置开发环境小计 PALM Tree - 适合多核并发架构的B+树 - 论文阅读小记 跨平台协程库 - libcopp 简介 C++20 Coroutine 性能测试 (附带和libcopp/libco/libgo/goroutine/linux ucontext对比) 尝鲜Github Action 一些xresloader(转表工具)的改进 protobuf、flatbuffer、msgpack 针对小数据包的简单对比 协程框架(libcopp) 小幅优化 Excel转表工具(xresloader) 增加protobuf插件功能和集成 UnrealEngine 支持 Anna(支持任意扩展和超高性能的KV数据库系统)阅读笔记 C++20 Coroutine libcopp merge boost.context 1.69.0 Google去中心化分布式系统论文三件套(Percolator、Spanner、F1)读后感 Rust玩具-企业微信机器人通用服务 使用ELK辅助监控开发测试环境服务质量和问题定位 2018年的新通用伪随机数算法(xoshiro / xoroshiro)的C++(head only)实现 Webpack+vue+boostrap+ejs构建Web版GM工具 Rust的第二次接触-写个小服务器程序 理解和适配AEAD加密套件 atsf4g-co的进化:协程框架v2、对象路由系统和一些其他细节优化 协程框架(libcopp)v2优化、自适应栈池和同类库的Benchmark对比 可执行文件压缩 初识Rust 使用restructedtext编写xresloader文档 atframework的etcd模块化重构 C++的backtrace ECDH椭圆双曲线(比DH快10倍的密钥交换)算法简介和封装 protobuf-net的动态Message实现 pbc的proto3接入 atgateway内置协议流程优化-加密、算法协商和ECDH 整理一波软件源镜像同步工具+DevOps工具 Blog切换到Hugo libcopp v2的第一波优化完成 libcopp(v2) vs goroutine性能测试 libcopp的线程安全、栈池和merge boost.context 1.64.0 GCC 7和LLVM+Clang+libc++abi 4.0的构建脚本 libatbus的几个藏得很深的bug 用cmake交叉编译到iOS和Android 开源项目得一些小维护 atapp的c binding和c#适配 对象路由系统设计 2016年总结 近期的一个协程流程BUG 重写了llvm+clang+libc++和libc++abi的构建脚本 atsf4g完整游戏工程示例|I'm OWenT atframework基本框架已经完成|I'm OWenT
[Rust] 实现一个线程安全且迭代器可以保存的链表
owent · 2021-03-10 · via I'm OWenT

blog-website

背景

今年有个想法,重新设计 libatbus 然后用 Rust 实现出来,然后可以加入一些云原生的支持。这需要一个定时器模块,我看了下 Rust 现有的几种定时器的实现,大多是基于堆或树的结构的,没有找到jiffies定时器的实现,所以想自己实现一个算了。这个定时器的实现又需要类似 C++ 的 std::list::iterator插入和删除某个迭代器对其他迭代器没有影响 的特性,但是 Rust 的数据结构都不是这种设计模型。所以就决定自己写一个吧。

为什么不使用现有的链表

像链表这种基础的数据结构,稍微现代化的语言肯定都是带的。Rust 也不例外,提供了标准库的 std::collections::LinkedList 。但是它的实现没法满足上面提到的需求。一个重要的原因是 std::collections::LinkedList 也遵循 Rust 的借用和可变借用的规则,另一方面也是由于它的实现是尽可能没有额外开销。

Rust 是在编译期去分析管理对象的生命周期的,所有对象的生命周期的持有者只能有一个。所有对象都只能有一个可变借用或多个不可变借用。但是可变借用和多个不可变借用直接不能共存,相当于是编译期的读写锁。 借用可以理解为不管理生命周期的引用。

稳定版本的 std::collections::LinkedList 的迭代器 IterIterMut 是没有插入和删除接口的。只有向前和向后迭代的接口,也就是说只能读写链表内的元素,不能修改链表本身。 Nightly版本的 Rust 标准库里的 std::collections::LinkedList 额外提供了 cursor_front(&self)cursor_front_mut(&mut self)cursor_back(&self)cursor_back_mut(&mut self) 来返回游标,这个游标就是在迭代器的基础上有增加了向前向后插入和删除接口,可以修改链表本身。乍看起来好像是可以符合需求,但是实际上也没法使用。

比如说,如果使用 cursor_front_mut(&mut self) 函数创建一个可变的 CursorMut。那么会占用掉容器的可变借用的权限。相当于会锁住这个 std::collections::LinkedList 。这时候直到我释放这个 CursorMut 前,对链表的其他操作都无法进行。所以就不能把这个游标保存起来以后用。那可不可以包一层 RefCell 来运行时借用,然后只用不可变的 Cursor 呢? 其实也是不可以的,因为首先 Cursor 和迭代器一样没有提供修改链表本身的接口,另一方面持有 Cursor 也会导致容器本身不能使用mutable的接口,也就无法完成增删链表节点的操作。

简单来说,无论是 Iter 还是 Cursor 都是用于放在栈上临时使用的,迭代器和游标的生命周期都低于容器本身,并不适用于需要长期保存的场景。

新链表的结构

从另一个角度说,我们需要的是能够保存迭代器,并在需要的时候基于迭代器操作。这本身是一个运行时可以修改容器的行为,属于运行时可变借用。与此同时还需要考虑多线程问题,即迭代器可以在多个线程中转移,就意味着可变借用这个过程可能在多个线程上同时发生。这两点都会带来额外开销。

链表的实体和节点数据结构如下:

pub type LinkedListItem<T> = Arc<T>;
type Node<T> = Arc<RwLock<NodeEntry<T>>>;

struct NodeEntry<T> {
    next: Option<NonNull<Node<T>>>,
    prev: Option<NonNull<Node<T>>>,
    element: Option<LinkedListItem<T>>,
    end: Weak<RwLock<NodeEntry<T>>>,
    leak: Option<NonNull<Node<T>>>,
}

struct UnmoveableLinkedList<T> {
    end: Node<T>,
    len: usize,
}

pub struct Iter<T> {
    node: Weak<RwLock<NodeEntry<T>>>,
    last_back: bool,
}

我采用了和 std::collections::LinkedList 类似的做法。数据节点创建完以后,通过 Box::leak 函数转换成指针,然后内部使用指针来手动维护数据。然后还有几个个标准库实现不同的地方。

首先是增加了 leak 字段,用于简化对自己的地址的访问。像标准库的实现,接口调用的数据源都是上层的 Iter 或者 Cursor 或者链表的头尾。里面都记录了对应节点的地址。但是我们这里是需要根据节点自身的数据反推出自己的地址的,不加 leak 字段就必须通过 nextprev 访问来取。比较麻烦而且增加冲突率,所以干脆直接加了 leak 字段。

其次增加了 end 字段指向链表的 Ghost 节点。这也有两个作用,其一是用于实现和 Cursor 类似的功能。如果移到最后或者第一个,再往后或往前移一次移到 Ghost 节点,之所以要这个字段来辅助是因为 std::collections::LinkedListCursor 是不能存在两个同时改的,而我们这个链表可以。这意味着可能迭代器向后移到 Ghost 之后,接下来最后一个节点被其他地方删除了,这个迭代器再向前移一次能够移动到新的尾部节点。其二是用于检查迭代器的所属容器,因为节点里的 end 总是指向容器的 end ,然后按迭代器做插入删除的时候,我们就能根据这个检查,如果迭代器对应的节点不属于调用的容器的时候要禁止操作。

至于容器里用 Node<T> 包一层而不是像 std::collections::LinkedList 直接存 nextprev 也是为了上面提到的目的。这个节点的生命周期也是跟着容器本身的。如果容器释放了,这个节点也就释放了,外部的迭代器对象无论是尝试解引用还是移到末尾都是会失败的。

关于Send语义、Sync语义、线程安全

标准库的 std::collections::LinkedList<T> 在实例化类型 T 支持 SendSync 语义的时候,分别也提供了 SendSync 语义的支持。但是到我们这里会更复杂一点。因为标准库的 std::collections::LinkedList<T> 实际上是走了 Rust 语言层面的对修改控制权限的管理。

包括标准库实现里的 IterCursor 里都存了 len 和提供方法获取后续有多少可用元素都是依赖于此。 因为 IterMutCursorMut 只要能创建出来,那之前一定没有不可变借用了。那么对 len 字段的修改就有且仅有最后创建出来的这个 IterMutCursorMut。 也就不存在修改一个迭代器或游标导致影响其他迭代器或游标的问题。

但是我们这里分离了迭代器和容器的生命周期,就不能简单地这么声明了。

首先,由于我需要让这个链表的迭代器和容器的生命周期解绑,所以对链表的节点包了一层 Arc<U> 。所以我们这个链表节点本身,其实对于所有的类型 T 都可以支持 Send 语义。但是访问实际数据的层面还是需要可以 Sync 才能跨线程读的。所以为了防止误用目前只对同时满足 Sync + SendT 类型实现 atlist_rs::LinkedList<T> 生命为支持 SendSync

减少锁的临界区

可以看到我们实际节点的类型为 Arc<RwLock<NodeEntry<T>>> , 中间还有一层 RwLock<U> 。因为我们解绑了迭代器和容器的生命周期,那么就无法在编译期保证多线程的场景下对节点的修改操作互相不冲突,这里的锁的作用其实也是为了支持多线程访问容器。对链表节点的 mutable 操作其实已经在链表接口那一层,通过 Rust 自带的借用管理控制了,不会发生冲突。举个例子,在迭代器和容器的生命周期解绑的情况下,可能发生一个线程在做删除操作,另一个线程在做这个节点的 prev 正在执行 next(&mut self) 。这时候需要读这个节点内的数据,然后读到的 next 的指针就有可能是无效的。

这个 RwLock<U> 我们控制在内部使用,不会暴露到外部,这样可以我们自己管理和缩减锁的临界区,并且避免死锁。

首先所有的加锁操作都是先 prevnext 这个顺序,就是不会出现一个线程锁 A->B 另一个线程锁 B->A 的死锁操作。另一方面,所有的写操作生命周期都是绑在 atlist_rs::LinkedList<T> 上的,根本不会并发。

其实在写操作被限制在 atlist_rs::LinkedList<T> 的基础上,这个 RwLock<U> 理论上是可以省掉的,如果我们的记录节点内部的 nextprev 直接用 Arc<U> 保存,而不是按标准库一样存指针,那么也是可以保证多线程安全的,但是目前 Arc<U> 只提供了非可变借用的访问接口, pub unsafe fn get_mut_unchecked(this: &mut Arc<T>) -> &mut T 还处于nightly阶段。我也不想多去造这个轮子,没啥意义,所以这个去除锁的操作还是等这个特性stable了,我再来优化吧。

另一种不依赖 pub unsafe fn get_mut_unchecked(this: &mut Arc<T>) -> &mut T 的方式是再套一层 RefCell 但是这会影响 SendSync 语义。所以也没有选择这么做。

运行时可变借用

我们的链表里,用户类型的定义为 pub type LinkedListItem<T> = Arc<T>; 。这意味着对外提供的解引用接口解出的 Arc<T> 只能获取 T 的immutable 借用。本来最初我是想要不要套一层 RefCell<U> 来实现运行时可变借用的。但是这样感觉会提供整个库使用的难度和复杂度,而且也不线程安全。要线程安全就得也套个 RwLock<U> 或者 Mutex<U> , 这样开销高不说也不能覆盖实际使用的场景。所以最终还是决定不套了。由使用者来决定要怎么用,要不要要跨线程等等。

未来可能的偏特化优化

还有个理想的情况是如果实例化的类型 T 不支持 Sync + Send ,那么我们的链表不需要使用 Arc<U> 包装,直接用 Rc<U> 就可以了,节点管理的 RwLock<U>也可以去掉。这样能省掉原子操作时的CPU Cache miss开销。现在的想法是使用 偏特化 实现。但是 Rust 里的 偏特化 特性目前还只处于 nightly 阶段,等这个特性stable了再优化吧。

开源和包

包地址: https://crates.io/crates/atlist-rs 代码仓库: https://github.com/atframework/atlist-rs 自动化文档: https://docs.rs/atlist-rs/

可以直接 Cargo.toml 里通过下面的配置来引用:

[dependencies]
atlist-rs >= "0.2"

目前单元测试覆盖率只有 80+% 。其实是因为有几处代码的分支是为了以后可能接入类似标准库里的splice接口预留的。现在是为了保证每个接口的完整性都实现了,其实没有外部接口引用到。以后看情况是否加这个接口或者补一下非导出接口的单元测试吧。