万物皆可多线程?深入解读 Rust 的 Send 和 Sync

完整阅读,移步《万物皆可多线程?深入解读 Rust 的 Send 和 Sync》
文章目录
- 故事始于“数据竞争”
- Sync 堵上了不可变引用的漏洞
- Send 堵上了跨线程 move 的漏洞
- Send / Sync 更深层成因的洞察
- 在打标记这件事上编译器做了什么?
- 为什么其他语言里没有 Send / Sync
坦率地说,目前大多数介绍 Send / Sync 的文章都有些“隔靴搔痒”的感觉,它们确实介绍了这两个 Trait 但读完之后又感觉好像什么也没说。Send / Sync 只是两个标记特质(Marker Trait),没有任何关联方法,它们唯一的用途是出现在约束里,让编译器做类型检查。只需寥寥数语,Send / Sync 就解释完了,但这也正是它们最难理解的地方,很多人都对这两个 Trait 的由来和作用感到困惑,要完全理解它们既需要对 Rust 的所有权和借用检查有深刻的认识,又需要一个与线程安全相关的“上下文”。本文,我们会带领大家把 Send / Sync 这两个 Trait 彻底搞明白。
1. 故事始于“数据竞争”
数据竞争不是线程安全中唯一的问题,但一定是发生频率和讨论度最高的。在多线程环境下,如果有两个以上的指针或引用指向同一个数据,且至少有一个指针或引用会写数据,在没有同步保护(例如加锁)的情况下就会出现“数据竞争”问题。多线程环境中“转账问题”就是一个非常典型的案例:正常情况下,多个账户之间相互转账,无论经历多少轮操作,所有账户的金额总和是不会变的;但一旦变成多线程并发操作,如果不对“账户余额”施加同步保护,则多轮转账过后,所有账户的金额总和就可能会发生改变,这是很严重的 bug,但只会在多线程环境下才会发生,并且很难追踪,因为转账操作本身并没有逻辑错误。在这个案例中,账户总金额被改变的这种现象叫“数据腐蚀”,账户余额就是被“竞争”的数据,数据腐蚀是数据竞争导致的一种典型错误。

浙公网安备 33010602011771号