Bagaimana cara membuat singleton global yang bisa berubah?

142

Apa cara terbaik untuk membuat dan menggunakan struct dengan hanya satu contoh dalam sistem? Ya, ini perlu, ini adalah subsistem OpenGL, dan membuat banyak salinan dari ini dan menyebarkannya ke mana-mana akan menambah kebingungan, daripada menghilangkannya.

Singleton harus seefisien mungkin. Tampaknya tidak mungkin untuk menyimpan objek sewenang-wenang di area statis, karena berisi Vecdengan destruktor. Opsi kedua adalah menyimpan pointer (tidak aman) di area statis, menunjuk ke singleton yang dialokasikan heap. Apa cara paling nyaman dan teraman untuk melakukan ini, sambil menjaga sintaks tetap singkat.

stevenkucera.dll
sumber
1
Pernahkah Anda melihat bagaimana binding Rust yang ada untuk OpenGL menangani masalah yang sama ini?
Shepmaster
20
Ya, ini perlu, ini adalah subsistem OpenGL, dan membuat banyak salinan dari ini dan menyebarkannya ke mana-mana akan menambah kebingungan, daripada menghilangkannya. => ini bukan definisi yang diperlukan , mungkin nyaman (pada awalnya) tetapi tidak perlu.
Matthieu M.
3
Ya, Anda benar. Meskipun karena OpenGL adalah mesin negara besar, saya hampir yakin tidak akan ada tiruannya di mana pun, yang penggunaannya hanya akan menghasilkan kesalahan OpenGL.
stevenkucera

Jawaban:

200

Bukan jawaban

Hindari keadaan global pada umumnya. Sebagai gantinya, buat objek di suatu tempat lebih awal (mungkin di main), lalu teruskan referensi yang dapat diubah ke objek tersebut ke tempat-tempat yang membutuhkannya. Ini biasanya akan membuat kode Anda lebih mudah untuk dipikirkan dan tidak perlu terlalu banyak membungkuk ke belakang.

Lihatlah diri Anda sendiri di cermin sebelum memutuskan bahwa Anda menginginkan variabel global yang bisa berubah. Ada kasus yang jarang terjadi di mana ini berguna, jadi itulah mengapa perlu diketahui bagaimana melakukannya.

Masih ingin membuatnya ...?

Menggunakan lazy-static

The malas-statis peti dapat mengambil beberapa kerepotan menciptakan tunggal secara manual. Berikut adalah vektor global yang bisa berubah:

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

Jika Anda menghapusnya Mutexmaka Anda memiliki singleton global tanpa perubahan apa pun.

Anda juga dapat menggunakan a, RwLockbukan a Mutexuntuk mengizinkan beberapa pembaca secara bersamaan.

Menggunakan once_cell

The once_cell crate dapat menghilangkan beberapa pekerjaan membosankan dalam membuat singleton secara manual. Berikut adalah vektor global yang bisa berubah:

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

Jika Anda menghapusnya Mutexmaka Anda memiliki singleton global tanpa perubahan apa pun.

Anda juga dapat menggunakan a, RwLockbukan a Mutexuntuk mengizinkan beberapa pembaca secara bersamaan.

Kasus khusus: atomics

Jika Anda hanya perlu melacak nilai integer, Anda dapat langsung menggunakan 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));
}

Manual, implementasi bebas ketergantungan

Ini sangat diambil dari implementasi Rust 1.0stdin dengan beberapa penyesuaian untuk Rust modern. Anda juga harus melihat implementasi modern dari io::Lazy. Saya telah berkomentar sesuai dengan apa yang dilakukan setiap baris.

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

Ini mencetak:

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

Kode ini dikompilasi dengan Rust 1.42.0. Implementasi nyata Stdinmenggunakan beberapa fitur yang tidak stabil untuk mencoba membebaskan memori yang dialokasikan, yang tidak dilakukan oleh kode ini.

Benar-benar, Anda mungkin ingin membuat SingletonReadermenerapkan Derefdan DerefMutsehingga Anda tidak perlu menyodok ke objek dan menguncinya sendiri.

Semua pekerjaan ini adalah apa yang dilakukan lazy-static atau once_cell untuk Anda.

Arti "global"

Harap dicatat bahwa Anda masih dapat menggunakan pelingkupan Rust normal dan privasi tingkat modul untuk mengontrol akses ke variabel staticatau lazy_static. Ini berarti Anda dapat mendeklarasikannya dalam modul atau bahkan di dalam fungsi dan tidak akan dapat diakses di luar modul / fungsi itu. Ini bagus untuk mengontrol akses:

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

Namun, variabel tersebut masih global karena ada satu contoh variabel yang ada di seluruh program.

Shepmaster
sumber
72
Setelah banyak pemikiran, saya yakin untuk tidak menggunakan Singleton, dan sebaliknya tidak menggunakan variabel global sama sekali dan meneruskan semuanya. Membuat kode lebih mendokumentasikan sendiri karena jelas fungsi apa yang mengakses perender. Jika saya ingin mengubah kembali ke tunggal, akan lebih mudah untuk melakukannya daripada sebaliknya.
stevenkucera
4
Terima kasih atas jawabannya, itu sangat membantu. Saya hanya berpikir saya akan membiarkan di sini komentar untuk menggambarkan apa yang saya lihat sebagai kasus penggunaan yang valid untuk lazy_static !. Saya menggunakannya untuk antarmuka ke aplikasi C yang memungkinkan modul pemuatan / pembongkaran (objek bersama) dan kode karat adalah salah satu modul ini. Saya tidak melihat banyak pilihan selain menggunakan global saat memuat karena saya tidak memiliki kendali atas main () sama sekali dan bagaimana aplikasi inti berinteraksi dengan modul saya. Saya pada dasarnya membutuhkan vektor hal-hal yang dapat ditambahkan pada runtime setelah mod saya dimuat.
Moises Silva
1
@MoisesSilva akan selalu ada beberapa alasan untuk membutuhkan singleton, tetapi tidak perlu menggunakannya dalam banyak kasus itu digunakan. Tanpa mengetahui kode Anda, mungkin aplikasi C harus mengizinkan setiap modul untuk mengembalikan "data pengguna" void *yang kemudian diteruskan kembali ke metode setiap modul. Ini adalah pola ekstensi tipikal untuk kode C. Jika aplikasi tidak mengizinkan hal ini dan Anda tidak dapat mengubahnya, ya, singleton mungkin merupakan solusi yang baik.
Shepmaster
3
@Worik maukah Anda menjelaskan mengapa? Saya mencegah orang melakukan sesuatu yang merupakan ide yang buruk di kebanyakan bahasa (bahkan OP setuju bahwa global adalah pilihan yang buruk untuk aplikasi mereka). Itulah arti secara umum . Saya kemudian menunjukkan dua solusi untuk bagaimana melakukannya. Saya baru saja menguji lazy_staticcontoh di Rust 1.24.1 dan berfungsi dengan tepat. Tidak ada external statictempat di sini. Mungkin Anda perlu memeriksa beberapa hal di pihak Anda untuk memastikan Anda telah memahami jawabannya sepenuhnya.
Shepmaster
1
@Worik jika Anda memerlukan bantuan dengan dasar-dasar cara menggunakan peti, saya sarankan Anda membaca kembali The Rust Programming Language . The bab tentang menciptakan permainan menebak menunjukkan bagaimana menambahkan dependensi.
Shepmaster
0

Gunakan SpinLock untuk akses 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(
        // ...
    );
}

Jika Anda menginginkan status yang bisa berubah (BUKAN Singleton), lihat Yang Tidak Dapat Dilakukan di Rust untuk penjelasan lebih lanjut.

Semoga bermanfaat.

unpluggedcoder
sumber
-1

Menjawab pertanyaan duplikat saya sendiri .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Root peti (lib.rs):

#[macro_use]
extern crate lazy_static;

Inisialisasi (tidak perlu blok yang tidak aman):

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

EDIT:

Berhasil menyelesaikannya dengan once_cell, yang tidak membutuhkan makro.

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
});
easychessanimations
sumber
2
Jawaban ini tidak memberikan sesuatu yang baru dibandingkan dengan jawaban yang sudah ada, baik yang sudah dibahas lazy_staticmaupun yang lebih baru once_cell. Titik menandai sesuatu sebagai duplikat pada SO adalah untuk menghindari informasi yang berlebihan.
Shepmaster