这是一个JAVA的面试题,如果使用RUST能否实现呢?
面试场景
面试官: JAVA多线程了解吗?你给我写一个,起两个线程交替打印 0-100 的奇偶数。
小黄:啊?
面试官:就是有两个线程,一个线程打印奇数另一个打印偶数,它们交替输出,类似:
偶线程:0 奇线程: 1 偶线程:2 ...... 奇线程: 99 偶线程:100
小黄:啊?
面试官:......嗯,好的。回去等通知吧。
解说
遇到这种突如其来的面试题,有时候会让人无从下手。尽管可能你学习过多线程的知识,但是面试官抛一个问题过来,短时间内可能想不出如何使用这些知识来解决这个具体的问题。其实这个问题考察的知识点并不难,但是如果准备面试的时候没有看过这种题,一时间还是比较难想出解决方案来的,而且这种题往往是让面试者手写代码。
回到题目上来。首先是两个线程,其次是交替打印。JAVA的思路是联系到线程之间的通信,以及加锁,哪个线程拿到锁就打印,然后释放锁让另一个线程获取锁。两个线程轮流拿到锁,实现交替打印的效果。在RUST中有没相关的实现方式呢?
讨巧的方案
比较容易想的一个方案是,要输出的时候判断一下当前需要输出的数是不是自己要负责打印的值,如果是就输出,不是就暂停当前线程。
use std::thread;
use std::time::Duration;
fn main() {
// 线程1
let t1 = thread::spawn(|| {
// 循环100次 偶数就打印否则使用 sleep 暂停当前线程
for i in 1..100 {
if i % 2 == 0 {
print!("偶线程: {} ", i);
} else {
thread::sleep(Duration::from_millis(1));
}
}
});
// 线程2
let t2 = thread::spawn(|| {
for i in 1..100 {
if i % 2 != 0 {
print!("奇线程: {} ", i);
} else {
thread::sleep(Duration::from_millis(1));
}
}
});
t1.join().unwrap();
t2.join().unwrap();
}
输出如
奇线程: 1 偶线程: 2 奇线程: 3 偶线程: 4 奇线程: 5 偶线程: 6 奇线程: 7 奇线程: 9 偶线程: 8 奇线程: 11 偶线程: 10 奇线程: 13 ......
从输出上看,是实现了题目上的部分要求,两个线程,一个打印奇数,一个打印偶数。但只是用了一个讨巧的方式实现了分别打印,而并没有交替轮流打印,有乱序,明显没有达到面试官想考察的考点上。代码中两个线程是分离的并没有任何数据同步方式。那RUST中如何进行线程同步以及数据交换呢?
Channel通信通道消息传递实现方案
在 RUST 中线程通信和同步相关概念有:
- 互斥锁 (Mutex)
- 屏障(Barrier)
- 条件变量(Condition Variable)
- Channel通信通道(mpsc)
查阅了很多资料,其中 Channel 通信通道是比较推荐的方式。
在Rust中,线程就是CSP(Communicating Sequential Processes,通信顺序进程)进程,而通信通道就是Channel。在Rust标准库的std::sync::mpsc
模块中为线程提供了Channel机制,其具体实现实际上是一个多生产者单消费者(Multi Producer Single Consumer,MPSC)的先进先出(FIFO)队列。线程通过Channel进行通信,从而可以实现无锁并发。
根据消息传递机制,创建两个通信通道和两个线程,一个线程获取到数据进行加1操作,再发送到一个通信通道,另一个线程相同操作只是数据使用另一个通道发送,直到数据大于100结束循环。
代码实现:
use std::thread;
use std::sync::{mpsc, Arc, Mutex};
use std::time::Duration;
const SLEEP: u64 = 10;
fn main() {
let control = Arc::new(Mutex::new(true));
let (tx1, rx1) = mpsc::channel::<i32>();
let (tx2, rx2) = mpsc::channel::<i32>();
let t1 = thread::spawn(move || {
loop {
let data = rx1.recv().unwrap_or(0);
if data > 100 {
break;
}
print!("偶线程:{} ", data);
if let Err(res) = tx2.send(data + 1) {
println!("=== {:?}", res);
break;
}
thread::sleep(Duration::from_millis(SLEEP));
}
});
let t2 = thread::spawn(move || {
loop {
let mut ctl = control.lock().unwrap();
if *ctl {
*ctl = false;
tx1.send(0).unwrap();
}
let data = rx2.recv().unwrap_or(0);
if data > 100 {
break;
}
print!("奇线程:{} ", data);
if let Err(res) = tx1.send(data + 1) {
println!("---- {:?}", res.to_string());
break;
}
thread::sleep(Duration::from_millis(SLEEP));
}
});
t2.join().unwrap();
t1.join().unwrap();
}
输出结果:
偶线程:0 奇线程:1 偶线程:2 奇线程:3 偶线程:4 ...... 奇线程:99 偶线程:100
本方案实现中使用了线程间通信和数据共享互斥锁,并且输出结果是交替轮流打印输出,完全满足面试题目要求。
代码优化
两个线程主逻辑公用部分抽离,最终代码如下:
use std::thread;
use std::sync::{mpsc, mpsc::{Sender, Receiver}, Arc, Mutex};
use std::time::Duration;
// 线程延时处理时间
const SLEEP: u64 = 10;
/**
* 线程操作共有逻辑处理
*
* @param name: String 线程名称
* @param rx: Receiver<i32> 信号接收
* @param tx: Sender<i32> 信号发送
* @param control Arc<Mutex<bool>> 控制器
* @param send_start bool 是否发送初始数据
*/
fn handle (
name: String,
rx: Receiver<i32>,
tx: Sender<i32>,
control: Arc<Mutex<bool>>,
send_start: bool,
) {
// 无限循环
loop {
// 初始运行需要发送数据 0
if send_start {
// 获取控制器锁
let mut ctl = control.lock().unwrap();
if *ctl {
*ctl = false;
// 发送数据 0
tx.send(0).unwrap();
}
}
// 从信息通道接收数据
let data = rx.recv().unwrap_or(0);
// 数据超过100停止循环 会自动停止当前线程
if data > 100 {
break;
}
// 每次接收到数据 进行加1操作再发送到另一个信号通道
if let Ok(_) = tx.send(data + 1) {
// 输出结果
print!("{}:{} ", name, data);
} else {
// 如果信号通道关闭停止当前循环
break;
}
// 当前线程延时处理 运行看起来慢一些 无实际意义
thread::sleep(Duration::from_millis(SLEEP));
}
}
fn main() {
// 共享控制器 第一次运行需要先发送一次初始数据 0
let control = Arc::new(Mutex::new(true));
// 信号通道1
let (tx1, rx1) = mpsc::channel::<i32>();
// 信号通道2
let (tx2, rx2) = mpsc::channel::<i32>();
let c1 = Arc::clone(&control);
let name1 = String::from("偶线程");
// 生成线程1
let t1 = thread::spawn(move || {
handle(name1, rx1, tx2, c1, false);
});
let c2 = Arc::clone(&control);
let name2 = String::from("奇线程");
// 生成线程2
let t2 = thread::spawn(move || {
handle(name2, rx2, tx1, c2, true);
});
t2.join().unwrap();
t1.join().unwrap();
}
至此,本题解决。
扩展
两个线程使用通信实现交替打印的问题解决了,如果
- 要使用JAVA一样获取锁再释放锁应该如何实现呢?
- 或是有3个线程进行交替打印又如何实现呢?即:
线程1:1 线程2:2 线程3:3 线程1:4 线程2:5 线程3:6
这两个问题暂时还没有实现思路,有想法欢迎留言。