정적 아이템
정적 아이템 (Static items)
정적 아이템(static item)은 상수(constant)와 비슷하지만 결정적인 차이가 있어요. 프로그램에 저장 공간(allocation)을 만들고 초기화 식(initializer expression)으로 그 값을 채워 넣는다는 점이죠. 이 글에서는 static의 동작 방식과 제약을 살펴볼게요.
출처: Rust Reference
본문
정적 아이템은 프로그램에 있는 저장 공간으로, 초기화 식으로 초기화돼요. 그 static을 가리키는 모든 참조와 원시 포인터는 같은 저장 공간을 가리켜요.
구문은 다음과 같아요.
StaticItem → ItemSafety? static mut? IDENTIFIER : Type ( = Expression )? ;
수명 (Lifetime)
정적 아이템은 static 수명을 가져요. 이는 Rust 프로그램에서 다른 모든 수명보다 오래 살아요. 정적 아이템은 프로그램 종료 시 drop을 호출하지 않아요.
저장 공간의 분리 (Storage disjointness)
static의 크기가 최소 1바이트라면, 이 저장 공간은 다른 모든 static 저장 공간, 힙 할당, 스택 할당 변수들과 분리(disjoint)돼요. 다만 불변 static 아이템의 저장 공간은 고유한 주소가 없는 할당들(예: promoted와 const 아이템)과 겹칠 수 있어요.
네임스페이스 (Namespace)
static 선언은 그것이 위치한 모듈이나 블록의 값 네임스페이스(value namespace)에 정적 값을 정의해요.
초기화 (Initialization)
static 초기화 식은 컴파일 타임에 평가되는 상수 표현식이에요. static 초기화 식은 다른 static을 참조하고 읽을 수 있어요. 가변 static을 읽을 때는 그 static의 초기 값을 읽어요.
읽기 전용 메모리 (Read-only memory)
내부 가변성(interior mutability)이 없는 타입을 담는 mut가 아닌 static 아이템은 읽기 전용 메모리에 배치될 수 있어요.
안전성과 제약 (Safety and restrictions)
static에 대한 모든 접근은 안전하지만, 몇 가지 제약이 있어요.
- 동기화: 스레드 안전 접근을 위해 타입에
Sync트레이트 바운드가 있어야 해요. - 초기화 식 생략:
extern블록 안에서는 초기화 식을 생략해야 하고, 자유(free) static 아이템에는 반드시 제공해야 해요. - 안전성 한정자:
safe와unsafe한정자는 의미적으로extern블록에서만 허용돼요.
static과 제네릭 (Statics & generics)
제네릭 스코프(예: blanket 또는 default 구현 안)에서 정의된 static 아이템은 정확히 하나의 static 아이템으로 정의돼요. 마치 static 정의를 현재 스코프에서 모듈로 끄집어낸 것처럼 말이죠. 단형화(monomorphization)당 하나씩 생기지 않아요.
use std::sync::atomic::{AtomicUsize, Ordering};
trait Tr {
fn default_impl() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
println!("default_impl: counter was {}", COUNTER.fetch_add(1, Ordering::Relaxed));
}
fn blanket_impl();
}
struct Ty1 {}
struct Ty2 {}
impl<T> Tr for T {
fn blanket_impl() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
println!("blanket_impl: counter was {}", COUNTER.fetch_add(1, Ordering::Relaxed));
}
}
fn main() {
<Ty1 as Tr>::default_impl();
<Ty2 as Tr>::default_impl();
<Ty1 as Tr>::blanket_impl();
<Ty2 as Tr>::blanket_impl();
}
이 코드는 다음과 같이 출력해요.
default_impl: counter was 0
default_impl: counter was 1
blanket_impl: counter was 0
blanket_impl: counter was 1
Ty1::default_impl과 Ty2::default_impl이 서로 다른 counter 값을 보는 걸 확인할 수 있죠? default 구현은 호출하는 타입마다 서로 다른 static을 가지는 것처럼 보이면서, 실제로는 하나씩 공유된다는 흥미로운 지점이에요.
가변 static (Mutable statics)
mut 키워드로 선언된 static 아이템은 프로그램이 수정할 수 있어요. 그런데 Rust의 목표 중 하나가 "동시성 버그를 맞닥뜨리기 어렵게 만들자"라는 점이라는 걸 떠올려 보세요. 가변 static은 당연히 레이스 컨디션이나 다른 버그의 아주 큰 원천이 될 수 있죠.
그래서 가변 static 변수를 읽거나 쓸 때는 unsafe 블록이 필요해요. 그리고 가변 static에 대한 수정이 같은 프로세스에서 실행 중인 다른 스레드에 대해 안전한지 주의를 기울여야 해요.
#![allow(unused)]
fn main() {
fn atomic_add(_: *mut u32, _: u32) -> u32 { 2 }
static mut LEVELS: u32 = 0;
// This violates the idea of no shared state, and this doesn't internally
// protect against races, so this function is `unsafe`
unsafe fn bump_levels_unsafe() -> u32 {
unsafe {
let ret = LEVELS;
LEVELS += 1;
return ret;
}
}
// As an alternative to `bump_levels_unsafe`, this function is safe, assuming
// that we have an atomic_add function which returns the old value. This
// function is safe only if no other code accesses the static in a non-atomic
// fashion. If such accesses are possible (such as in `bump_levels_unsafe`),
// then this would need to be `unsafe` to indicate to the caller that they
// must still guard against concurrent access.
fn bump_levels_safe() -> u32 {
unsafe {
return atomic_add(&raw mut LEVELS, 1);
}
}
}
가변 static은 여전히 아주 유용해요. C 라이브러리와 함께 사용할 수 있고, extern 블록에서 C 라이브러리로부터 바인딩할 수도 있죠.
가변 static은 일반 static과 같은 제약을 가지지만, 예외 하나는 Sync 트레이트를 구현할 필요가 없다는 점이에요.
static을 쓸까 const를 쓸까 (Using statics or consts)
상수 아이템(constant item)과 static 아이템 중 무엇을 써야 할지 헷갈릴 때가 있어요. 일반적으로는 static보다 const를 선호해야 해요. 단, 다음 중 하나라도 해당된다면 static을 고려해 보세요.
- 대량의 데이터를 저장하는 경우
- static의 단일 주소(single-address) 속성이 필요한 경우
- 내부 가변성(interior mutability)이 필요한 경우
더 알아보기 (Learn more)
- 상수 아이템 (Constant items) — static과 대비되는 const의 동작
- 연관 아이템 (Associated items) — 트레이트에서 static 정의
- 변수 — 스택 지역 변수와의 차이