시그널 다루기
커맨드라인 애플리케이션과 같은 프로세스는 운영체제가 보낸 시그널에 반응해야 합니다. 시그널의 가장 흔한 예시는 아마도, 일반적으로 프로세스를 종료시킬 때 쓰는 Ctrl+C일 것 입니다. 러스트 프로그램에서 시그널을 다루기 위해서는 시그널에 반응하는 방법뿐 아니라 시그널을 수신하는 방법에 대해서도 고민해 봐야 합니다.
운영체제 간 차이점
유닉스 시스템(리눅스, macOS, FreeBSD 등)에서 프로세스는 시그널을 받을 수 있습니다. 프로세스는 시그널을 받아서 프로그램이 정의한 방식으로 시그널을 처리하는 기본적인 방법(OS가 제공)으로 시그널에 반응할 수도 있고, 시그널을 통째로 무시할 수도 있습니다.
윈도우즈에는 시그널이 없습니다. 대신 콘솔 핸들러를 이용하여 이벤트가 발생했을 때 실행되는 콜백을 정의할 수 있습니다. 또한 윈도우즈에는 0으로 나누기, 잘못된 접근, 스택오버플로우 등 모든 종류의 시스템 예외를 처리할 수 있는 구조적 예외 처리도 있습니다.
Ctrl+C 다루기
ctrlc 크레이트는 이름 그대로의 일을 합니다. ctrlc는 다양한 플랫폼에 대해 사용자가 Ctrl+C을 눌렀을 때 프로그램이 반응할 수 있도록 만들어 줍니다. ctrlc의 주요 사용법은 아래와 같습니다:
use std::{thread, time::Duration};
fn main() {
ctrlc::set_handler(move || {
println!("received Ctrl+C!");
})
.expect("Error setting Ctrl-C handler");
// 다음 코드는 실제로 동작하며, Ctrl-C로 인터럽트될 수 있다.
// 이 예시에서는 몇 초간 기다린다.
thread::sleep(Duration::from_secs(2));
}
물론 이렇게 하면 별 도움이 되지 않습니다. 메시지를 출력할 뿐 프로그램을 종료시키지는 않으니까요.
실제 프로그램의 경우,
시그널 핸들러에서 변수를 설정해
프로그램 곳곳에서 상태를 체크하는 것이 좋습니다.
예를 들어,
시그널 핸들러에서 Arc<AtomicBool>
(스레드 간에 공유할 수 있는 불리언 타입)
변수를 설정하면, 루프(hot loop)를 돌거나
스레드를 대기할 때 그 값을 주기적으로
체크하면서 true가 되면 프로그램을
종료하도록 할 수 있습니다.
다른 시그널 다루기
ctrlc 크레이트는 Ctrl+C,
혹은 유닉스 시스템에서 SIGINT
(“인터럽트” 시그널)라고 불리는
시그널만 다룰 수 있습니다.
더 많은 유닉스 시그널에 반응하기 위해서는
signal-hook를 사용해야 합니다.
signal-hook의 설계는 이 블로그 글에
설명되어 있으며, 현재 가장 광범위한 커뮤니티 지원을 받는 라이브러리입니다.
여기 간단한 예시가 있습니다:
use signal_hook::{consts::SIGINT, iterator::Signals};
use std::{error::Error, thread, time::Duration};
fn main() -> Result<(), Box<dyn Error>> {
let mut signals = Signals::new(&[SIGINT])?;
thread::spawn(move || {
for sig in signals.forever() {
println!("Received signal {:?}", sig);
}
});
// 다음 코드는 실제로 동작하며, Ctrl-C로 인터럽트될 수 있다.
// 이 예시에서는 몇 초간 기다린다.
thread::sleep(Duration::from_secs(2));
Ok(())
}
채널 사용하기
변수를 설정하고, 프로그램이 그 변수를 체크하도록 만드는 대신 채널을 사용할 수 있습니다: 채널을 만들면 시그널을 수신할 때마다 시그널 핸들러가 채널로 값을 내보내 줍니다. 애플리케이션 코드에서는 한 채널과 다른 채널을 스레드 간의 동기화 지점으로 사용하게 됩니다. crossbeam-channel을 사용하면 아래와 같은 모습이 됩니다:
use std::time::Duration;
use crossbeam_channel::{bounded, tick, Receiver, select};
use anyhow::Result;
fn ctrl_channel() -> Result<Receiver<()>, ctrlc::Error> {
let (sender, receiver) = bounded(100);
ctrlc::set_handler(move || {
let _ = sender.send(());
})?;
Ok(receiver)
}
fn main() -> Result<()> {
let ctrl_c_events = ctrl_channel()?;
let ticks = tick(Duration::from_secs(1));
loop {
select! {
recv(ticks) -> _ => {
println!("working!");
}
recv(ctrl_c_events) -> _ => {
println!();
println!("Goodbye!");
break;
}
}
}
Ok(())
}
퓨쳐(futures)와 스트림 사용하기
tokio를 사용하고 있다면,
여러분은 이미 비동기 패턴과 이벤트 주도 설계를 적용하여
애플리케이션을 작성하고 있을 확률이 높습니다.
이때는 crossbeam의 채널을 직접 사용하지 않고
signal-hook의 tokio-support
기능을 사용할 수 있습니다.
이 기능을 이용하면 signal-hook의 Signals
타입에 대해
.into_async()
를 호출하여 futures::Stream
을 구현하는
새로운 타입을 얻을 수 있습니다.
첫 Ctrl+C 시그널을 처리하는 도중 또 다른 Ctrl+C 시그널을 수신했을 때
사용자가 Ctrl+C를 누르면 여러분의 프로그램은 몇 초 뒤 종료되거나 진행 상황을 알려줄 것입니다. 만약 그렇지 않으면 사용자는 Ctrl+C을 한 번 더 누를 것입니다. 이때 일반적인 동작은 애플리케이션을 즉시 종료하는 것입니다.