【Rust 指南】并发编程|无畏并发的原因_rust

文章目录

  • ​​  前言​​
  • ​​1、线程​​
  • ​​1.1、通过 spawn 创建新线程​​
  • ​​1.2、join 方法​​
  • ​​2、move 强制所有权迁移​​
  • ​​3、使用消息传递跨线程传递数据​​
  • ​​3.1、Send 方法​​
  • ​​3.2 、Sync 方法​​

   安全高效的处理并发是 Rust 诞生的目的之一,主要解决的是服务器高负载承受能力。
并发(​​concurrent​​)的概念是指程序不同的部分独立执行,这与并行(​​parallel​​)的概念容易混淆,并行强调的是"同时执行",而并发往往会造成并行。

Rust 无畏并发:允许你编写没有细微 Bug 的代码,并在不引入新 Bug 的情况下易于重构


1、线程

线程(thread)是一个程序中独立运行的一个部分,不同于进程(process)的地方是线程是程序以内的概念,程序往往是在一个进程中执行的。

在有操作系统的环境中进程往往被交替地调度得以执行,线程则在进程以内由程序进行调度。

由于线程并发很有可能出现并行的情况,所以在并行中可能遇到的死锁、延宕错误常出现于含有并发机制的程序。

为了解决这些问题,很多其它语言(如 Java、C#)采用特殊的运行时(runtime)软件来协调资源,但这样无疑极大地降低了程序的执行效率。
C/C++ 语言在操作系统的最底层也支持多线程,且语言本身以及其编译器不具备侦察和避免并行错误的能力,这对于开发者来说压力很大,开发者需要花费大量的精力避免发生错误。

Rust 不依靠运行时环境,这一点像 C/C++ 一样,但 Rust 在语言本身就设计了包括所有权机制在内的手段来尽可能地把最常见的错误消灭在编译阶段,这一点其他语言不具备。
但这不意味着我们编程的时候可以不小心,迄今为止由于并发造成的问题还没有在公共范围内得到完全解决,仍有可能出现错误,并发编程时要尽量小心!

1.1、通过 spawn 创建新线程

使用 ​​thread::spawn​​ 函数可以创建新线程:

  • 参数:一个闭包(在新线程里运行的代码)
  • 示例:
use std::{thread, time::Duration};

fn main() {
// 新线程
thread::spawn(|| {
for i in 1..10{
println!("hi number {} from the spawn thread!",i);
thread::sleep(Duration::from_millis(1));
}
});
// 主线程
for i in 1..5{
println!("hi number {} from the main thread!",i);
thread::sleep(Duration::from_millis(1));
}
}

【Rust 指南】并发编程|无畏并发的原因_主线程_02

这个结果在某些情况下顺序有可能变化,但总体上是这样打印出来的。
此程序有一个子线程,目的是打印 9 行文字,主线程打印 4 行文字,但很显然随着主线程的结束,spawn 线程也随之结束了,并没有完成所有打印。

1.2、join 方法

​join​​ 方法可以使子线程运行结束后再停止运行程序:

use std::{thread, time::Duration};

fn main() {
let handle = thread::spawn(|| {
for i in 1..10{
println!("hi number {} from the spawn thread!",i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5{
println!("hi number {} from the main thread!",i);
thread::sleep(Duration::from_millis(20));
}
// 放在主线程最后
handle.join().unwrap();
}

【Rust 指南】并发编程|无畏并发的原因_原力计划_03

放到主线程最后,当主线程运行完毕,等子线程运行完毕,程序才结束

如果放在主线程之前,情况就会变化:

【Rust 指南】并发编程|无畏并发的原因_后端_04

这里是子线程执行完毕后才执行主线程,继而程序结束

2、move 强制所有权迁移

来看看常见的问题:

use std::thread;

fn main() {
let s = "hello";

let handle = thread::spawn(|| {
println!("{}", s);
});

handle.join().unwrap();
}

子线程中尝试使用当前函数的资源,这一定是错误的!因为所有权机制禁止这种危险情况的产生,它将破坏所有权机制销毁资源的一定性。

我们可以使用闭包的 ​​move​​ 关键字来处理:

use std::thread;

fn main() {
let s = "hello";
// 解决方法 move ||
let handle = thread::spawn(move || {
println!("{}", s);
});

handle.join().unwrap();
}

3、使用消息传递跨线程传递数据

消息传递是一种很流行且能保证安全并发的技术,线程通过彼此发送消息来进行通信。

​Go​​ 语言名言:不要用共享内存来通信,要用通信来共享内存

Rust 中一个实现消息传递并发的主要工具是通道(​​channel​​),通道有两部分组成,一个发送者(​​transmitter​​)和一个接收者(​​receiver​​)。

​std::sync::mpsc​​ 包含了消息传递的方法:

use std::thread;
use std::sync::mpsc;

fn main() {
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});

let received = rx.recv().unwrap();
println!("Got: {}", received);
}
// 运行结果:Got: hi

子线程获得了主线程的发送者 ​​tx​​​,并调用了它的 ​​send​​ 方法发送了一个字符串,然后主线程就通过对应的接收者 ​​rx​​ 接收到了。

3.1、Send 方法

  • 实现​​Send trait​​的类型可在线程间转移所有权
  • Rust 中几乎所有的类型都实现了Send
  • 但​​Rc<T>​​没有实现Send,它只用于单线程情景
  • 任何完全由Send类型组成的类型也被标记为Send
  • 除了原始指针之外,几乎所有的基础类型都是Send

3.2 、Sync 方法

  • 实现 Sync 的类型可以安全的被多个线程引用
  • 如果 ​​T​​ 是 Sync,那么 ​​&T​​ 就是 Send
  • 引用可以被安全的送往另一个线程
  • 基础类型都是 Sync
  • 完全由 Sync 类型组成的类型也是 Sync
  • 但,​​Rc<T>​​ 不是 Sync
  • ​RefCell<T>​​​和​​Cell<T>​​家族也不是 Sync
  • 而 ​​Mutex<T>​​是 Sync

最后要注意:手动来实现 Send​Sync​ 是不安全的,需要非常谨慎的使用。