当前位置:首页 > 技术 > 正文内容

Rust并发编程中的 Sync 和 Send Trait 详解

访客 技术 2026年7月4日 2

在并发编程中,数据竞争是最常见的安全隐患,即多个线程同时对同一变量进行读写。然而,当你在 Safe Rust 中编写可能产生数据竞争的代码时,编译器会直接拒绝编译。这背后的机制正是 SyncSend 这两个标记 trait。

Send trait 表示类型的所有权可以在线程间转移,而 Sync trait 表示类型的引用可以在线程间共享。它们都没有方法声明或方法体,仅作为类型约束的标记信息,帮助编译器识别线程不安全的代码。

定义如下:

pub unsafe auto trait Send { }
pub unsafe auto trait Sync { }

本文深入探讨 SyncSend trait,解释为何某些类型实现它们而另一些不实现,并讨论 Rust 并发编程的最佳实践。

Sync Trait

Sync trait 表明类型可以被多个线程同时安全地访问,这里的"访问"指的是只读共享。Rust 中几乎所有原始类型都实现了 Sync

例如:

let x = 5; // i32 实现了 Sync

i32 类型实现了 Sync,因此在线程间共享 i32 值是安全的。

另一方面,提供内部可变性的类型(即在拥有不可变引用时仍能获取可变引用修改数据),如 Mutex<T>,当 T 未实现 Sync 时,该类型本身也不安全。

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}

因为 Mutex 使用锁保护内部数据,若多个线程同时访问,可能导致数据竞争或死锁。

例如:

use std::sync::Mutex;
let m = Mutex::new(5); // Mutex<i32> 未实现 Sync

Mutex<i32> 未实现 Sync,因此跨线程共享是不安全的。要安全访问非 Sync 类型,必须使用适当的同步操作,如获取锁、执行操作和释放锁。

实现 Sync 的类型

Rust 中的 Sync trait 确保同一数据的多个引用(可变或不可变)可以安全地从多个线程并发访问。实现 Sync 的类型 T 被视为"线程安全"。

常见的 Sync 类型示例:

  • 原始类型:i32boolchar 等。
  • 简单聚合类型:元组 (i32, bool)
  • 原子类型:AtomicBool

而非同步类型不能同时使用多个引用,例如:

  • Mutex<i32> — 访问内部 i32 前需锁定互斥锁。
  • RefCell<i32> — 访问内部值前需借用 RefCell。
  • Rc<i32> — 共享内部 i32 所有权,多可变借用不安全。

非 Sync 类型的多线程访问

Mutex

要安全访问非同步类型,使用互斥锁等同步原语。例如,使用作用域线程(crossbeam)结合 Mutex:

use std::sync::Mutex;
use crossbeam::scope;

let data = Mutex::new(String::from("hello"));

scope(|s| {
    for _ in 0..3 {
        s.spawn(|_| {
            let mut val = data.lock().unwrap();
            val.push_str(" world");
        });
    }
}).unwrap();

println!("{}", data.lock().unwrap()); // 输出类似 "hello world world world"

这里,Mutex 保护内部字符串,lock() 获取锁以阻止其他线程访问。

Atomic

原子类型如 AtomicU64 可用原子操作(如 fetch_add)安全访问:

use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;

let counter = AtomicU64::new(0);
let mut handles = vec![];

for _ in 0..10 {
    let c = &counter;
    handles.push(thread::spawn(move || {
        c.fetch_add(1, Ordering::SeqCst);
    }));
}

for h in handles {
    h.join().unwrap();
}

println!("{}", counter.load(Ordering::SeqCst)); // 输出 10

总结

在 Rust 中跨线程共享数据,数据必须:

  • 类型为 Sync(原始/不可变类型)。
  • 封装在互斥或原子类型中(MutexRwLockAtomic*)。
  • 使用通道等消息传递技术传递所有权。

Send Trait

Send trait 表示类型可以安全地跨线程边界传输,即类型值的所有权可在线程间转移。原始类型如 i32boolSend,因为它们在线程间共享时没有内部引用或可变问题。

例如:

use std::thread;

let x = 42;
let handle = thread::spawn(move || {
    println!("{}", x); // 安全,i32 实现了 Send
});
handle.join().unwrap();

Rc<i32> 未实现 Send,因为其引用计数内部可变,多线程同时修改会导致内存不安全:

use std::rc::Rc;
use std::thread;

let rc = Rc::new(5);
let handle = thread::spawn(move || {
    println!("{}", rc); // 编译错误:Rc<i32> 未实现 Send
});

Send 类型如 Rc<T> 只能单线程使用,但可包装在 Arc<T> 等线程安全包装器中,Arc 使用原子操作管理引用计数,允许内部类型在线程间共享。

Send 的关键点:

  • Send 类型可在线程间转移所有权。
  • 原始类型是 Send
  • 具有内部可变性的类型(如 Rc<T>)通常不是 Send
  • Send 类型可在单线程中使用,或包装在 Arc 中共享。
  • 跨线程传输非 Send 类型会导致未定义行为。

自定义实现 Sync 和 Send

要创建自定义 SyncSend 类型,需实现相应 trait。例如,结构体 MyBox 持有裸指针 *const u8,由于裸指针未实现 SendSync,因此 MyBox 默认也不是 SendSync

若手动实现 SendSync,则可通过 Arc 在线程间传递和共享数据。但建议谨慎实现,因为需要确保线程安全性。

某些类型不可能实现 SyncSend,例如 Rc<T> 不能设为 Send(引用计数需原子更新),RefCell<T> 不能设为 Sync(借用检查非线程安全)。

规则与最佳实践

理解 Sync/Send 与混合类型的规则至关重要:

  • 类型必须是 Send 才能在线程间移动。
  • 若类型包含非 Send 类型,则外部类型不能是 Send(如 Option<Rc<i32>>)。
  • Sync 类型可通过共享引用并发使用;非 Sync 类型一次只能在一个线程中可变。
  • 若类型包含非 Sync 类型,则外部类型不能是 Sync(如 Mutex<Rc<i32>>)。

并发 Rust 代码的最佳实践:

  • 优先使用不可变数据结构。
  • 需要修改时,使用 Mutex<T> 等同步原语。
  • 使用消息传递而非直接共享内存,避免数据竞争。
  • 限制锁定范围,避免长时间持有锁影响性能。
  • 根据是否实现 SyncSend 选择类型,如优先 Arc 而非 Rc
  • 使用原子类型实现简单并发访问,避免加锁。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。