RwLock în Rust

Lacăt cititori-scriitor: mai multe fire pot citi în același timp, dar scrierea are nevoie de acces exclusiv.

Sintaxă
use std::sync::RwLock;

const fn new(t: T) -> RwLock<T>
fn read(&self) -> LockResult<RwLockReadGuard<'_, T>>
fn write(&self) -> LockResult<RwLockWriteGuard<'_, T>>

Ce face RwLock

RwLock<T> seamănă cu Mutex, dar face diferența între citire și scriere. read() dă un guard de citire; pot exista mulți în paralel. write() dă un guard de scriere, care așteaptă până nu mai există niciun cititor sau scriitor.

Este util pentru date citite des și modificate rar: o configurare, un cache, un tabel de prețuri. Dacă scrierile sunt frecvente, un Mutex simplu este de obicei la fel de bun. Ca la Mutex, rezultatul este Result din cauza otrăvirii, deci se scrie .unwrap().

Parametri

Parametru Descriere
t Valoarea protejată (la new).

Ce returnează

read: guard de citire (ca &T). write: guard de scriere (ca &mut T).

Exemple

Exemplul 1: Configurare citită de mai multe fire

Cele trei fire citesc în paralel. Rezultatele vin prin join, în ordine. Scrierea se face după.

use std::sync::RwLock;
use std::thread;

fn main() {
    let curs_euro = RwLock::new(4.97);

    thread::scope(|s| {
        let fire: Vec<_> = [100.0, 250.0, 40.0]
            .into_iter()
            .map(|lei| {
                let curs = &curs_euro;
                s.spawn(move || {
                    let c = curs.read().unwrap();
                    format!("{lei} lei = {:.2} euro", lei / *c)
                })
            })
            .collect();
        for f in fire {
            println!("{}", f.join().unwrap());
        }
    });

    *curs_euro.write().unwrap() = 5.02;
    println!("curs nou: {}", curs_euro.read().unwrap());
}

Rezultat

100 lei = 20.12 euro
250 lei = 50.30 euro
40 lei = 8.05 euro
curs nou: 5.02

Exemplul 2: Doi cititori în același timp

Al doilea read reușește chiar dacă primul guard există încă; try_write eșuează cât timp există cititori.

use std::sync::RwLock;

fn main() {
    let orase = RwLock::new(vec!["Iași", "Cluj"]);

    let r1 = orase.read().unwrap();
    let r2 = orase.read().unwrap();
    println!("r1 = {:?}, r2 = {:?}", *r1, *r2);
    println!("pot scrie acum: {}", orase.try_write().is_ok());
    drop(r1);
    drop(r2);

    orase.write().unwrap().push("Sibiu");
    println!("după scriere: {:?}", orase.read().unwrap());
}

Rezultat

r1 = ["Iași", "Cluj"], r2 = ["Iași", "Cluj"]
pot scrie acum: false
după scriere: ["Iași", "Cluj", "Sibiu"]

Greșeli frecvente

  • Ceri write() în același fir în care ții încă un guard de read(): firul se blochează pentru totdeauna.
  • Folosești RwLock pentru date modificate des: costul de gestionare este mai mare decât la Mutex și nu câștigi nimic.

Comenzi înrudite

  • Mutex: Protejează o valoare folosită din mai multe fire: lock() așteaptă până când firul curent este singurul cu acces.
  • Arc: Varianta lui Rc care poate fi împărțită între fire: contorul de proprietari este actualizat atomic.
  • OnceLock: Celulă care primește o valoare o singură dată, sigur între fire; ideală pentru valori globale calculate la prima folosire.