Apache License 2.0

技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

深度分析 GitHub 仓库 “frostyplanet/crossfire-rs”

项目背景与痛点

跨平台异步和同步通信是现代软件开发中面临的常见问题。随着多核处理器的普遍使用,锁机制在并行任务调度中的效率成为一个关键技术瓶颈。项目 “frostyplanet/crossfire-rs” 提供了一种无锁的多生产者多消费者(MPMC)和多生产者单消费者(MPS)通信机制,这是为了在异步/同步环境中提高系统的并发处理能力。传统的同步锁机制在高并发环境下容易产生死锁或性能瓶颈,而无锁设计则能显著改善这些问题。

核心技术揭秘

项目的核心设计在于实现一个无锁通信模型,它引入了一些底层原则和技术手段来保证数据的一致性和可靠性。Crossfire-rs 充分利用了 Rust 语言的内存安全机制和异步特性,使得其在高性能并发编程场景中颇具优势。

graph TD A[应用进程] --> B[异步/同步调用] B --> C{"通道类型选择"} C -->|"MPMC"| D[多生产者多消费者通道] C -->|"MPSC"| E[多生产者单消费者通道] D --> F[异步数据传输] E --> F F --> G[进程间同步或异步数据处理]

在无锁的 MPMC 和 MPSC 通道实现中,项目使用了先进原子操作和内存屏障技术,以确保在并发访问时能正确执行读写操作。同时,它结合 Rust 的所有权系统和生命周期管理,避免了传统多线程编程中的数据竞争和内存泄漏问题。项目中的数据流模型设计,通过对通道的状态机进行精确的控制,使得多线程间的通信能够流畅且高效地进行。

功能亮点与差异

相比于竞品,Crossfire-rs 在保证无锁通信的同时,提供了强大的异步和同步编程模型,支持跨平台通信,并且其模块化的设计让其易于与现有系统集成。该项目的一大亮点是其对高性能应用的支持,尤其适合需求高并发但又不能容忍锁带来的性能损耗的应用场景。

此外,Crossfire-rs 通过其独特的通道实现,进一步抽象出了异步生产和消费的概念,使得开发者可以在不关心底层实现细节的情况下编写高度并发的程序。这种模块化和抽象化的设计,从本质上优化了开发者的使用体验。

应用场景与落地建议

在生产环境中部署 Crossfire-rs 时,需要注意系统的并发量和对延迟的敏感度。虽然 Crossfire-rs 在高并发场景下表现出色,但也需要考量具体应用的需求——在低延迟要求的场景下,确保无锁机制能够高效工作。项目建议在分布式系统和高性能计算场景中使用,如消息队列、RPC 服务端、实时数据处理系统等。

另外,了解 Crossfire-rs 如何应用在现有架构中,有助于更好地集成并发挥其优势。需要确保开发人员充分理解异步与同步任务在 Rust 中的执行逻辑。

综合评价

优点:Crossfire-rs 项目通过无锁机制的引入,契合了现代高性能并发编程的需求,其对 Rust 语言特性的充分利用,展示了一个高效且可靠的数据通信模型。无锁的设计保证了数据在多线程环境下的安全与一致性。

缺点:对于那些并不需要特别关注锁竞争的场景,Crossfire-rs 可能显得相对复杂,额外的无锁逻辑可能增加了一定的代码复杂度。

总体来说,该库为开发者提供了强大的工具,来实现业务逻辑复杂度极高的高性能应用。在未来版本迭代中,我们可以期待更多优化和新特性的加入,以进一步提升整体性能表现。

关于

GitHub 原始描述(英文)

A lockless mpmc/mpsc to support async/sync (derive from crossbeam)

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

简要分析

热度分
910
价值分
312
活跃状态
活跃

语言占比

Makefile
Rust
Shell

README

Crossfire

Build Status License Cargo Documentation Rust 1.36+

High-performance lockless spsc/mpsc/mpmc channels, algorithm derives crossbeam with improvements.

It supports async contexts and bridges the gap between async and blocking contexts.

For the concept, please refer to the wiki.

Version history

  • v1.0: Used in production since 2022.12.

  • v2.0: [2025.6] Refactored the codebase and API by removing generic types from the ChannelShared type, which made it easier to code with.

  • v2.1: [2025.9] Removed the dependency on crossbeam-channel and implemented with a modified version of crossbeam-queue, brings 2x performance improvements for both async and blocking contexts.

  • v3.0: [2026.1] Refactored API back to generic flavor interface, added select. Dedicated optimization: Bounded SPSC +70%, MPSC +30%, one-size +20%. Eliminate enum dispatch cost, async performance improved for another 33%. Checkout compat for migration from v2.x.

Performance

Being a lockless channel, crossfire outperforms other async-capable channels. And thanks to a lighter notification mechanism, in a blocking context, most cases are even better than the original crossbeam-channel,

mpsc bounded size 100 blocking context mpmc bounded size 100 blocking context mpsc bounded size 100 async context mpmc bounded size 100 async context

More benchmark data is posted on wiki.

Also, being a lockless channel, the algorithm relies on spinning and yielding. Spinning is good on multi-core systems, but not friendly to single-core systems (like virtual machines). So we provide a function detect_backoff_cfg() to detect the running platform. Calling it within the initialization section of your code, will get a 2x performance boost on VPS.

The benchmark is written in the criterion framework. You can run the benchmark by:

make bench crossfire
make bench crossfire_select

APIs

Concurrency Modules

  • spsc, mpsc, mpmc. Each has different underlying implementation optimized to its concurrent model. The SP or SC interface is only for non-concurrent operation. It’s more memory-efficient in waker registration, and has atomic ops cost reduced in the lockless algorithm.

  • oneshot has its special sender/receiver type because using Tx / Rx will be too heavy.

  • select:

    • Select<'a>: crossbeam-channel style type erased API, borrows receiver address and select with “token”
    • Multiplex: Multiplex stream that owns multiple receiver, select from the same type of channel flavors, for the same type of message.
  • waitgroup High performance WaitGroup that allows custom threshold.

Flavors

The following lockless queues are expose in flavor module, and each one have type alias in spsc/mpsc/mpmc:

  • List (which use crossbeam SegQueue)
  • Array (which is an enum that wraps crossbeam ArrayQueue, and a One if init with size<=1)
    • For a bounded channel, a 0 size case is not supported yet. (rewrite as 1 size).
    • The implementation for spsc & mpsc is simplified from mpmc version.
  • One (which derives from ArrayQueue algorithm, but have better performance in size=1 scenario, because it have two slots to allow reader and writer works concurrently)
  • Null (See the doc null), for cancellation purpose channel, that only wakeup on closing.

NOTE : Although the name Array, List are the same between spsc/mpsc/mpmc module, they are different type alias local to its parent module. We suggest distinguish by namespace when import for use.

Channel builder function

Aside from function bounded_*, unbounded_* which specify the sender / receiver type, each module has build() and new() function, which can apply to any channel flavors, and any async/blocking combinations.

Types

Context Sender (Producer) Receiver (Consumer)
Single Multiple Single Multiple
Blocking BlockingTxTrait BlockingRxTrait
Tx MTx Rx MRx
Weak referenceWeakTx
Async AsyncTxTrait AsyncRxTrait
AsyncTx MAsyncTx AsyncRx MAsyncRx

Safety: For the SP / SC version, AsyncTx, AsyncRx, Tx, and Rx are not Clone and without Sync. Although can be moved to other threads, but not allowed to use send/recv while in an Arc. (Refer to the compile_fail examples in the type document).

The benefit of using the SP / SC API is completely lockless waker registration, in exchange for a performance boost.

The sender/receiver can use the From trait to convert between blocking and async context counterparts (refer to the example below)

Error types

Error types are the same as crossbeam-channel:

TrySendError, SendError, SendTimeoutError, TryRecvError, RecvError, RecvTimeoutError

Async compatibility

Tested on tokio-1.x and async-std-1.x, crossfire is runtime-agnostic.

The following scenarios are considered:

  • The AsyncTx::send() and AsyncRx::recv() operations are cancellation-safe in an async context. You can safely use the select! macro and timeout() function in tokio/futures in combination with recv(). On cancellation, SendFuture and RecvFuture will trigger drop(), which will clean up the state of the waker, making sure there is no memory-leak and deadlock. But you cannot know the true result from SendFuture, since it’s dropped upon cancellation. Thus, we suggest using AsyncTx::send_timeout() instead.

  • When the “tokio” or “async_std” feature is enabled, we also provide two additional functions:

  • AsyncTx::send_timeout(), which will return the message that failed to be sent in SendTimeoutError. We guarantee the result is atomic. Alternatively, you can use AsyncTx::send_with_timer().

  • AsyncRx::recv_timeout(), we guarantee the result is atomic. Alternatively, you can use AsyncRx::recv_with_timer().

  • The waker footprint:

When using a multi-producer and multi-consumer scenario, there’s a small memory overhead to pass along a Weak reference of wakers. Because we aim to be lockless, when the sending/receiving futures are canceled (like tokio::time::timeout()), it might trigger an immediate cleanup if the try-lock is successful, otherwise will rely on lazy cleanup. (This won’t be an issue because weak wakers will be consumed by actual message send and recv). On an idle-select scenario, like a notification for close, the waker will be reused as much as possible if poll() returns pending.

  • Handle written future:

The future object created by AsyncTx::send(), AsyncTx::send_timeout(), AsyncRx::recv(), AsyncRx::recv_timeout() is Sized. You don’t need to put them in Box.

If you like to use poll function directly for complex behavior, you can call AsyncSink::poll_send() or AsyncStream::poll_item() with Context.

Usage

Cargo.toml:

[dependencies]
crossfire = "3.1"

Feature flags

  • compat: Enable the compat model, which has the same API namespace struct as V2.x

  • tokio: Enable send_timeout(), recv_timeout() with tokio sleep function. (conflict with async_std feature)

  • async_std: Enable send_timeout, recv_timeout with async-std sleep function. (conflict with tokio feature)

  • trace_log: Development mode, to enable internal log while testing or benchmark, to debug deadlock issues.

Example

blocking / async sender receiver mixed together


extern crate crossfire;
use crossfire::*;
#[macro_use]
extern crate tokio;
use tokio::time::{sleep, interval, Duration};

#[tokio::main]
async fn main() {
    let (tx, rx) = mpmc::bounded_async::<usize>(100);
    let mut recv_counter = 0;
    let mut co_tx = Vec::new();
    let mut co_rx = Vec::new();
    const ROUND: usize = 1000;

    let _tx: MTx<mpmc::Array<usize>> = tx.clone().into_blocking();
    co_tx.push(tokio::task::spawn_blocking(move || {
        for i in 0..ROUND {
            _tx.send(i).expect("send ok");
        }
    }));
    co_tx.push(tokio::spawn(async move {
        for i in 0..ROUND {
            tx.send(i).await.expect("send ok");
        }
    }));
    let _rx: MRx<mpmc::Array<usize>> = rx.clone().into_blocking();
    co_rx.push(tokio::task::spawn_blocking(move || {
        let mut count: usize = 0;
        'A: loop {
            match _rx.recv() {
                Ok(_i) => {
                    count += 1;
                }
                Err(_) => break 'A,
            }
        }
        count
    }));
    co_rx.push(tokio::spawn(async move {
        let mut count: usize = 0;
        'A: loop {
            match rx.recv().await {
                Ok(_i) => {
                    count += 1;
                }
                Err(_) => break 'A,
            }
        }
        count
    }));
    for th in co_tx {
        let _ = th.await.unwrap();
    }
    for th in co_rx {
        recv_counter += th.await.unwrap();
    }
    assert_eq!(recv_counter, ROUND * 2);
}

Test status

NOTE: Because we has push the speed to a level no one has gone before, it can put a pure pressure to the async runtime. Some hidden bug (especially atomic ops on weaker ordering platform) might occur:

The test is placed in test-suite directory, run with:

make test
archruntimeworkflowstatus
x86_64 threaded cron_master_threaded_x86 STABLE
tokio 1.47.1 cron_master_tokio_x86 STABLE
async-std cron_master_async_std_x86 STABLE
smol cron_master_smol-x86 STABLE
compio cron_master_compio-x86 verifying
arm threaded cron_master_threaded_arm
STABLE
tokio >= 1.48 (tokio PR #7622) cron_master_tokio_arm
SHOULD UPGRADE tokio to 1.48
STABLE
async-std cron_master_async_std_arm STABLE
smol cron_master_smol_arm STABLE
compio cron_master_compio_arm verifying
miri (emulation) threaded miri_tokio
miri_tokio_cur
STABLE
tokioSTABLE
async-std- (timerfd_create) not supported by miri
smol- (timerfd_create) not supported by miri

Debugging deadlock issue

Debug locally:

Use --features trace_log to run the bench or test until it hangs, then press ctrl+c or send SIGINT, there will be latest log dump to /tmp/crossfire_ring.log (refer to tests/common.rs _setup_log())

Debug with github workflow: https://github.com/frostyplanet/crossfire-rs/issues/37

评论

暂无评论