MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust Once初始化与线程安全

2024-04-063.8k 阅读

Rust Once初始化机制

在Rust中,Once 类型是 std::sync::Once,它提供了一种在程序中进行一次性初始化的机制。这种初始化机制非常重要,尤其是在多线程环境下,能够确保某些代码只执行一次,避免重复初始化带来的问题。

Once 的基本原理

Once 类型内部使用了原子操作来实现线程安全的一次性初始化。它通过一个 AtomicUsize 字段来跟踪初始化状态。AtomicUsize 是Rust标准库中提供的用于原子操作的类型,它允许在多线程环境下对 usize 类型的值进行无锁的原子读写操作。

当使用 Once 进行初始化时,首先会检查 AtomicUsize 中的值,判断初始化是否已经完成。如果尚未完成,就会进入初始化流程,在初始化完成后,将 AtomicUsize 的值设置为表示初始化完成的状态。由于 AtomicUsize 的原子操作特性,多个线程同时尝试初始化时,只有一个线程能够成功执行初始化代码,其他线程会等待初始化完成。

Once 的使用方法

Once 类型提供了 call_once 方法来执行一次性初始化。call_once 方法接受一个闭包作为参数,这个闭包中的代码就是需要进行一次性执行的初始化代码。

下面是一个简单的单线程示例:

use std::sync::Once;

static INIT: Once = Once::new();

fn setup() {
    println!("Initializing...");
}

fn main() {
    INIT.call_once(setup);
    INIT.call_once(setup);
}

在这个示例中,INIT 是一个 Once 类型的静态变量。main 函数中两次调用 INIT.call_once(setup),但由于 Once 的特性,setup 函数只会被执行一次。运行这个程序,你会看到输出:

Initializing...

Once 与线程安全

在多线程环境下,Once 的线程安全特性就显得尤为重要。Rust的 Once 类型设计初衷就是为了在多线程程序中提供可靠的一次性初始化机制。

多线程下的 Once 使用

考虑以下多线程示例:

use std::sync::{Once, Arc};
use std::thread;

static INIT: Once = Once::new();

fn setup() {
    println!("Initializing...");
}

fn main() {
    let num_threads = 10;
    let threads: Vec<_> = (0..num_threads).map(|_| {
        thread::spawn(move || {
            INIT.call_once(setup);
        })
    }).collect();

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

在这个示例中,我们创建了10个线程,每个线程都尝试调用 INIT.call_once(setup)。由于 Once 的线程安全特性,setup 函数只会被执行一次,无论有多少个线程同时尝试初始化。运行这个程序,你仍然只会看到一次 Initializing... 的输出。

Once 线程安全的实现细节

从实现层面来看,Once 使用了 AtomicUsizecompare_and_swap(在Rust中通常使用 compare_exchange 方法)原子操作。这个操作会比较 AtomicUsize 当前的值与预期值,如果相等,则将其设置为新值,并返回旧值;如果不相等,则返回当前值。

Once 的初始化过程中,首先会检查 AtomicUsize 的值是否表示初始化已经完成。如果没有完成,线程会尝试使用 compare_exchange 操作将其设置为表示正在初始化的状态。只有第一个成功将其设置为正在初始化状态的线程能够继续执行初始化代码,其他线程会进入等待状态。当初始化完成后,AtomicUsize 的值会被设置为表示初始化完成的状态,等待中的线程会再次检查状态,发现已经初始化完成后,就不会再尝试执行初始化代码。

结合 OnceLazy

Rust还提供了 std::sync::Lazy 类型,它与 Once 密切相关,并且在很多场景下结合使用会更加方便。

Lazy 的基本概念

Lazy 类型用于延迟初始化一个值。它内部实际上是基于 Once 实现的,提供了一种更简洁的方式来定义和使用延迟初始化的值。Lazy 类型在首次访问其值时会执行初始化闭包,并且确保初始化只发生一次,这与 Once 的一次性初始化特性相契合。

Lazy 的使用示例

use std::sync::Lazy;

static DATA: Lazy<String> = Lazy::new(|| {
    println!("Initializing data...");
    "Hello, Lazy!".to_string()
});

fn main() {
    println!("Accessing data: {}", DATA);
    println!("Accessing data again: {}", DATA);
}

在这个示例中,DATA 是一个 Lazy<String> 类型的静态变量。Lazy::new 接受一个闭包,闭包中的代码会在首次访问 DATA 时执行,用于初始化 DATA 的值。运行这个程序,你会看到输出:

Initializing data...
Accessing data: Hello, Lazy!
Accessing data again: Hello, Lazy!

可以看到,初始化代码只执行了一次,即使多次访问 DATA

多线程下的 Lazy

Lazy 在多线程环境下同样是线程安全的。下面是一个多线程使用 Lazy 的示例:

use std::sync::{Lazy, Arc};
use std::thread;

static DATA: Lazy<String> = Lazy::new(|| {
    println!("Initializing data...");
    "Hello, Lazy in threads!".to_string()
});

fn main() {
    let num_threads = 10;
    let threads: Vec<_> = (0..num_threads).map(|_| {
        thread::spawn(move || {
            println!("Thread accessing data: {}", DATA);
        })
    }).collect();

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

在这个示例中,10个线程同时尝试访问 DATA。由于 Lazy 基于 Once 实现,初始化代码只会在第一个访问 DATA 的线程中执行一次,其他线程会等待初始化完成后直接使用已初始化的值。运行这个程序,你会看到初始化输出只出现一次,每个线程都能正确访问到初始化后的值。

Once 与静态变量初始化

在Rust中,Once 常常用于静态变量的初始化,以确保静态变量在多线程环境下的正确初始化。

静态变量的初始化问题

在多线程程序中,静态变量的初始化需要特别小心。如果没有合适的机制,可能会出现多个线程同时尝试初始化静态变量的情况,导致未定义行为或数据竞争。例如:

// 以下代码存在数据竞争风险
static mut DATA: i32 = 0;

fn init_data() {
    unsafe {
        if DATA == 0 {
            DATA = 42;
        }
    }
}

fn main() {
    let num_threads = 10;
    let threads: Vec<_> = (0..num_threads).map(|_| {
        thread::spawn(move || {
            init_data();
            unsafe {
                println!("Thread sees data: {}", DATA);
            }
        })
    }).collect();

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

在这个示例中,init_data 函数尝试在 DATA 未初始化时进行初始化。但是由于没有同步机制,多个线程可能同时进入 if DATA == 0 块,导致数据竞争。

使用 Once 解决静态变量初始化问题

通过使用 Once,可以确保静态变量的初始化是线程安全的。以下是修正后的代码:

use std::sync::Once;

static DATA: i32;
static INIT: Once = Once::new();

fn init_data() {
    INIT.call_once(|| {
        DATA = 42;
    });
}

fn main() {
    let num_threads = 10;
    let threads: Vec<_> = (0..num_threads).map(|_| {
        thread::spawn(move || {
            init_data();
            println!("Thread sees data: {}", DATA);
        })
    }).collect();

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

在这个版本中,INIT 是一个 Once 类型的静态变量,init_data 函数使用 INIT.call_once 来确保 DATA 只被初始化一次。无论有多少个线程调用 init_dataDATA 的初始化都只会发生一次,从而避免了数据竞争问题。

Once 在复杂场景中的应用

除了基本的一次性初始化和静态变量初始化,Once 在一些复杂场景中也有重要应用。

初始化全局资源

在一些应用中,可能需要初始化一些全局资源,如数据库连接池、网络服务等。这些资源的初始化通常是昂贵的,并且只需要进行一次。使用 Once 可以有效地管理这些全局资源的初始化。

use std::sync::{Once, Mutex};
use std::collections::HashMap;

// 模拟数据库连接
struct DatabaseConnection {
    // 实际的数据库连接相关字段
}

impl DatabaseConnection {
    fn new() -> Self {
        println!("Connecting to database...");
        DatabaseConnection {}
    }
}

static DB_CONNECTION: Mutex<Option<DatabaseConnection>> = Mutex::new(None);
static INIT_DB: Once = Once::new();

fn get_database_connection() -> DatabaseConnection {
    INIT_DB.call_once(|| {
        let mut guard = DB_CONNECTION.lock().unwrap();
        *guard = Some(DatabaseConnection::new());
    });
    DB_CONNECTION.lock().unwrap().as_ref().unwrap().clone()
}

fn main() {
    let num_threads = 10;
    let threads: Vec<_> = (0..num_threads).map(|_| {
        thread::spawn(move || {
            let conn = get_database_connection();
            // 使用数据库连接进行操作
        })
    }).collect();

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

在这个示例中,DatabaseConnection 模拟了一个数据库连接。DB_CONNECTION 是一个 Mutex 保护的 Option<DatabaseConnection>,用于存储数据库连接实例。INIT_DBOnce 类型,确保数据库连接只初始化一次。get_database_connection 函数使用 INIT_DB.call_once 来初始化数据库连接,并在后续调用中返回已初始化的连接。

动态初始化与配置

在一些情况下,初始化过程可能需要根据运行时的配置进行动态调整。OnceLazy 结合可以很好地满足这种需求。

use std::sync::{Once, Lazy};
use std::env;

// 假设这是一个根据配置动态初始化的对象
struct ConfiguredObject {
    value: i32,
}

impl ConfiguredObject {
    fn new() -> Self {
        let config_value: i32 = match env::var("CONFIG_VALUE") {
            Ok(val) => val.parse().unwrap_or(42),
            Err(_) => 42,
        };
        ConfiguredObject { value: config_value }
    }
}

static CONFIGURED_OBJECT: Lazy<ConfiguredObject> = Lazy::new(|| {
    println!("Initializing ConfiguredObject...");
    ConfiguredObject::new()
});

fn main() {
    println!("Using configured object: {}", CONFIGURED_OBJECT.value);
}

在这个示例中,ConfiguredObject 的初始化依赖于环境变量 CONFIG_VALUELazy 类型确保 ConfiguredObject 只在首次访问时初始化,并且 Once 保证了初始化的线程安全性。这样,无论在单线程还是多线程环境下,ConfiguredObject 都能根据运行时配置正确初始化。

Once 的性能考量

虽然 Once 提供了可靠的线程安全一次性初始化机制,但在性能敏感的场景中,也需要考虑其带来的开销。

原子操作的开销

Once 内部使用的 AtomicUsize 原子操作在现代硬件上已经非常高效,但仍然比普通的内存读写操作要慢。尤其是在频繁调用 call_once 的情况下,原子操作的开销可能会变得明显。例如,在一个性能关键的循环中,如果每次迭代都调用 call_once,即使初始化已经完成,原子操作的开销也会累积。

为了减少这种开销,可以尽量将 Once 的初始化放在程序启动阶段,或者在初始化完成后避免不必要的 call_once 调用。例如,可以在初始化完成后设置一个标志,在后续代码中直接检查这个标志,而不是每次都调用 call_once

初始化闭包的开销

call_once 接受的闭包中的代码也是性能考量的一部分。如果闭包中的初始化代码非常复杂或耗时,那么这将直接影响到初始化的性能。在这种情况下,可以考虑将复杂的初始化逻辑分解为多个步骤,或者使用异步初始化的方式来避免阻塞主线程或其他关键线程。

例如,对于一些需要网络请求或磁盘I/O的初始化操作,可以使用Rust的异步编程模型(如 async/await)来进行异步初始化,从而在初始化过程中不影响其他线程的执行。

Once 与其他同步原语的比较

在Rust中,除了 Once,还有其他同步原语可以用于解决初始化和线程安全问题,了解它们之间的差异有助于选择合适的工具。

OnceMutex

Mutex 是Rust中常用的互斥锁,用于保护共享资源的访问。与 Once 不同,Mutex 主要用于控制对共享资源的并发访问,而不是一次性初始化。

如果只是需要保护某个资源在多线程环境下的读写操作,Mutex 是合适的选择。但如果需要确保某个初始化过程只执行一次,Mutex 本身并不能提供这种保证。例如,使用 Mutex 来实现一次性初始化可能会导致多次初始化的问题:

use std::sync::Mutex;

static mut DATA: i32 = 0;
static MUTEX: Mutex<()> = Mutex::new(());

fn init_data() {
    let _lock = MUTEX.lock().unwrap();
    unsafe {
        if DATA == 0 {
            DATA = 42;
        }
    }
}

fn main() {
    let num_threads = 10;
    let threads: Vec<_> = (0..num_threads).map(|_| {
        thread::spawn(move || {
            init_data();
            unsafe {
                println!("Thread sees data: {}", DATA);
            }
        })
    }).collect();

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

在这个示例中,虽然使用了 Mutex 来保护对 DATA 的访问,但由于多个线程可能同时进入 if DATA == 0 块,仍然可能出现多次初始化的情况。而 Once 能够确保初始化只发生一次。

OnceCondvar

Condvar 是条件变量,用于线程间的同步通信,通常与 Mutex 结合使用。Condvar 主要用于线程等待某个条件满足后再继续执行,而不是一次性初始化。

例如,一个线程可能需要等待另一个线程完成某个任务后才能继续执行,这时可以使用 Condvar。但对于一次性初始化的需求,Condvar 并不是合适的工具。Once 专注于确保初始化的一次性和线程安全性,与 Condvar 的功能有明显区别。

总结 Once 的适用场景

Once 在Rust中是一个非常有用的工具,适用于多种场景:

  1. 多线程环境下的一次性初始化:无论是初始化全局资源、静态变量,还是其他需要确保只初始化一次的场景,Once 都能提供可靠的线程安全保证。
  2. 延迟初始化:结合 Lazy 类型,Once 可以实现延迟初始化,在首次访问时才进行初始化操作,并且保证初始化的线程安全性。
  3. 复杂初始化逻辑:当初始化过程涉及动态配置、资源获取等复杂操作时,Once 能够确保这些操作在多线程环境下正确执行且只执行一次。

通过深入理解 Once 的原理、使用方法和性能考量,开发者可以在Rust程序中更好地利用这一机制,编写高效、线程安全的代码。在实际应用中,根据具体场景选择合适的同步原语,并合理优化初始化过程,是编写高质量Rust多线程程序的关键。

希望通过本文的介绍,你对Rust的 Once 初始化与线程安全机制有了更深入的理解,并能在自己的项目中灵活运用。如果你在使用过程中遇到任何问题或有进一步的思考,欢迎与社区交流分享。

继续拓展关于 Once 的更多细节,我们来看一些更深入的场景。

Once 在跨模块初始化中的应用

在大型Rust项目中,代码通常会被组织成多个模块。在这种情况下,Once 对于跨模块的一次性初始化同样非常有用。

模块间共享资源的初始化

假设我们有一个项目,其中有多个模块需要使用同一个数据库连接。我们可以使用 Once 来确保数据库连接在整个项目中只初始化一次,并且在各个模块中都能安全地获取到已初始化的连接。

// db.rs
use std::sync::{Once, Mutex};

// 模拟数据库连接
struct DatabaseConnection {
    // 实际的数据库连接相关字段
}

impl DatabaseConnection {
    fn new() -> Self {
        println!("Connecting to database...");
        DatabaseConnection {}
    }
}

pub static DB_CONNECTION: Mutex<Option<DatabaseConnection>> = Mutex::new(None);
pub static INIT_DB: Once = Once::new();

pub fn get_database_connection() -> DatabaseConnection {
    INIT_DB.call_once(|| {
        let mut guard = DB_CONNECTION.lock().unwrap();
        *guard = Some(DatabaseConnection::new());
    });
    DB_CONNECTION.lock().unwrap().as_ref().unwrap().clone()
}

// module1.rs
use super::{get_database_connection, DB_CONNECTION};

pub fn module1_function() {
    let conn = get_database_connection();
    // 使用数据库连接进行操作
    let _guard = DB_CONNECTION.lock().unwrap();
    // 其他与数据库连接相关的操作
}

// module2.rs
use super::{get_database_connection, DB_CONNECTION};

pub fn module2_function() {
    let conn = get_database_connection();
    // 使用数据库连接进行操作
    let _guard = DB_CONNECTION.lock().unwrap();
    // 其他与数据库连接相关的操作
}

// main.rs
mod db;
mod module1;
mod module2;

fn main() {
    module1::module1_function();
    module2::module2_function();
}

在这个示例中,db.rs 模块定义了数据库连接的初始化逻辑,并通过 Once 确保连接只初始化一次。module1.rsmodule2.rs 模块都可以通过调用 get_database_connection 函数来获取已初始化的数据库连接,从而在不同模块间共享这个资源,同时保证了线程安全。

跨模块静态变量的初始化

类似地,对于跨模块的静态变量初始化,Once 也能发挥重要作用。假设我们有一个配置相关的静态变量,多个模块都需要使用:

// config.rs
use std::sync::{Once, Mutex};

// 假设这是一个配置结构体
struct Config {
    value: i32,
}

impl Config {
    fn new() -> Self {
        Config { value: 42 }
    }
}

pub static CONFIG: Mutex<Option<Config>> = Mutex::new(None);
pub static INIT_CONFIG: Once = Once::new();

pub fn get_config() -> Config {
    INIT_CONFIG.call_once(|| {
        let mut guard = CONFIG.lock().unwrap();
        *guard = Some(Config::new());
    });
    CONFIG.lock().unwrap().as_ref().unwrap().clone()
}

// module_a.rs
use super::{get_config, CONFIG};

pub fn module_a_function() {
    let config = get_config();
    // 使用配置进行操作
    let _guard = CONFIG.lock().unwrap();
    // 其他与配置相关的操作
}

// module_b.rs
use super::{get_config, CONFIG};

pub fn module_b_function() {
    let config = get_config();
    // 使用配置进行操作
    let _guard = CONFIG.lock().unwrap();
    // 其他与配置相关的操作
}

// main.rs
mod config;
mod module_a;
mod module_b;

fn main() {
    module_a::module_a_function();
    module_b::module_b_function();
}

在这个例子中,config.rs 模块负责初始化 Config 结构体,并通过 Once 确保其只初始化一次。module_a.rsmodule_b.rs 模块可以通过 get_config 函数获取已初始化的配置,实现了跨模块静态变量的线程安全初始化和共享。

Once 在错误处理场景中的应用

在实际编程中,初始化过程可能会失败,因此需要考虑如何在 Once 初始化机制中处理错误。

处理初始化闭包中的错误

call_once 方法接受的闭包通常不允许返回错误,因为 Once 的设计初衷是确保初始化只执行一次,而错误处理可能会导致多次尝试初始化。然而,我们可以通过一些变通的方法来处理初始化错误。

一种方法是在闭包中使用 Result 类型,并将错误存储在一个共享的变量中。例如:

use std::sync::{Once, Mutex};

// 模拟一个可能失败的初始化函数
fn initialize() -> Result<(), &'static str> {
    // 这里模拟一个可能失败的初始化操作
    if rand::random::<bool>() {
        Ok(())
    } else {
        Err("Initialization failed")
    }
}

static INIT_RESULT: Mutex<Option<Result<(), &'static str>>> = Mutex::new(None);
static INIT: Once = Once::new();

fn get_initialized() -> Result<(), &'static str> {
    INIT.call_once(|| {
        let result = initialize();
        let mut guard = INIT_RESULT.lock().unwrap();
        *guard = Some(result);
    });
    INIT_RESULT.lock().unwrap().as_ref().unwrap().clone()
}

fn main() {
    match get_initialized() {
        Ok(()) => println!("Initialization successful"),
        Err(e) => println!("Initialization failed: {}", e),
    }
}

在这个示例中,initialize 函数模拟了一个可能失败的初始化操作。INIT_RESULT 用于存储初始化结果,INIT 确保 initialize 只被调用一次。get_initialized 函数在调用 call_once 后返回存储在 INIT_RESULT 中的初始化结果,从而实现了对初始化错误的处理。

重试初始化

在某些情况下,我们可能希望在初始化失败时进行重试。虽然 Once 本身不支持直接重试,但我们可以通过额外的逻辑来实现。

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

// 模拟一个可能失败的初始化函数
fn initialize() -> Result<(), &'static str> {
    // 这里模拟一个可能失败的初始化操作
    if rand::random::<bool>() {
        Ok(())
    } else {
        Err("Initialization failed")
    }
}

static INIT_RESULT: Mutex<Option<Result<(), &'static str>>> = Mutex::new(None);
static INIT: Once = Once::new();

fn get_initialized() -> Result<(), &'static str> {
    let mut attempt = 0;
    loop {
        INIT.call_once(|| {
            let result = initialize();
            let mut guard = INIT_RESULT.lock().unwrap();
            *guard = Some(result);
        });
        let result = INIT_RESULT.lock().unwrap().as_ref().unwrap().clone();
        if result.is_ok() {
            return result;
        }
        if attempt >= 3 {
            return result;
        }
        attempt += 1;
        thread::sleep(Duration::from_secs(1));
    }
}

fn main() {
    match get_initialized() {
        Ok(()) => println!("Initialization successful"),
        Err(e) => println!("Initialization failed: {}", e),
    }
}

在这个版本中,get_initialized 函数在初始化失败时会进行最多3次重试,每次重试间隔1秒。通过这种方式,我们在 Once 的基础上实现了初始化失败时的重试机制。

Once 在异步编程中的应用

随着Rust异步编程模型的广泛应用,了解 Once 在异步场景中的使用也很重要。

异步初始化与 Once

在异步代码中,我们可能需要一次性初始化一些异步资源,如异步数据库连接池、异步网络客户端等。虽然 Once 本身是基于同步操作的,但我们可以通过一些技巧来实现异步一次性初始化。

一种常见的方法是使用 OnceCellasync_once 库。OnceCellstd::cell::OnceCell,它提供了一种类似 Once 的一次性初始化机制,但适用于非 'static 生命周期的值。async_once 库则进一步扩展了 OnceCell,使其支持异步初始化。

首先,添加依赖:

[dependencies]
async_once = "1.0"

然后,使用示例如下:

use async_once::OnceCell;
use std::sync::Arc;
use tokio::sync::Mutex;

// 模拟异步数据库连接
struct AsyncDatabaseConnection {
    // 实际的异步数据库连接相关字段
}

impl AsyncDatabaseConnection {
    async fn new() -> Self {
        println!("Connecting to async database...");
        AsyncDatabaseConnection {}
    }
}

static ASYNC_DB_CONNECTION: OnceCell<Arc<Mutex<AsyncDatabaseConnection>>> = OnceCell::new();

async fn get_async_database_connection() -> Arc<Mutex<AsyncDatabaseConnection>> {
    ASYNC_DB_CONNECTION.get_or_init(|| async {
        Arc::new(Mutex::new(AsyncDatabaseConnection::new().await))
    }).await
}

#[tokio::main]
async fn main() {
    let conn1 = get_async_database_connection().await;
    let conn2 = get_async_database_connection().await;
    assert!(Arc::ptr_eq(&conn1, &conn2));
}

在这个示例中,OnceCellasync_once 库结合使用,确保 AsyncDatabaseConnection 只被异步初始化一次。get_async_database_connection 函数使用 get_or_init 方法,该方法接受一个异步闭包,在首次调用时执行异步初始化操作,并返回已初始化的值。

异步线程安全与 Once

在异步多线程环境中,Once 的线程安全特性同样重要。async_once 库实现的异步一次性初始化也是线程安全的,这意味着在多个异步任务同时尝试初始化时,只有一个任务会执行初始化操作,其他任务会等待初始化完成。

例如,假设有多个异步任务需要访问同一个异步初始化的资源:

use async_once::OnceCell;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::task;

// 模拟异步数据库连接
struct AsyncDatabaseConnection {
    // 实际的异步数据库连接相关字段
}

impl AsyncDatabaseConnection {
    async fn new() -> Self {
        println!("Connecting to async database...");
        AsyncDatabaseConnection {}
    }
}

static ASYNC_DB_CONNECTION: OnceCell<Arc<Mutex<AsyncDatabaseConnection>>> = OnceCell::new();

async fn get_async_database_connection() -> Arc<Mutex<AsyncDatabaseConnection>> {
    ASYNC_DB_CONNECTION.get_or_init(|| async {
        Arc::new(Mutex::new(AsyncDatabaseConnection::new().await))
    }).await
}

async fn async_task() {
    let conn = get_async_database_connection().await;
    // 使用异步数据库连接进行操作
}

#[tokio::main]
async fn main() {
    let num_tasks = 10;
    let tasks: Vec<_> = (0..num_tasks).map(|_| {
        task::spawn(async move {
            async_task().await;
        })
    }).collect();

    for task in tasks {
        task.await.unwrap();
    }
}

在这个示例中,10个异步任务同时尝试获取异步数据库连接。由于 OnceCellasync_once 库的线程安全机制,AsyncDatabaseConnection 只会被初始化一次,所有任务都能安全地获取到已初始化的连接。

通过以上对 Once 在不同场景下的应用介绍,相信你对 Once 的功能和使用方法有了更全面的认识。在实际开发中,根据具体需求合理运用 Once,能够有效地提高代码的质量和性能,确保多线程和异步环境下的程序正确性。