Skip to main content
14 并发

14 并发

•2 min•347 words
并发与并行是两个概念

应该学习过分时操作系统,时间分片的概念。并发的两种实现形式:交替执行、并行执行。
并发就是在一段时间内,宏观上同时在执行多个任务。
并行就是在任何时刻,都在执行多个任务。

Summary
  • 竞态条件,多个线程以不一致的顺序访问数据或资源(实际上需要配合才能完成任务)
  • 死锁,两个线程相互等待对方,这会阻止两者继续运行
  • 只会发生在特定情况且难以稳定重现和修复的 bug

线程模型

Summary

rust提供的线程都对应一个操作系统线程。

生命周期管理

新建、就绪、运行、阻塞、销毁

Summary
  • std::thread::spawn(|| { ... }):创建一个以当前线程为主线程的新的操作系统原生线程(1:1 模型),并返回一个 JoinHandle。
  • std::thread::Builder::new():允许配置线程的栈大小(stack_size)和名称(name),适合需要大栈或调试的场景。
实际上新的线程就像是树的分支,依赖于根线程的存在

已有的线程派生出的新线程依赖于旧线程而存在,形成了树状的多线程结构。从目的来看,派生的线程应该被叫作临时任务线程。主线程的目的是从临时任务线程做一些东西。

Warning

如果不获取所有权,Rust 不知道这个新建线程会执行多久,所以无法知晓对 v 的引用是否一直有效。move是新线程访问原有变量的最佳方式。

Summary
  1. 分离(Detach):如果你丢弃(drop)了 JoinHandle,该线程变为分离状态,后台继续运行,直到其闭包执行完毕自动销毁(由 OS 回收)
  2. 进程结束:主线程(Main)结束时,整个进程终止,所有未完成的子线程会被操作系统强行终止。
Summary

返回值:Result<T, Box<dyn Any + Send>>。如果子线程发生 panic!,join 会返回 Err,允许父线程捕获 panic 信息(这是 Rust 隔离线程崩溃的重要手段)。

调度控制

Summary
  1. sleep 阻塞在定时器上,当前线程进入睡眠状态,主动放弃剩余的时间片。在指定时长内,该线程不参与 CPU 调度(阻塞在定时器上),时长耗尽后进入“就绪态”等待调度。
  2. 主动阻塞 ,直到有人唤醒。std::thread::park():阻塞当前线程,thread.unpark()唤醒此线thread对象线程,thread对象来自于thread::current() 获取当前线程句柄,或由 JoinHandle.thread() 获取。
  3. 交出时间片,std::thread::yield_now():“我自愿放弃剩余时间片,但依然处于就绪态”。通常用于**自旋锁(Spinlock)**或忙等待(Busy-wait)优化,避免单一线程过度占用 CPU 而导致其他线程饥饿(Starvation)。
Warning

换理解的就是可以提前打招呼。

Summary
  • condition.wait(mutex_guard).unwrap():原子性地释放传入的互斥锁(Mutex),并将当前线程阻塞在条件变量(的队列)上。当被唤醒后,它会重新(尝试)获取锁并继续执行。
  • 唤醒 (Condvar::notify_one(condition) / notify_all(condition)):唤醒等待在此条件变量上的一个线程(如果存在)。唤醒所有等待在此条件变量上的线程。

其他API

  • 获取当前线程句柄:thread::current(),返回 Thread 类型,可用于调用 unpark() 或获取线程名称。

  • 获取 ID:thread::current().id(),用于调试或哈希表键值。

  • 获取硬件并发数:thread::available_parallelism(),返回 CPU 核心数,常用于决定线程池大小(Rust 1.59+ 稳定)。

  • 作用域线程(Scoped Threads):thread::scope(|s| { s.spawn(...) })。允许子线程安全地借用父线程栈上的局部变量(而非必须 move),且保证在 scope 结束前所有线程自动 join,简化了生命周期管理。

线程间传送数据

线程间数据交流的传统方式是内存共享

但是内存共享需要在所有共享的线程做同步代码设计,不然就容易数据竞争。

信道

Summary

信道有两个组成部分:发送端(transmitter)变量和接收端(receiver)变量。
当发送端或接收端任一被全部丢弃时可以认为信道被关闭(closed)了。

请想象一下channel的概念,就像物理世界中的信道,电磁波的频道。

多生产者,多消费者信号

std::sync::mpsc是 multiple producer, signal consumer多生产者,单消费者
crossbeam_channel,多生产者,多消费者
还有众多第三方实现的channel,tokio的实现,flume的,等等。

标准库的实现:

std::mpsc::channel
mpsc::channel()返回一个元组,其中第一个元素是发送端,第二个元素是接收端

发送端send 方法

send要发送的值,并返回一个 Result<T, E> 类型,所以如果接收
端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。

接收端有两个有用的方法: recv 和 try_recv 。

recv: 阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值, recv 会在一个
Result<T, E> 中返回它。当信道发送端关闭, recv 会返回一个错误

try_recv 不会阻塞,相反它立刻返回一个 Result<T, TryRecvError> : Ok 值包含可用的信息,TryRecvError 是一个枚举,包含两种可能的错误:

  • TryRecvError::Empty: 表示通道当前为空,没有可用的消息。
  • TryRecvError::Disconnected: 表示通道的所有发送端(Sender)都已经被丢弃,通道已断开连接,不会再收到任何新消息。
值在信道的传递,是直接转移所有权

如果不转移所有权,将会导致数据竞争问题。

接收端rx实际上实现了迭代器trait

Receiver<T>实现了迭代器trait,每次next会阻塞,并返回T,如果error则返回None,迭代器结束。

Summary

crossbeam 通道

这个通道不得了,全面超越mpsc!

crossbeam是多生产者多消费者,但是这并不意味着他的性能更差,相关测试表明其性能优于标准库的通道!
.
提供了更丰富的API,send_timeout、is_empty、len、capacity 等实用方法,提供 bounded(cap)(有界)和 unbounded()(无界)两种创建方式。
.
允许使用select!()宏,同时等待多个通道操作(发送或接收),一旦其中任何一个就绪,就执行对应的分支。当多个操作同时就绪时,它会随机选择一个执行,避免了饥饿问题!
.
提供了多种便捷通道,after(duration),tick(duration),never(),zero()

特性std::sync::mpsccrossbeam-channel
并发模型多生产者单消费者 (MPSC)多生产者多消费者 (MPMC)
Receiver 可克隆❌ 否✅ 是
Sender/Receiver 是 Sync❌ 否✅ 是
发送者类型Sender 和 SyncSender统一的 Sender
select! 支持有限且不安全,已废弃强大、安全,支持超时/非阻塞
额外通道无after, tick, never, zero
性能一般优化更好,锁竞争更少

共享状态

共享状态实际上也是共享内存,但是这片共享内存有一套同步操作API

通常是锁结构,有同步锁,自旋锁等。

什么是原语?

原语也叫原子操作就是不可拆分、插入、打断的操作系列。编程语言保证了原语操作在多线程下的顺序性,使得其功能在多线程下也不受影响。

什么是CAS、什么是自旋?

CAS就是读取、计算、比较、覆盖。比较的时候如果发现版本变化了,就重新CAS。
自旋就相当于是等待,不让出CPU,疯狂等待,然后再让出cpu,理由是认为这个锁很快就会让出了,所以我不必休眠。

Summary
  1. 在使用数据之前,必须获取锁。
  2. 使用完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。

使用关联函数 new 来创建一个 Mutex<T> 。使用 lock 方法来获取锁,从而可以访问互斥器中的数据。这个调用会阻塞当前线程,直到我们拥有锁为止。

如果另一个线程拥有锁,并且那个线程 panic 了,则 lock 调用会失败(返回Result::Error)。

提供了内部可变性,就像 Cell 系列类型那样

[!tip] Mutex<T> 提供了内部可变性,就像 Cell 系列类型那样
Mutex<T> 也有造成死锁(deadlock)的风险:当某个操作需要锁住两个资源,而两个线程分别持有两个资源的其中一个锁时,它们会永远相互等待。

Summary

MutexGuard 智能指针实现了 Deref 来指向其内部数据;它也实现了Drop ,当 MutexGuard 离开作用域时,自动释放锁。

一旦获取了锁,就可以将返回值(命名为 num )视为一个其内部数据的可变引用了。

原子引用计数 只保护指针元数据的同步

[!summary] 原子引用计数 Arc<T> 只保护指针元数据的同步
就像mutex一样,Arc使用CAS等轻量操作来保护Arc指针的元数据计数的并发安全。

Arc只保护了元数据的安全,没有保证内部数据安全

也就是指针所指向的数据本身需要是mutex的,反正要有同步化操作来保护。

可扩展并发

处理并发的方案并不受标准库或语言所限:我们可以编写自己的或使用他人编写的并发特性。

使用 Send 和 Sync trait 可以实现扩展,但是这是后话unsafe的内容了。