Làm cách nào để tạo một singleton toàn cầu, có thể thay đổi được?


140

Cách tốt nhất để tạo và sử dụng một cấu trúc chỉ có một khởi tạo trong hệ thống là gì? Vâng, điều này là cần thiết, đó là hệ thống con OpenGL, và việc tạo nhiều bản sao của hệ thống này và chuyển nó đi khắp nơi sẽ gây thêm sự nhầm lẫn, thay vì giải tỏa nó.

Singleton cần phải hiệu quả nhất có thể. Dường như không thể lưu trữ một đối tượng tùy ý trên vùng tĩnh, vì nó chứa một Vechàm hủy. Tùy chọn thứ hai là lưu trữ một con trỏ (không an toàn) trên vùng tĩnh, trỏ đến một singleton được phân bổ heap. Cách thuận tiện nhất và an toàn nhất để làm điều này là gì, trong khi vẫn giữ ngắn gọn cú pháp.


1
Bạn đã xem các ràng buộc Rust hiện có cho OpenGL xử lý vấn đề tương tự như thế nào chưa?
Shepmaster

20
Vâng, điều này là cần thiết, đó là hệ thống con OpenGL, và việc tạo nhiều bản sao của hệ thống này và chuyển nó đi khắp nơi sẽ gây thêm sự nhầm lẫn, thay vì giải tỏa nó. => đây không phải là định nghĩa của cần thiết , có thể thuận tiện (lúc đầu) nhưng không cần thiết.
Matthieu M.

3
Có bạn có một điểm. Mặc dù OpenGL dù sao cũng là một máy trạng thái lớn, tôi chắc chắn rằng sẽ không có bản sao của nó ở bất kỳ đâu, mà việc sử dụng nó sẽ chỉ dẫn đến lỗi OpenGL.
stevenkucera

Câu trả lời:


198

Câu trả lời không trả lời

Tránh trạng thái toàn cầu nói chung. Thay vào đó, hãy xây dựng đối tượng ở đâu đó sớm (có thể là trong main), sau đó chuyển các tham chiếu có thể thay đổi đến đối tượng đó vào những nơi cần nó. Điều này thường sẽ làm cho mã của bạn dễ suy luận hơn và không yêu cầu phải cúi nhiều về phía sau.

Hãy nhìn kỹ bản thân trong gương trước khi quyết định rằng bạn muốn các biến toàn cục có thể thay đổi. Có những trường hợp hiếm hoi mà nó hữu ích, vì vậy đó là lý do tại sao nó đáng để biết cách làm.

Vẫn muốn làm một ...?

Sử dụng lazy-static

Các lười biếng tĩnh thùng có thể lấy đi một số nhàm chán của tay tạo ra một singleton. Đây là một vectơ có thể thay đổi toàn cục:

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

Nếu bạn loại bỏ Mutexthì bạn có một singleton toàn cục mà không có bất kỳ khả năng thay đổi nào.

Bạn cũng có thể sử dụng RwLockthay vì a Mutexđể cho phép nhiều trình đọc đồng thời.

Sử dụng once_cell

Các once_cell thùng có thể lấy đi một số nhàm chán của tay tạo ra một singleton. Đây là một vectơ có thể thay đổi toàn cục:

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

Nếu bạn loại bỏ Mutexthì bạn có một singleton toàn cục mà không có bất kỳ khả năng thay đổi nào.

Bạn cũng có thể sử dụng RwLockthay vì a Mutexđể cho phép nhiều trình đọc đồng thời.

Một trường hợp đặc biệt: nguyên tử

Nếu bạn chỉ cần theo dõi một giá trị số nguyên, bạn có thể sử dụng trực tiếp một nguyên tử :

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

Triển khai thủ công, không phụ thuộc

Điều này được ghi lại rất nhiều từ việc triển khai Rust 1.0stdin với một số tinh chỉnh cho Rust hiện đại. Bạn cũng nên nhìn vào việc thực hiện hiện đại của io::Lazy. Tôi đã nhận xét nội tuyến với những gì mỗi dòng làm.

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

Điều này in ra:

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

Mã này biên dịch với Rust 1.42.0. Việc triển khai thực sự Stdinsử dụng một số tính năng không ổn định để cố gắng giải phóng bộ nhớ được cấp phát, điều mà mã này không làm được.

Thực sự, bạn có thể muốn tạo nông SingletonReadercụ DerefDerefMutvì vậy bạn không cần phải tự mình chọc vào đối tượng và khóa nó.

Tất cả công việc này là những gì lazy-static hoặc once_cell làm cho bạn.

Ý nghĩa của "toàn cầu"

Xin lưu ý rằng bạn vẫn có thể sử dụng phạm vi Rust bình thường và quyền riêng tư cấp mô-đun để kiểm soát quyền truy cập vào một statichoặc lazy_staticbiến. Điều này có nghĩa là bạn có thể khai báo nó trong một mô-đun hoặc thậm chí bên trong một chức năng và nó sẽ không thể truy cập được bên ngoài mô-đun / chức năng đó. Điều này tốt cho việc kiểm soát quyền truy cập:

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

Tuy nhiên, biến vẫn có tính toàn cục ở chỗ có một trường hợp của nó tồn tại trong toàn bộ chương trình.


72
Sau rất nhiều suy nghĩ, tôi bị thuyết phục là không sử dụng Singleton, và thay vào đó, không sử dụng biến toàn cục nào cả và chuyển mọi thứ xung quanh. Làm cho mã tự ghi lại nhiều hơn vì nó rõ ràng những chức năng nào truy cập vào trình kết xuất. Nếu tôi muốn thay đổi trở lại singleton, nó sẽ dễ dàng hơn để làm điều đó so với cách khác.
stevenkucera

4
Cảm ơn vì câu trả lời, nó đã giúp ích rất nhiều. Tôi chỉ nghĩ rằng tôi sẽ để ở đây một nhận xét để mô tả những gì tôi thấy là một trường hợp sử dụng hợp lệ cho lazy_static !. Tôi đang sử dụng nó để giao diện với một ứng dụng C cho phép tải / dỡ các mô-đun (đối tượng dùng chung) và mã gỉ là một trong những mô-đun này. Tôi không thấy có nhiều lựa chọn hơn là sử dụng toàn cục khi tải vì tôi hoàn toàn không kiểm soát được main () và cách ứng dụng cốt lõi giao tiếp với mô-đun của tôi. Về cơ bản, tôi cần một vector của những thứ có thể được thêm vào trong thời gian chạy sau khi mod của tôi được tải.
Moises Silva

1
@MoisesSilva luôn có một số lý do để cần một singleton, nhưng không cần thiết phải sử dụng nó trong nhiều trường hợp nó được sử dụng. Nếu không biết mã của bạn, có thể ứng dụng C sẽ cho phép mỗi mô-đun trả về một "dữ liệu người dùng" void *, sau đó được chuyển lại vào các phương thức của mỗi mô-đun. Đây là một mẫu mở rộng điển hình cho mã C. Nếu ứng dụng không cho phép điều này và bạn không thể thay đổi nó, thì có, một singleton có thể là một giải pháp tốt.
Shepmaster

3
@Worik bạn có muốn giải thích tại sao không? Tôi không khuyến khích mọi người làm điều gì đó là một ý tưởng kém trong hầu hết các ngôn ngữ (ngay cả OP cũng đồng ý rằng toàn cầu là một lựa chọn tồi cho ứng dụng của họ). Đó là những gì nói chung có nghĩa là. Sau đó, tôi chỉ ra hai giải pháp cho cách làm điều đó. Tôi vừa thử nghiệm lazy_staticví dụ trong Rust 1.24.1 và nó hoạt động chính xác. Không có external staticnơi nào ở đây. Có lẽ bạn cần kiểm tra lại mọi thứ để đảm bảo rằng bạn đã hiểu đầy đủ câu trả lời.
Shepmaster

1
@Worik nếu bạn cần trợ giúp về những điều cơ bản về cách sử dụng thùng, tôi khuyên bạn nên đọc lại Ngôn ngữ lập trình Rust . Các chương về việc tạo ra một trò chơi đoán cho thấy cách để thêm phụ thuộc.
Shepmaster

0

Sử dụng SpinLock để truy cập toàn cầu.

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

Nếu bạn muốn trạng thái có thể thay đổi (KHÔNG phải là Singleton), hãy xem Việc không nên làm trong Rust để biết thêm mô tả.

Hy vọng nó hữu ích.


-1

Trả lời câu hỏi trùng lặp của riêng tôi .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Gốc thùng (lib.rs):

#[macro_use]
extern crate lazy_static;

Khởi tạo (không cần khối không an toàn):

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

BIÊN TẬP:

Được quản lý để giải quyết nó bằng once_cell, không cần 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
Câu trả lời này không cung cấp bất cứ điều gì mới so với các câu trả lời hiện có, đã thảo luận lazy_staticvà mới hơn once_cell. Điểm đánh dấu mọi thứ là trùng lặp trên SO là để tránh có thông tin thừa.
Shepmaster
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.