전역적이고 가변적 인 싱글 톤은 어떻게 만듭니 까?


140

시스템에서 인스턴스화가 하나 뿐인 구조체를 만들고 사용하는 가장 좋은 방법은 무엇입니까? 예, 이것이 필요합니다. 이것은 OpenGL 하위 시스템이며, 여러 복사본을 만들어 모든 곳에 전달하면 혼란을 덜어주기보다는 혼란을 가중시킬 수 있습니다.

싱글 톤은 가능한 한 효율적이어야합니다. Vec소멸자 가 포함되어 있기 때문에 정적 영역에 임의의 개체를 저장할 수 없습니다 . 두 번째 옵션은 힙 할당 싱글 톤을 가리키는 정적 영역에 (안전하지 않은) 포인터를 저장하는 것입니다. 구문을 간결하게 유지하면서이를 수행하는 가장 편리하고 안전한 방법은 무엇입니까?


1
OpenGL 용 기존 Rust 바인딩이이 같은 문제를 어떻게 처리하는지 살펴 보셨습니까?
Shepmaster

20
예, 이것이 필요합니다. 이것은 OpenGL 하위 시스템이며, 여러 복사본을 만들어 모든 곳에 전달하면 혼란을 덜어주기보다는 혼란을 가중시킬 수 있습니다. =>이 정의되지 않습니다 필요 가 어쩌면이다, 편리 (처음에는)하지만 필요가 없습니다.
Matthieu M.

3
네, 포인트가 있습니다. 어쨌든 OpenGL은 큰 상태 기계이기 때문에 나는 그것을 어디서나 복제하지 않을 것이라고 확신합니다.
stevenkucera 2015 년

답변:


198

무응답 답변

일반적으로 글로벌 상태를 피하십시오. 대신 어딘가에 (아마도에서 main) 객체를 생성 한 다음 해당 객체에 대한 변경 가능한 참조를 필요한 위치로 전달하십시오. 이것은 일반적으로 코드를 추론하기 쉽게 만들고 거꾸로 구부릴 필요가 없습니다.

전역 가변 변수를 원한다고 결정하기 전에 거울을 자세히 살펴보십시오. 유용한 경우가 드물기 때문에 방법을 아는 것이 좋습니다.

아직도 하나 만들고 싶어 ...?

게으른 정적 사용

게으른 정적의 상자 수동으로 싱글을 만드는 천역의 일부를 멀리 걸릴 수 있습니다. 다음은 전역 가변 벡터입니다.

use lazy_static::lazy_static; // 1.4.0
use std::sync::Mutex;

lazy_static! {
    static ref ARRAY: Mutex<Vec<u8>> = Mutex::new(vec![]);
}

fn do_a_call() {
    ARRAY.lock().unwrap().push(1);
}

fn main() {
    do_a_call();
    do_a_call();
    do_a_call();

    println!("called {}", ARRAY.lock().unwrap().len());
}

를 제거하면 Mutex가변성이없는 전역 싱글 톤이 있습니다.

a RwLock대신 a Mutex를 사용하여 여러 동시 판독기를 허용 할 수도 있습니다 .

once_cell 사용

once_cell 상자 수동으로 싱글을 만드는 천역의 일부를 멀리 걸릴 수 있습니다. 다음은 전역 가변 벡터입니다.

use once_cell::sync::Lazy; // 1.3.1
use std::sync::Mutex;

static ARRAY: Lazy<Mutex<Vec<u8>>> = Lazy::new(|| Mutex::new(vec![]));

fn do_a_call() {
    ARRAY.lock().unwrap().push(1);
}

fn main() {
    do_a_call();
    do_a_call();
    do_a_call();

    println!("called {}", ARRAY.lock().unwrap().len());
}

를 제거하면 Mutex가변성이없는 전역 싱글 톤이 있습니다.

a RwLock대신 a Mutex를 사용하여 여러 동시 판독기를 허용 할 수도 있습니다 .

특별한 경우 : 원 자학

정수 값만 추적해야하는 경우 atomic을 직접 사용할 수 있습니다 .

use std::sync::atomic::{AtomicUsize, Ordering};

static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);

fn do_a_call() {
    CALL_COUNT.fetch_add(1, Ordering::SeqCst);
}

fn main() {
    do_a_call();
    do_a_call();
    do_a_call();

    println!("called {}", CALL_COUNT.load(Ordering::SeqCst));
}

수동, 종속성없는 구현

이것은 Rust 1.0 구현stdin 에서 큰 영향을 받았습니다 . 또한 io::Lazy. 나는 각 라인이하는 일에 대해 인라인으로 주석을 달았습니다.

use std::sync::{Arc, Mutex, Once};
use std::time::Duration;
use std::{mem, thread};

#[derive(Clone)]
struct SingletonReader {
    // Since we will be used in many threads, we need to protect
    // concurrent access
    inner: Arc<Mutex<u8>>,
}

fn singleton() -> SingletonReader {
    // Initialize it to a null value
    static mut SINGLETON: *const SingletonReader = 0 as *const SingletonReader;
    static ONCE: Once = Once::new();

    unsafe {
        ONCE.call_once(|| {
            // Make it
            let singleton = SingletonReader {
                inner: Arc::new(Mutex::new(0)),
            };

            // Put it in the heap so it can outlive this call
            SINGLETON = mem::transmute(Box::new(singleton));
        });

        // Now we give out a copy of the data that is safe to use concurrently.
        (*SINGLETON).clone()
    }
}

fn main() {
    // Let's use the singleton in a few threads
    let threads: Vec<_> = (0..10)
        .map(|i| {
            thread::spawn(move || {
                thread::sleep(Duration::from_millis(i * 10));
                let s = singleton();
                let mut data = s.inner.lock().unwrap();
                *data = i as u8;
            })
        })
        .collect();

    // And let's check the singleton every so often
    for _ in 0u8..20 {
        thread::sleep(Duration::from_millis(5));

        let s = singleton();
        let data = s.inner.lock().unwrap();
        println!("It is: {}", *data);
    }

    for thread in threads.into_iter() {
        thread.join().unwrap();
    }
}

다음과 같이 출력됩니다.

It is: 0
It is: 1
It is: 1
It is: 2
It is: 2
It is: 3
It is: 3
It is: 4
It is: 4
It is: 5
It is: 5
It is: 6
It is: 6
It is: 7
It is: 7
It is: 8
It is: 8
It is: 9
It is: 9
It is: 9

이 코드는 Rust 1.42.0으로 컴파일됩니다. 의 실제 구현은 Stdin일부 불안정한 기능을 사용하여 할당 된 메모리를 해제하려고 시도하지만이 코드는 그렇지 않습니다.

정말로, 당신은 아마도 SingletonReader구현 Deref하고 싶을 DerefMut것이므로 객체를 찔러서 직접 잠글 필요가 없습니다.

이 모든 작업은 lazy-static 또는 once_cell이 수행하는 작업입니다.

"글로벌"의 의미

static또는 lazy_static변수에 대한 액세스를 제어하기 위해 일반 Rust 범위 지정 및 모듈 수준 개인 정보를 계속 사용할 수 있습니다 . 즉, 모듈 또는 함수 내부에서 선언 할 수 있으며 해당 모듈 / 함수 외부에서 액세스 할 수 없습니다. 액세스 제어에 유용합니다.

use lazy_static::lazy_static; // 1.2.0

fn only_here() {
    lazy_static! {
        static ref NAME: String = String::from("hello, world!");
    }

    println!("{}", &*NAME);
}

fn not_here() {
    println!("{}", &*NAME);
}
error[E0425]: cannot find value `NAME` in this scope
  --> src/lib.rs:12:22
   |
12 |     println!("{}", &*NAME);
   |                      ^^^^ not found in this scope

그러나 변수는 전체 프로그램에 걸쳐 존재하는 하나의 인스턴스가 있다는 점에서 여전히 전역 적입니다.


72
많은 생각 끝에 Singleton을 사용하지 않고 대신 전역 변수를 전혀 사용하지 않고 모든 것을 전달합니다. 어떤 함수가 렌더러에 액세스하는지 명확하기 때문에 코드를 더 자체 문서화합니다. 싱글 톤으로 다시 바꾸고 싶다면 다른 방법보다 그렇게하는 것이 더 쉬울 것입니다.
stevenkucera 2015 년

4
답변 감사합니다. 많은 도움이되었습니다. lazy_static!의 유효한 사용 사례를 설명하기 위해 여기에 주석을 달겠다고 생각했습니다. 모듈 (공유 객체)로드 / 언로드를 허용하는 C 응용 프로그램에 인터페이스하는 데 사용하고 있으며 rust 코드는 이러한 모듈 중 하나입니다. 나는 main ()을 전혀 제어 할 수없고 핵심 애플리케이션이 내 모듈과 인터페이스하는 방식을 전혀 제어 할 수 없기 때문에로드시 전역을 사용하는 것보다 많은 옵션을 볼 수 없습니다. 기본적으로 모드가로드 된 후 런타임에 추가 할 수있는 벡터가 필요했습니다.
Moises Silva

1
@MoisesSilva 항상 거기있을거야 약간의 싱글을 필요로하는 이유는,하지만 사용되는 많은 경우에 사용하는 것이 필요합니다. 코드를 모르면 C 응용 프로그램이 각 모듈이 "사용자 데이터"를 반환하도록 허용 void *한 다음 각 모듈의 메서드로 다시 전달 될 수 있습니다. 이것은 C 코드의 일반적인 확장 패턴입니다. 응용 프로그램이이를 허용하지 않고 변경할 수없는 경우 예, 싱글 톤이 좋은 솔루션 일 수 있습니다.
Shepmaster

3
@Worik이 이유를 설명해 주시겠습니까? 나는 사람들이 대부분의 언어에서 좋지 않은 생각을하지 않도록 권장합니다 (OP조차도 글로벌이 그들의 응용 프로그램에 나쁜 선택이라는 데 동의했습니다). 그것이 일반적으로 의미하는 바입니다. 그런 다음 어쨌든 그것을 수행하는 방법에 대한 두 가지 솔루션을 보여줍니다. 방금 lazy_staticRust 1.24.1 에서 예제를 테스트 했는데 정확히 작동합니다. 여기에는 아무 external static데도 없습니다 . 아마도 답을 완전히 이해했는지 확인하기 위해 끝까지 확인해야 할 수도 있습니다.
Shepmaster

1
@Worik 크레이트 사용법의 기본에 대한 도움이 필요하다면 The Rust Programming Language 를 다시 읽어 보시기 바랍니다 . 추측 게임 만들기에 장 방법 종속성을 추가하는 방법을 보여줍니다.
Shepmaster

0

전역 액세스를 위해 SpinLock 을 사용 합니다.

#[derive(Default)]
struct ThreadRegistry {
    pub enabled_for_new_threads: bool,
    threads: Option<HashMap<u32, *const Tls>>,
}

impl ThreadRegistry {
    fn threads(&mut self) -> &mut HashMap<u32, *const Tls> {
        self.threads.get_or_insert_with(HashMap::new)
    }
}

static THREAD_REGISTRY: SpinLock<ThreadRegistry> = SpinLock::new(Default::default());

fn func_1() {
    let thread_registry = THREAD_REGISTRY.lock();  // Immutable access
    if thread_registry.enabled_for_new_threads {
    }
}

fn func_2() {
    let mut thread_registry = THREAD_REGISTRY.lock();  // Mutable access
    thread_registry.threads().insert(
        // ...
    );
}

변경 가능한 상태 (NOT Singleton)를 원한다면 Rust에서하지 말아야 할 일을 참조하세요 .

도움이 되었기를 바랍니다.


-1

내 자신의 중복 질문에 대답 .

Cargo.toml :

[dependencies]
lazy_static = "1.4.0"

상자 루트 (lib.rs) :

#[macro_use]
extern crate lazy_static;

초기화 (안전하지 않은 블록 필요 없음) :

/// EMPTY_ATTACK_TABLE defines an empty attack table, useful for initializing attack tables
pub const EMPTY_ATTACK_TABLE: AttackTable = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

lazy_static! {
    /// KNIGHT_ATTACK is the attack table of knight
    pub static ref KNIGHT_ATTACK: AttackTable = {
        let mut at = EMPTY_ATTACK_TABLE;
        for sq in 0..BOARD_AREA{
            at[sq] = jump_attack(sq, &KNIGHT_DELTAS, 0);
        }
        at
    };
    ...

편집하다:

매크로가 필요없는 once_cell로 해결했습니다.

Cargo.toml :

[dependencies]
once_cell = "1.3.1"

square.rs :

use once_cell::sync::Lazy;

...

/// AttackTable type records an attack bitboard for every square of a chess board
pub type AttackTable = [Bitboard; BOARD_AREA];

/// EMPTY_ATTACK_TABLE defines an empty attack table, useful for initializing attack tables
pub const EMPTY_ATTACK_TABLE: AttackTable = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

/// KNIGHT_ATTACK is the attack table of knight
pub static KNIGHT_ATTACK: Lazy<AttackTable> = Lazy::new(|| {
    let mut at = EMPTY_ATTACK_TABLE;
    for sq in 0..BOARD_AREA {
        at[sq] = jump_attack(sq, &KNIGHT_DELTAS, 0);
    }
    at
});

2
이 답변은 이미 논의 된 기존 답변 lazy_static과 최신 답변에 비해 새로운 것을 제공하지 않습니다 once_cell. SO에서 물건을 중복으로 표시하는 요점은 중복 정보를 피하는 것입니다.
Shepmaster
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.