rust static 변수 사용

wangki·2025년 1월 29일
0

Rust

목록 보기
8/54

rust에서 전역변수

전역 변수는 프로그램 어디에서나 접근할 수 있다. 프로그램을 만들며 최대한 지양 해야하는 부분이라고 생각을 한다. 그러나 어쩔 수 없이 사용해야 하는 경우가 있다.
rust에서는 데이터를 동시에 읽고 쓰는 것을 컴파일러가 금지한다. 전역변수는 기본적으로 immutable하다. 따라서 Mutex를 활용하면 데이터에 대한 접근을 잠금을 통해 안전하게 이루어지도록 한다.

전역 변수 사용 이유

cpal 크레이트를 활용해 녹음 기능을 구현하는데 녹음한 데이터가 Vec<f32> 형태로 저장이 된다. 데이터를 어딘가에 저장을 해야 하는 상황이 발생한다.

  1. 파일 형태로 데이터를 저장 후 출력 시 파일을 읽어온다.
  2. 전역 변수 형태로 데이터를 저장한 뒤 출력 시 호출한다.

구현하려는 기능이 파일은 필요 없을 듯하여 2번으로 전역 상태로 저장하기로 했다.
녹음하는 시간이 3초 이내이므로 전역으로 데이터를 가지고 있는 것이 부담스럽지 않기 때문이다. 그러나 녹음 시간이 길어지거나 이전 녹음을 듣기 위해서는 파일 형태로 저장을 고려해 봐야겠다.

static AUDIO_DATA: LazyLock<Mutex<Vec<f32>>> = LazyLock::new(|| {
    Mutex::new(Vec::new())
});

LazyLock을 활용하여 지연 초기화를 사용했다. 최초 AUDIO_DATA가 녹음이 되어 사용될 때 초기화가 되도록 했다. Mutex를 통해 데이터의 동시 접근을 방지하였다.

closure앞에 move 키워드를 붙이면 소유권을 이동시킨다는 의미이다.

예를 들어보겠다.

    let x = 10;

    let closure_no_move = || {
        println!("x : {}", x);
    };

    closure_no_move(); 
    
    // x를 호출 가능 
    println!("x : {}", x);

변수 xclosure를 통해서 캡처를 하였다. 이 후 x를 사용하는데 문제가 없다.

그러면 move키워드를 붙여보겠다.

    let x = 10;

    let closure_move = move || {
        println!("x : {}", x);
    };

    closure_move(); 
    
    // x를 호출 가능 
    println!("x : {}", x);

move 키워드를 붙인 후 x를 캡처했지만 아무 이상없이 컴파일이 되었다.
그 이유는 copy trait에 있다. 만약 캡처한 변수의 타입이 copy trait를 구현하고 있다면 내부적으로 값복사가 이루어지기때문에 소유권의 이동이 일어나지 않는다.

    let text = "테스트".to_string();

    let closure_move = move || {
        println!("text : {}", text);
    };

    closure_move(); 
    
    // 컴파일 에러 발생 ! 
    println!("text : {}", text);

변수 text는 힙에 할당된 데이터를 가르키는 변수이다.
즉, move를 통해서 캡처 시 소유권이 이동된다.

그렇다면 어떻게 소유권을 넘기면서도 같은 데이터를 안전하게 다룰 수 있을까?
정답은 Rc<RefCell<T>>를 활용하는 것이다. 싱글 스레드 환경에서는 참조 카운트 값을 증가시켜 안전하게 데이터 공유와 수정이 가능하다. 그러나 멀티 스레드 환경에서는 Arc<Mutex<T>> 를 사용하는 것이 적합하다.

이야기가 조금 샛길로 샜는데 위와 같은 이유로 안전하게 다루기위해서 전역 데이터를
Mutex<Vec<f32>> 로 선언하였다. Arc를 사용하지 않은 이유는 static 변수로 선언하면 전역에서 사용가능하기 때문이다.

cpal을 활용해 input stream을 만들어야 한다. 아래 코드를 보자

*stream_clone = Some(device.build_input_stream(
        &config.config(), 
        move |data: &[f32], _: &cpal::InputCallbackInfo| {
            let mut record_audio_vec = record_audio_data_clone.lock().unwrap();
            for &sample in data.iter() {
                record_audio_vec.push(sample);
            }
        }, 
        |err| {
            eprintln!("Error during recording: {:?}", err);
        }, 
        None
        ).unwrap()
    );

build_input_stream의 두번 째 매개변수로 FnMut(&[T], &InputCallbackInfo) + Send + 'static, 를 넘겨야한다.
Send는 안전하게 다른 스레드로 이동 가능하다는 것을 나타내는 것이다. 즉, 싱글 스레드환경에서만 동작된다는 것이 보장되지 않는다.

static은 클로저와 그 내부에 캡처된 값들이 프로그램 전체 실행동안 유효해야 함을 의미한다. 데이터의 소유권을 클로저로 이동하거나, 데이터가 'static 라이프 타임을 가져야 한다.

let mut record_audio_vec = AUDIO_DATA.lock().unwrap();

위와 같이 AUDIO_DATA를 lock을 통해서 경쟁하지 않도록 보장하였다.

녹음을 완료한 후 음성 데이터를 저장한 AUDIO_DATA에 다시 접근하여 output하였다.

    let audio_data = AUDIO_DATA.lock().unwrap().clone();
    let mut audio_data_iter = audio_data.into_iter();

데이터에 접근 후 clone을 하여 소유권을 넘기지 않고 데이터를 깊은 복사하였다.

성공적으로 출력 후 다시 녹음할 경우

    {
        AUDIO_DATA.lock().unwrap().clear();
    }

위와 같이 스코프 내부에서 접근하여 초기화를 진행하였고, 스코프를 벗어나면 lock이 해제되도록 구현하였다.

결론

프로그래밍에서 최대한 전역변수를 지양해야 하지만 꼭 필요한 경우가 있다.
tauricommand를 통해서 rust 코드를 호출하여 녹음해야 하는 경우 전역변수를 사용하는 것이 하나의 방법이라고 생각한다. 사용해야 한다면 스레드 안전성 보장을 위해 Mutex, RwLock, LazyLock등을 활용해야 한다.

더 좋은 방법이 있는지 꾸준하게 찾아보고 공부할 것이다.

0개의 댓글