上文中的网络客户端中实现了一个基本的线程应用。线程,几乎在所有的高级编程语言中都是不可避免的难点和重点。那么到底什么是多线程?这就需要说明什么是并行什么并发?并发的意义就是在于多个事件在同时(或者说接近同时)发生;并行是只多个事件完全在不同处理单元上同时进行;映射到计算机上,并发更接近于多线程在单CPU上的的应用而并行更接近于多核甚至是多CPU的执行应用。
但是无论是并发还是并行编程,都是多线(进)程的广义上的应用。
做为新出现的一门高级语言,RUST当仁不让的要支持并发和并行的编程,也就是说一定会提供多线程的开发应用。
RUST中的多线程,
来看一下《Rust Prime》中提供的两个例子:
use std::thread;
fn main() {
// 创建一个线程
let new_thread = thread::spawn(move || {
println!("I am a new thread.");
});
// 等待新建线程执行完成
new_thread.join().unwrap();
}
use std::thread;
fn main() {
// 创建一个线程,线程名称为 thread1, 堆栈大小为4k
let new_thread_result = thread::Builder::new()
.name("thread1".to_string())
.stack_size(4*1024*1024).spawn(move || {
println!("I am thread1.");
});
// 等待新创建的线程执行完成
new_thread_result.unwrap().join().unwrap();
}
这两个例子,一种是使用基本的堆栈变量大小,另外一种可以自行定制,虽然调用的接口不同,但是结果都是使用spawn来完成“下蛋”产生新的线程。
正所谓有生必有死,线程有创建就会有线程的销毁,做其它高级语言中,如c++、Java等都提供过类似Cancel、Stop等相关函数,但在新版本中,都无一例外的把它们给删除了。线程的创建容易理解,但是线程的退出,特别是异常退出,始终是编程的一个痛点问题。一般来说,线程推荐是使用自己运行完毕后自行退出整个线程。但总有些情况下需要线程被动退出。这时候,就得需要一种通知机制,让主线程等待相关子线程结束后再完全退出,保证资源访问的安全和相关并发执行的顺序的保证,防止出现交发执行的线程访问资源被提前终止或者释放的情况。
也就是说,在高级语言都要提供这种机制,保证线程可以协调顺序,在RUST中也有,即上面代码中的unwrap().join().unwrap()或者join().unwrap()的代码。
在多线程编程中,同步和多线程间的数据交换始终是个问题。传统的多线程间数据交换有很多种方式,如静态变量、全局变量、内存映射、文件、socket、管道、消息等。在RUST中也有类似的机制即静态变量和堆。
1、数据交换
看一下下面的例子:
use std::thread;
static mut VAR: i32 = 5;
fn main() {
// 创建一个新线程
let new_thread = thread::spawn(move|| {
unsafe {
println!("static value in new thread: {}", VAR);
VAR = VAR + 1;
}
});
// 等待新线程先运行
new_thread.join().unwrap();
unsafe {
println!("static value in main thread: {}", VAR);
}
}
再看一个共享内存的:
use std::thread;
use std::sync::Arc;
fn main() {
let var : Arc = Arc::new(5);
let share_var = var.clone();
// 创建一个新线程
let new_thread = thread::spawn(move|| {
println!("share value in new thread: {}, address: {:p}", share_var, &*share_var);
});
// 等待新建线程先执行
new_thread.join().unwrap();
println!("share value in main thread: {}, address: {:p}", var, &*var);
}
说明:以上代码均出自《RustPrimer》,非常感谢。
在Rust进行堆的多线程操作,必须使用std::boxed::Box,它又分为可跨线程和不可以跨线程的即std::sync::Arc和std::rc::Rc。如果在多线程中应用后者,编译器会提示错误。
2、消息
Rust中提供了消息这个机制也就是Channel,这玩意儿是不是有点类似Go语言中的通道。而它又分为同步通道和异步通道,看下面的同步通道例子:
use std::sync::mpsc;
use std::thread;
fn main() {
// 创建一个同步通道
let (tx, rx): (mpsc::SyncSender, mpsc::Receiver) = mpsc::sync_channel(0);
// 创建线程用于发送消息
let new_thread = thread::spawn(move || {
// 发送一个消息,此处是数字id
println!("before send");
tx.send(1).unwrap();
println!("after send");
});
println!("before sleep");
thread::sleep_ms(5000);
println!("after sleep");
// 在主线程中接收子线程发送的消息并输出
println!("receive {}", rx.recv().unwrap());
new_thread.join().unwrap();
}
再看下异步通道的例子:
use std::sync::mpsc;
use std::thread;
// 线程数量
const THREAD_COUNT :i32 = 2;
fn main() {
// 创建一个通道
let (tx, rx): (mpsc::Sender, mpsc::Receiver) = mpsc::channel();
// 创建线程用于发送消息
for id in 0..THREAD_COUNT {
// 注意Sender是可以clone的,这样就可以支持多个发送者
let thread_tx = tx.clone();
thread::spawn(move || {
// 发送一个消息,此处是数字id
thread_tx.send(id + 1).unwrap();
println!("send {}", id + 1);
});
}
thread::sleep_ms(2000);
println!("wake up");
// 在主线程中接收子线程发送的消息并输出
for _ in 0..THREAD_COUNT {
println!("receive {}", rx.recv().unwrap());
}
}
其它就是把c++机制中的同步机制和异步机制的另外一种语言的变形(在c++11中提供的async,以及其提供的Future和Promise、std::packaged_task等),从语言的演进角度看,就可以清楚的看明白这些技术的由来和发展过程,这样能够从宏观角度来看技术的本质,更容易理解和分析相关技术并彻底将其掌握。
3、同步
线程和进程的同步,有很多种方法,在Rust中提供了原子变量、时间等待以及锁,主要包括:
时间处理相关:std::thread::sleep,std::thread::sleep_ms,std::thread::park_timeout,std::thread::park_timeout_ms;
线程调度:std::thread::yield_now;
线程等待:std::thread::JoinHandle::join,std::thread::park
锁和异步:std::sync::Mutex::lock;std::thread和std::sync。
线程通知 :std::sync::Condvar::wait和std::sync::Condvar::notify_one,std::sync::Condvar::notify_all,std::sync::Barrier
其实还有很多相关的线程操作,可以去查看一下相关的帮助文档,Rust正在处于快速的迭代期,要注意其技术的细节性演进,包括一些已经注明在新版本中不再推荐使用的方法和接口。
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
fn main() {
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair2 = pair.clone();
// 创建一个新线程
thread::spawn(move|| {
let &(ref lock, ref cvar) = &*pair2;
let mut started = lock.lock().unwrap();
*started = true;
cvar.notify_one();
println!("notify main thread");
});
// 等待新线程先运行
let &(ref lock, ref cvar) = &*pair;
let mut started = lock.lock().unwrap();
while !*started {
println!("before wait");
started = cvar.wait(started).unwrap();
println!("after wait");
}
}
再看一下原子类型:
use std::thread;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
let var : Arc = Arc::new(AtomicUsize::new(5));
let share_var = var.clone();
// 创建一个新线程
let new_thread = thread::spawn(move|| {
println!("share value in new thread: {}", share_var.load(Ordering::SeqCst));
// 修改值
share_var.store(9, Ordering::SeqCst);
});
// 等待新建线程先执行
new_thread.join().unwrap();
println!("share value in main thread: {}", var.load(Ordering::SeqCst));
}
原子类型其实就是一种以CAS方式实现的无锁编程,但是在RUST的原子类型,底层实现的原理应该是类似于内存序的控制原理,有机会看一下源码,再分析。
在RUST中,大量的库封装,使得直接操作锁等比较难于控制的过程的编程都被抽象到应用层的封装上,使得在c++等语言中的并行编程在RUST中表现的非常简单,看一下例子:
extern crate rayon;
use rayon::prelude::*;
fn main() {
let mut colors = [-20.0f32, 0.0, 20.0, 40.0,
80.0, 100.0, 150.0, 180.0, 200.0, 250.0, 300.0];
println!("original: {:?}", &colors);
colors.par_iter_mut().for_each(|color| {
let c : f32 = if *color < 0.0 {
0.0
} else if *color > 255.0 {
255.0
} else {
*color
};
*color = c / 255.0;
});
println!("transformed: {:?}", &colors);
}
当看到use rayon::prelude这个引用时,就明白了,这个是一个并行相关的东东。使用库有使用库的好处,那就是上层应用编程变得简单,但也同样变得无聊;对于小菜鸟来说,是个福音。但这种情况下,再怎么懂并行编程,也只是停留在了表面上,真正遇到问题时,还是会蒙圈,个人认为真正的掌握并行编程还是要深入到语言的底层认真查看,真正明白封装的含义。
正所谓,知其然,然后知其所以然。
上述所有代码全部引自《RustPrimer》,非常感谢!
在前面的文章中提到过,在RUST中也提供了协程的开发应用,在接下来的篇章内,会找机会对协程在RUST中的使用方式和应用场景进行一下分析。多线程是基础,协程是未来的发展方向,目前看来不会有什么更改。
努力吧,归来的少年!