5 个不稳定版本
0.3.1 | 2022年9月30日 |
---|---|
0.3.0 | 2022年9月14日 |
0.2.0 | 2022年9月14日 |
0.1.1 | 2022年9月5日 |
0.1.0 | 2022年9月5日 |
#1286 in 数据结构
2,257 每月下载量
26KB
518 行
Cueue
Cueue 是一个高性能的、单生产者、单消费者、固定大小的循环缓冲区,支持无锁的原子批量操作,适合线程间通信。
示例
fn main() {
let (mut w, mut r) = cueue::cueue(1 << 20).unwrap();
let buf = w.write_chunk();
assert!(buf.len() >= 9);
buf[..9].copy_from_slice(b"foobarbaz");
w.commit(9);
let read_result = r.read_chunk();
assert_eq!(read_result, b"foobarbaz");
r.commit();
}
一个有固定容量的 cueue
由一个 Writer 和一个 Reader 引用。Writer 可以请求空间来写入(write_chunk
),由队列容量减去已提交但未读的空间限制。请求到的空间可以被写入,然后提交(end_write
)。这个容器的特殊之处在于存储的元素总是初始化的(一开始默认初始化,因此 T
必须实现 Default
),并且在队列被丢弃时才被丢弃。因此,writer 可以重用之前写入但后来消费的元素(如果元素拥有堆分配的内存等,这很有用),在这些情况下,避免了生产者在堆锁上的竞争(否则如果消费者连续丢弃生产者分配的堆分配的元素,这种情况就会发生)。
Reader 可以检查写入的元素(read_chunk
),随意处理,然后将其标记为已消费(commit
)。返回的元素切片可能是多个 writer 提交的结果(即:读取是批量的),但它永远不会包含未提交的元素(即:写入提交是原子的)。这防止了 reader 观察到部分消息。
使用场景
这种数据结构旨在允许一个线程(actor)向不同的线程(actor)发送可变大小的消息(字节),而该线程以批量方式处理这些消息(例如:写入文件、通过网络发送等)。例如,异步日志。
替代方案
-
使用字符串的标准通道(或
Vec<u8>
)。这很慢,因为字符串需要内存分配,并且由于一个线程分配而另一个线程释放,很快就会在堆锁上产生竞争。 -
使用固定大小数组的标准通道。这可行,但限制了消息的大小并浪费内存。
-
使用两个
Vec<u8>
容器(一个用于发送数据,一个用于重用已消耗的向量)。不允许高效读取(单独的消息不连续)。需要估计飞行中的最大消息数量,而不是消息最大总大小。
此数据结构使用用户指定容量的单个数组。在任何给定时间,此数组被分割成三个逻辑部分:分配给写入、准备好读取、未写入。 (其中任意两个可以大小为零)
write_chunk
将未写入部分与已分配给写入的部分连接:结果受容量减去准备好读取的空间限制。 Writer::commit
使写入空间准备好读取,并将分配给写入的切片清零。 read_chunk
确定准备好读取的空间边界,Reader::commit
标记此空间为未写入。感谢cueue
的真实环形特性,写入者和读取者可以自由追逐。
它如何工作
cueue
构造函数创建一个内存区域,并将其映射到虚拟内存两次,两个映射相邻。这意味着对于容量为cap
的结果map
,map[0]
和map[cap]
引用相同的字节。 (通常,对于每个索引0 <= N < cap
,map[N]
和map[cap+N]
是相同的)
使用这个双映射,无需回绕,这最大化了队列在任何使用点的有效容量,并简化了代码的索引逻辑。写入者和读者之间的同步是通过原子操作完成的,没有互斥锁或锁定ASM指令前缀(在测试平台:x86和M1)。
(此处未显示,但此结构还允许使用共享内存进行进程间通信,并从核心转储中恢复数据)
限制
- 支持的平台:Linux(3.17)和macOS
- rust 1.63
- 使用
unsafe
操作
构建和测试
$ cargo build
$ cargo test
$ cargo run --example basics
$ cargo fmt
$ cargo clippy
$ cargo bench
$ cargo doc --open
致谢
依赖项
~43KB