如何防止线程在视觉上混淆彼此的输出?

How to prevent threads to visually mix up each others' output?

我有一个程序 运行 两个线程,其中一个将状态消息打印到控制台,另一个接受用户输入。但是,因为它们都使用相同的控制台,所以如果我在另一个线程打印时使用一个线程中途键入命令,它将采用我已经用它编写的内容(仅在视觉上 - 命令仍将正确执行)。

这是代码示例,如果您尝试在控制台中键入内容,它将不断受到第二个线程的干扰。

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

fn main() {
    thread::spawn(move || {
        loop {
            println!("Interrupting line");
            thread::sleep(time::Duration::from_millis(1000));
        };
    });
    loop {
        let mut userinput: String = String::new();
        io::stdin().read_line(&mut userinput);
        println!("{}",userinput)
    }
}

现在,这就是尝试在控制台中键入 "i am trying to write a full sentence here" 时控制台最终的样子:

Interrupting line
i aInterrupting line
m trying Interrupting line
to write a fInterrupting line
ull senInterrupting line
tence hereInterrupting line

i am trying to write a full sentence here

Interrupting line
Interrupting line

如您所见,当第二个线程循环并打印 "Interrupting line" 时,我在控制台中写入的任何内容都与该行一起传送。理想情况下,当我正在打字时,它看起来像这样(无论打字需要多长时间):

Interrupting line
Interrupting line
Interrupting line
i am trying to 

然后,当我完成输入并按回车键后,它会看起来像这样:

Interrupting line
Interrupting line
Interrupting line
i am trying to write a full sentence here
i am trying to write a full sentence here

第一句是我实际输入的内容,第二句是将我输入的内容打印回控制台。

有没有一种方法可以将行打印到控制台,而不会导致任何正在进行的用户输入被打印消息破坏?

正如我们在上面的评论部分中提到的,您很可能希望使用外部库来处理每个终端的内部功能。

然而,与上面讨论的不同,对于这样一个简单的 "UI",您甚至可能不需要 tui,您可以使用 termion(实际的板条箱 tui在引擎盖下使用)。

下面的代码片段完全符合您上面描述的内容,甚至比上面描述的还要多一点。但这只是一个粗略的初步实现,里面还有很多东西需要进一步完善。 (例如,您可能希望在程序 运行ning 时处理终端的调整大小事件,或者您希望优雅地处理中毒的互斥锁状态等)

因为下面的代码片段很长,所以让我们分成小的、易于理解的块来过一遍。

首先,让我们从无聊的部分开始,我们将在整个代码中使用所有导入和一些类型别名。

use std::{
    time::Duration,
    thread::{
        spawn,
        sleep,
        JoinHandle,
    },
    sync::{
        Arc,
        Mutex,
        TryLockError,
        atomic::{
            AtomicBool,
            Ordering,
        },
    },
    io::{
        self,
        stdin,
        stdout,
        Write,
    },
};

use termion::{
    terminal_size,
    input::TermRead,
    clear,
    cursor::Goto,
    raw::IntoRawMode,
};

type BgBuf = Arc<Mutex<Vec<String>>>;
type FgBuf = Arc<Mutex<String>>;
type Signal = Arc<AtomicBool>;

那不碍事,我们可以专注于我们的 background-thread。这是所有 "interrupting" 行应该去的地方。 (在此片段中,如果您按 RETURN,则输入的 "command" 也将添加到这些行中,以演示线程间通信。)

为了便于调试和演示,对行进行了索引。由于后台线程实际上只是一个辅助线程,它不像处理用户输入的主要线程那样激进(foreground-thread) 所以它只使用 try_lock。因此,最好使用线程本地缓冲区来存储在共享缓冲区不可用时无法放入共享缓冲区的条目,这样我们就不会错过任何条目。

fn bg_thread(bg_buf: BgBuf,
             terminate: Signal) -> JoinHandle<()>
{
    spawn(move ||
    {
        let mut i = 0usize;
        let mut local_buffer = Vec::new();
        while !terminate.load(Ordering::Relaxed)
        {
            local_buffer.push(format!("[{}] Interrupting line", i));

            match bg_buf.try_lock()
            {
                Ok(mut buffer) =>
                {
                    buffer.extend_from_slice(&local_buffer);
                    local_buffer.clear();
                },
                Err(TryLockError::Poisoned(_)) => panic!("BgBuf is poisoned"),
                _ => (),
            }

            i += 1;
            sleep(Duration::from_millis(1000));
        };
    })
}

然后是我们的前台线程,它读取用户的输入。它必须在一个单独的线程中,因为它等待来自用户的按键(又名事件),而当它这样做时它会阻塞它的线程。

正如您可能注意到的那样,两个线程都使用 terminate(共享的 AtomicBool)作为信号。后台线程和主线程只读它,而这个前台线程写它。因为我们在前台线程中处理所有键盘输入,自然这是我们处理 CTRL + C 中断的地方,因此我们使用 terminate 如果我们的用户想要退出,则向其他线程发出信号。

fn fg_thread(fg_buf: FgBuf,
             bg_buf: BgBuf,
             terminate: Signal) -> JoinHandle<()>
{
    use termion::event::Key::*;
    spawn(move ||
    {
        for key in stdin().keys()
        {
            match key.unwrap()
            {
                Ctrl('c') => break,
                Backspace =>
                {
                    fg_buf.lock().expect("FgBuf is poisoned").pop();
                },
                Char('\n') =>
                {
                    let mut buf = fg_buf.lock().expect("FgBuf is poisoned");
                    bg_buf.lock().expect("BgBuf is poisoned").push(buf.clone());
                    buf.clear();
                },
                Char(c) => fg_buf.lock().expect("FgBuf is poisoned").push(c),
                _ => continue,
            };
        }

        terminate.store(true, Ordering::Relaxed);
    })
}

最后但并非最不重要的一点是我们的主线程。我们在这里创建了三个线程共享的主要数据结构。我们将终端设置为 "raw" 模式,这样我们就可以手动控制屏幕上显示的内容,而不是依赖于一些内部缓冲,因此我们可以实现 clipping 机制。

我们测量终端的大小 window 以确定我们应该从后台缓冲区打印多少行。

在每个成功的帧渲染之前,我们清除屏幕,然后打印出背景缓冲区的最后 n 个条目,然后将用户输入打印为最后一行。然后为了最终让这些东西出现在屏幕上,我们刷新 stdout.

如果我们收到终止信号,我们会清理其他两个线程(即等待它们完成),清除屏幕,重置光标位置,并向我们的用户说再见。

fn main() -> io::Result<()>
{
    let bg_buf = Arc::new(Mutex::new(Vec::new()));
    let fg_buf = Arc::new(Mutex::new(String::new()));
    let terminate = Arc::new(AtomicBool::new(false));

    let background = bg_thread(Arc::clone(&bg_buf),
                               Arc::clone(&terminate));
    let foreground = fg_thread(Arc::clone(&fg_buf),
                               Arc::clone(&bg_buf),
                               Arc::clone(&terminate));

    let mut stdout = stdout().into_raw_mode().unwrap();

    let (_, height) = terminal_size().unwrap();

    while !terminate.load(Ordering::Relaxed)
    {
        write!(stdout, "{}", clear::All)?;

        {
            let entries = bg_buf.lock().expect("BgBuf is poisoned");
            let entries = entries.iter().rev().take(height as usize - 1);
            for (i, entry) in entries.enumerate()
            {
                write!(stdout, "{}{}", Goto(1, height - i as u16 - 1), entry)?;
            }
        }

        {
            let command = fg_buf.lock().expect("FgBuf is poisoned");
            write!(stdout, "{}{}", Goto(1, height), command)?;
        }

        stdout.flush().unwrap();
        sleep(Duration::from_millis(50));
    }

    background.join().unwrap();
    foreground.join().unwrap();

    writeln!(stdout, "{0}{1}That's all folks!{1}", clear::All, Goto(1, 1))
}

如果我们将所有这些东西放在一起,编译它并运行它,我们可以得到以下输出:

[0] Interrupting line
[1] Interrupting line
[2] Interrupting line
[3] Interrupting line
This is one command..
[4] Interrupting line
[5] Interrupting line
..and here's another..
[6] Interrupting line
[7] Interrupting line
..and it can do even more!
[8] Interrupting line
[9] Interrupting line
Pretty cool, eh?
[10] Interrupting line
[11] Interrupting line
[12] Interrupting line
[13] Interrupting line
I think it is! :)