ฉันจะสร้างซิงเกิลตันระดับโลกที่เปลี่ยนรูปได้อย่างไร


143

วิธีใดเป็นวิธีที่ดีที่สุดในการสร้างและใช้โครงสร้างที่มีเพียงอินสแตนซ์เดียวในระบบ ใช่สิ่งนี้จำเป็นเป็นระบบย่อย OpenGL และการทำสำเนาหลาย ๆ ชุดและส่งต่อไปทั่วทุกที่จะเพิ่มความสับสนแทนที่จะบรรเทา

ซิงเกิลตันจะต้องมีประสิทธิภาพมากที่สุด ดูเหมือนจะเป็นไปไม่ได้ที่จะจัดเก็บวัตถุโดยพลการบนพื้นที่คงที่เนื่องจากมีวัตถุที่Vecมีตัวทำลาย ตัวเลือกที่สองคือการจัดเก็บตัวชี้ (ไม่ปลอดภัย) บนพื้นที่คงที่โดยชี้ไปที่ heap ที่จัดสรรซิงเกิลตัน อะไรคือวิธีที่สะดวกและปลอดภัยที่สุดในการดำเนินการนี้ในขณะเดียวกันก็รักษาไวยากรณ์ให้สั้นลง


1
คุณได้ดูว่าการผูก Rust ที่มีอยู่สำหรับ OpenGL จัดการกับปัญหาเดียวกันนี้อย่างไร
Shepmaster

20
ใช่สิ่งนี้จำเป็นเป็นระบบย่อย OpenGL และการทำสำเนาหลาย ๆ ชุดและส่งต่อไปทั่วทุกที่จะเพิ่มความสับสนแทนที่จะบรรเทา => นี่ไม่ใช่คำจำกัดความของความจำเป็นอาจจะสะดวก (ในตอนแรก) แต่ไม่จำเป็น
Matthieu M.

3
ใช่คุณมีประเด็น แม้ว่า OpenGL จะเป็นเครื่องขนาดใหญ่ แต่ฉันก็ใกล้จะแน่ใจว่าจะไม่มีการโคลนจากที่ใดก็ได้ซึ่งการใช้งานจะส่งผลให้เกิดข้อผิดพลาดของ OpenGL เท่านั้น
stevenkucera

คำตอบ:


200

คำตอบที่ไม่ใช่คำตอบ

หลีกเลี่ยงสภาวะทั่วโลกโดยทั่วไป ให้สร้างวัตถุไว้ที่ใดที่หนึ่งในช่วงต้น (อาจจะเป็นmain) จากนั้นส่งการอ้างอิงที่ไม่แน่นอนไปยังวัตถุนั้นไปยังสถานที่ที่ต้องการ ซึ่งโดยปกติจะทำให้รหัสของคุณหาเหตุผลได้ง่ายขึ้นและไม่ต้องก้มไปข้างหลังมากนัก

มองตัวเองในกระจกให้ดีก่อนที่จะตัดสินใจว่าคุณต้องการตัวแปรที่เปลี่ยนแปลงได้ทั่วโลก มีบางกรณีที่มีประโยชน์ดังนั้นจึงควรทราบวิธีการทำ

ยังต้องการสร้าง ... ?

ใช้ lazy-static

ขี้เกียจไฟฟ้าสถิตลังสามารถนำมาใช้บางส่วนของงานน่าเบื่อหน่ายของตนเองสร้างเดี่ยวที่ นี่คือเวกเตอร์ที่ไม่แน่นอนส่วนกลาง:

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แทน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แทนMutexเพื่ออนุญาตให้มีผู้อ่านพร้อมกันหลายคน

กรณีพิเศษ: อะตอม

หากคุณต้องการติดตามเฉพาะค่าจำนวนเต็มคุณสามารถใช้อะตอมได้โดยตรง:

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.0stdinด้วยการปรับแต่งบางอย่างสำหรับ Rust สมัยใหม่ 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 ทำเพื่อคุณ

ความหมายของ "global"

โปรดทราบว่าคุณยังสามารถใช้ Rust scoping และความเป็นส่วนตัวระดับโมดูลเพื่อควบคุมการเข้าถึงstaticหรือlazy_staticตัวแปรได้ ซึ่งหมายความว่าคุณสามารถประกาศได้ในโมดูลหรือแม้แต่ภายในฟังก์ชันและจะไม่สามารถเข้าถึงได้จากภายนอกโมดูล / ฟังก์ชันนั้น สิ่งนี้เหมาะสำหรับการควบคุมการเข้าถึง:

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

4
ขอบคุณสำหรับคำตอบมันช่วยได้มาก ฉันแค่คิดว่าฉันจะแสดงความคิดเห็นเพื่ออธิบายสิ่งที่ฉันเห็นว่าเป็นกรณีการใช้งานที่ถูกต้องสำหรับ lazy_static! ฉันใช้มันเพื่อเชื่อมต่อกับแอปพลิเคชัน C ที่อนุญาตให้โหลด / ยกเลิกการโหลดโมดูล (วัตถุที่ใช้ร่วมกัน) และรหัสสนิมเป็นหนึ่งในโมดูลเหล่านี้ ฉันไม่เห็นตัวเลือกมากไปกว่าการใช้ global on load เพราะฉันไม่สามารถควบคุม main () ได้เลยและวิธีที่แอปพลิเคชันหลักเชื่อมต่อกับโมดูลของฉัน โดยพื้นฐานแล้วฉันต้องการเวกเตอร์ของสิ่งต่าง ๆ ที่สามารถเพิ่มบนรันไทม์ได้หลังจากโหลด mod แล้ว
Moises Silva

1
@MoisesSilva มักจะมีเหตุผลบางอย่างที่ต้องใช้ซิงเกิลตัน แต่ไม่จำเป็นต้องใช้ในหลาย ๆ กรณีที่ใช้ หากไม่ทราบรหัสของคุณเป็นไปได้ว่าแอปพลิเคชัน C ควรอนุญาตให้แต่ละโมดูลส่งคืน "ข้อมูลผู้ใช้" void *ซึ่งจะส่งกลับไปยังวิธีการของแต่ละโมดูล นี่เป็นรูปแบบส่วนขยายทั่วไปสำหรับรหัส C หากแอปพลิเคชันไม่อนุญาตให้ทำสิ่งนี้และคุณไม่สามารถเปลี่ยนแปลงได้ใช่ซิงเกิลตันอาจเป็นทางออกที่ดี
Shepmaster

3
@Worik คุณอยากอธิบายว่าทำไม? ฉันไม่แนะนำให้ผู้คนทำอะไรบางอย่างที่เป็นความคิดที่ไม่ดีในภาษาส่วนใหญ่ (แม้แต่ OP ก็เห็นด้วยว่าโลกเป็นตัวเลือกที่ไม่ดีสำหรับการสมัครของพวกเขา) นั่นคือความหมายโดยทั่วไป จากนั้นฉันจะแสดงวิธีแก้ปัญหาสองวิธีสำหรับวิธีการต่อไป ฉันเพิ่งทดสอบlazy_staticตัวอย่างใน Rust 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(
        // ...
    );
}

หากคุณต้องการสถานะที่ไม่แน่นอน (ไม่ใช่ 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.