Como faço para criar um singleton global mutável?


140

Qual é a melhor maneira de criar e usar uma estrutura com apenas uma instanciação no sistema? Sim, isso é necessário, é o subsistema OpenGL, e fazer várias cópias disso e distribuí-lo em todos os lugares aumentaria a confusão, em vez de aliviá-la.

O singleton precisa ser o mais eficiente possível. Não parece possível armazenar um objeto arbitrário na área estática, pois contém um Veccom um destruidor. A segunda opção é armazenar um ponteiro (não seguro) na área estática, apontando para um singleton alocado no heap. Qual é a maneira mais conveniente e segura de fazer isso, mantendo a sintaxe concisa.


1
Você já viu como as associações Rust existentes para OpenGL lidam com esse mesmo problema?
Shepmaster

20
Sim, isso é necessário, é o subsistema OpenGL, e fazer várias cópias disso e distribuí-lo em todos os lugares aumentaria a confusão, em vez de aliviá-la. => esta não é a definição de necessário , pode ser conveniente (no início), mas não necessário.
Matthieu M.

3
Sim, você tem razão. Embora, como o OpenGL seja uma grande máquina de estado, estou quase certo de que não haverá um clone dele em lugar nenhum, cujo uso resultaria apenas em erros de OpenGL.
stevenkucera

Respostas:


198

Resposta sem resposta

Evite o estado global em geral. Em vez disso, construa o objeto em algum lugar no início (talvez emmain ) e, em seguida, transmita referências mutáveis ​​a esse objeto nos lugares que precisam dele. Isso geralmente tornará seu código mais fácil de raciocinar e não exigirá muito esforço para retroceder.

Olhe bem para si mesmo no espelho antes de decidir que deseja variáveis ​​globais mutáveis. Existem raros casos em que é útil, por isso vale a pena saber como fazer.

Ainda quer fazer um ...?

Usando lazy-static

O engradado estático preguiçoso pode eliminar parte do trabalho enfadonho de criar manualmente um único. Aqui está um vetor mutável global:

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

Se você remover o Mutex , terá um singleton global sem qualquer mutabilidade.

Você também pode usar um em RwLockvez de umMutex para permitir vários leitores simultâneos.

Usando once_cell

A caixa once_cell pode tirar um pouco do trabalho enfadonho de criar manualmente um singleton. Aqui está um vetor mutável global:

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

Se você remover o Mutex , terá um singleton global sem qualquer mutabilidade.

Você também pode usar um em RwLockvez de umMutex para permitir vários leitores simultâneos.

Um caso especial: atômica

Se você só precisa rastrear um valor inteiro, pode usar diretamente um atômico :

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

Implementação manual, livre de dependência

Isso é muito limitado pela implementação do Rust 1.0stdin com alguns ajustes para o Rust moderno. Você também deve olhar para a implementação moderna de io::Lazy. Eu comentei inline com o que cada linha faz.

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

Isso imprime:

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

Este código é compilado com Rust 1.42.0. As implementações reais de Stdinusam alguns recursos instáveis ​​para tentar liberar a memória alocada, o que este código não faz.

Na verdade, você provavelmente gostaria de fazer um SingletonReaderimplemento Derefe, DerefMutportanto, não precisou cutucar o objeto e travá-lo sozinho.

Todo esse trabalho é o que lazy-static ou once_cell fazem por você.

O significado de "global"

Observe que você ainda pode usar o escopo normal do Rust e a privacidade no nível do módulo para controlar o acesso a uma variável staticou lazy_static. Isso significa que você pode declará-lo em um módulo ou mesmo dentro de uma função e não estará acessível fora desse módulo / função. Isso é bom para controlar o acesso:

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

No entanto, a variável ainda é global porque há uma instância dela que existe em todo o programa.


72
Depois de muito pensar, estou convencido de não usar o Singleton e, em vez disso, não usar nenhuma variável global e passar tudo adiante. Torna o código mais autodocumentado, pois fica claro quais funções acessam o renderizador. Se eu quiser voltar para o singleton, será mais fácil fazer isso do que o contrário.
stevenkucera

4
Obrigado pela resposta, ajudou muito. Apenas pensei em deixar aqui um comentário para descrever o que considero um caso de uso válido para lazy_static !. Estou usando-o para fazer interface com um aplicativo C que permite carregar / descarregar módulos (objetos compartilhados) e o código de ferrugem é um desses módulos. Não vejo muita opção do que usar um global on load porque não tenho nenhum controle sobre main () e como o aplicativo principal faz interface com meu módulo. Eu basicamente precisava de um vetor de coisas que podem ser adicionadas no tempo de execução depois que meu mod é carregado.
Moises Silva

1
@MoisesSilva sempre haverá algum motivo para precisar de um singleton, mas é desnecessário usá-lo em muitos dos casos em que é usado. Sem conhecer seu código, é possível que o aplicativo C permita que cada módulo retorne "dados do usuário" void *que são então passados ​​de volta para os métodos de cada módulo. Este é um padrão de extensão típico para código C. Se o aplicativo não permitir isso e você não puder alterá-lo, sim, um singleton pode ser uma boa solução.
Shepmaster

3
@Worik, você se importaria de explicar por quê? Eu desencorajo as pessoas de fazer algo que é uma ideia ruim na maioria dos idiomas (até mesmo o OP concordou que um global foi uma escolha ruim para sua aplicação). Isso é o que em geral significa. Em seguida, mostro duas soluções para fazer isso de qualquer maneira. Acabei de testar o lazy_staticexemplo em Rust 1.24.1 e funciona exatamente. Não há nenhum external staticlugar aqui. Talvez você precise verificar as coisas do seu lado para ter certeza de que entendeu a resposta completamente.
Shepmaster

1
@Worik se você precisar de ajuda com o básico de como usar uma caixa, sugiro que você releia The Rust Programming Language . O capítulo sobre como criar um jogo de adivinhação mostra como adicionar dependências.
Shepmaster

0

Use SpinLock para acesso global.

#[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(
        // ...
    );
}

Se você quiser um estado mutável (NÃO Singleton), consulte O que não fazer no Rust para obter mais descrições.

Espero que seja útil.


-1

Respondendo minha própria pergunta duplicada .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Raiz da caixa (lib.rs):

#[macro_use]
extern crate lazy_static;

Inicialização (sem necessidade de bloqueio inseguro):

/// 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
    };
    ...

EDITAR:

Consegui resolver com once_cell, que não precisa de macro.

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
Esta resposta não fornece nada de novo em comparação com as respostas existentes, que já discutem lazy_statice as mais recentes once_cell. O objetivo de marcar coisas como duplicatas no SO é evitar informações redundantes.
Shepmaster
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.