Rust 实现两个线程交替打印 0-100 的奇偶数

这是一个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

这两个问题暂时还没有实现思路,有想法欢迎留言。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容