gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-29 08:07:04 26 4
gpt4 key购买 nike

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

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

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)
}
}

现在,当尝试在控制台中输入“我正在尝试在这里写一个完整的句子”时,控制台最终看起来是这样的:

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 来摆脱困境(实际的 crate tui 在后台使用)。

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

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

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

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>;

这不碍事,我们可以专注于我们的后台线程。这是所有“中断”线应该去的地方。 (在此代码段中,如果您按 RETURN,则输入的“命令”也将添加到这些行中,以演示线程间通信。)

为了便于调试和演示,对行进行了索引。由于后台线程实际上只是一个辅助线程,它不像处理用户输入的主要线程(前台线程 ) 所以它只使用 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);
})
}

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

我们测量终端窗口的大小以确定我们应该从背景缓冲区中打印出多少行。

在每个成功的帧渲染之前,我们清除屏幕,然后打印出背景缓冲区的最后 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! :)

关于multithreading - 如何防止线程在视觉上混淆彼此的输出?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57520623/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com