Məzmuna keç
Educora
İrəli22 dəq9 / 10

Ağıllı göstəricilər və eyni vaxtlı proqramlaşdırma

`Box`, `Rc` və `RefCell` ağıllı göstəricilərini öyrən, sonra axınlar (thread), kanallar və `Arc<Mutex<T>>` ilə verilənlər yarışı olmadan paralel kod yaz.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • Box ilə qiyməti heap-də saxlamaq və rekursiv tip qurmaq
  • Rc ilə bir neçə sahib, RefCell ilə daxili dəyişkənlik yaratmaq
  • Axınları move qapanmaları ilə işə salmaq və join ilə nəticələri toplamaq
  • Kanallar və Arc<Mutex<T>> ilə axınlar arasında məlumat paylaşmaq

İndiyə qədər hər qiymətin tək sahibi var idi. Amma bəzən başqa şey lazım olur: ölçüsü əvvəlcədən bilinməyən rekursiv struktur, eyni mahnının bir neçə pleylistdə olması, ortaq məlumatı dəyişmək və ya işi prosessorun bir neçə nüvəsinə bölmək. Bunun üçün Rust-da ağıllı göstəricilər və eyni vaxtlı proqramlaşdırma (concurrency) alətləri var — və onlar da sahiblik qaydalarına tabedir.

`Box<T>`: qiymət heap-də

Box::new(x) qiyməti heap-ə yerləşdirir, stekdə isə yalnız sabit ölçülü göstərici qalır. Qutunun sahibi görünmə sahəsindən çıxanda heap-dəki qiymət də avtomatik silinir. Box üç halda lazımdır: ölçüsü kompilyasiya zamanı bilinməyən rekursiv tiplər, Box<dyn Trait> kimi trait obyektləri və stekdə köçürülməsi baha olan böyük məlumat.

Xəta E0072
enum List {
    Node(i32, List),
    Empty,
}
`Box` ilə düzgün
enum List {
    Node(i32, Box<List>),
    Empty,
}
Solda List öz içində List saxlayır, ona görə kompilyator yazır: recursive type List has infinite size. Box isə sabit ölçülü göstəricidir və dövrəni qırır.
Rust
enum List {
    Node(i32, Box<List>),
    Empty,
}

use List::{Empty, Node};

fn sum(list: &List) -> i32 {
    match list {
        Node(value, next) => value + sum(next),
        Empty => 0,
    }
}

fn main() {
    let boxed = Box::new(5);
    println!("boxed + 1 = {}", *boxed + 1);

    let list = Node(1, Box::new(Node(2, Box::new(Node(3, Box::new(Empty))))));
    println!("sum = {}", sum(&list));
}
Gözlənilən nəticə
boxed + 1 = 6
sum = 6

`Rc<T>`: bir neçə sahib

Rc (reference counting — istinadların sayılması) bir qiymətin bir neçə sahibi olmasına imkan verir. Rc::clone(&x) məlumatın surətini çıxarmır, sadəcə sayğacı bir vahid artırır — bu, çox ucuzdur. Hər sahib silinəndə sayğac azalır, sıfıra çatanda isə qiymət silinir. Rc qiyməti yalnız oxumağa icazə verir və yalnız bir axın daxilində işləyir.

Rust
use std::rc::Rc;

fn main() {
    let song = Rc::new(String::from("lesson.mp3"));
    println!("owners: {}", Rc::strong_count(&song));

    let playlist_a = Rc::clone(&song);
    {
        let playlist_b = Rc::clone(&song);
        println!("owners: {}", Rc::strong_count(&song));
        println!("b plays {playlist_b}");
    }
    println!("owners: {}", Rc::strong_count(&song));
    println!("a plays {playlist_a}");
}
Gözlənilən nəticə
owners: 1
owners: 3
b plays lesson.mp3
owners: 2
a plays lesson.mp3
playlist_b daxili blok bitəndə silindi və sayğac 3-dən 2-yə düşdü. Mətnin özü yaddaşda cəmi bir dəfə saxlanılır.

`RefCell<T>`: daxili dəyişkənlik

RefCell borc qaydalarını kompilyasiya zamanı yox, proqram işləyərkən yoxlayır. borrow() oxumaq üçün, borrow_mut() dəyişmək üçün borc verir. Bu, dəyişməz istinad arxasındakı qiyməti dəyişməyə imkan verir. Rc<RefCell<T>> birləşməsi tipik həlldir: bir neçə sahib ortaq məlumatı həm oxuya, həm də dəyişə bilir.

Rust
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let scores = Rc::new(RefCell::new(vec![80, 95]));
    let teacher = Rc::clone(&scores);

    teacher.borrow_mut().push(70);
    scores.borrow_mut().push(100);

    println!("{:?}", scores.borrow());
    let total: i32 = teacher.borrow().iter().sum();
    println!("total = {total}");
}
Gözlənilən nəticə
[80, 95, 70, 100]
total = 345
TipSahiblərDəyişməkAxınlar arası paylaşma
Box<T>birsahib mut-dursabəli
Rc<T>bir neçəyoxyox
RefCell<T>birbəli, işləmə zamanı yoxlanıryox — başqa axına köçürmək olar, paylaşmaq olmaz (Sync deyil)
Arc<T>bir neçəyoxbəli
Arc<Mutex<T>>bir neçəbəli, kilid iləbəli

Axınlar (threads)

thread::spawn qapanmanı yeni axında işə salır və JoinHandle qaytarır. join() axının bitməsini gözləyir və onun nəticəsini verir. Qapanmanın qarşısındakı move istifadə etdiyi dəyişənlərin sahibliyini axına ötürür. Diqqət: axınların öz daxilindən çap etdiyi sətirlərin sırası hər işə salmada fərqli ola bilər. Ona görə aşağıda axınlar nəticə qaytarır, main isə onları sabit sırada çap edir.

Rust
use std::thread;

fn main() {
    let mut handles = Vec::new();
    for id in 1..=3_u64 {
        let handle = thread::spawn(move || {
            let sum: u64 = (1..=id * 1000).sum();
            (id, sum)
        });
        handles.push(handle);
    }
    for handle in handles {
        let (id, sum) = handle.join().unwrap();
        println!("thread {id}: sum = {sum}");
    }
}
Gözlənilən nəticə
thread 1: sum = 500500
thread 2: sum = 2001000
thread 3: sum = 4501500
Xəta E0373
use std::thread;

fn main() {
    let names = vec!["Aysel", "Murad"];
    let handle = thread::spawn(|| {
        println!("{:?}", names);
    });
    handle.join().unwrap();
}
`move` ilə düzgün
use std::thread;

fn main() {
    let names = vec!["Aysel", "Murad"];
    let handle = thread::spawn(move || {
        println!("{:?}", names);
    });
    handle.join().unwrap();
}
Solda: closure may outlive the current function, but it borrows names. Axın main-dən uzun yaşaya bilər, ona görə istinad kifayət deyil. Sağdakı proqram ["Aysel", "Murad"] çap edir.

Kanallar və `Arc<Mutex<T>>`

Axınlar arasında məlumatı iki üsulla paylaşırlar. Birincisi kanaldır: mpsc::channel() ötürücü (tx) və qəbuledici (rx) yaradır, bir axın send ilə göndərir, digəri alır. Qəbuledicini for ilə gəzmək olar — bütün ötürücülər silinəndə dövr bitir. Tək göndərənin mesajları göndərildiyi sırada gəlir.

Rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    let worker = thread::spawn(move || {
        for step in ["download", "unpack", "install"] {
            tx.send(step).unwrap();
        }
    });
    for message in rx {
        println!("got: {message}");
    }
    worker.join().unwrap();
    println!("done");
}
Gözlənilən nəticə
got: download
got: unpack
got: install
done

İkinci üsul ortaq vəziyyətdir. Arc — Rc-nin axınlar üçün təhlükəsiz (atomar) variantıdır, Mutex isə kilidir: lock() çağıran axın məlumata tək başına sahib olur, qaytarılan qoruyucu (guard) görünmə sahəsindən çıxanda kilid avtomatik açılır. Birlikdə Arc<Mutex<T>> bir neçə axının eyni qiyməti növbə ilə dəyişməsinə imkan verir.

Rust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut n = counter.lock().unwrap();
            *n += 1;
        }));
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Result: {}", *counter.lock().unwrap());
}
Gözlənilən nəticə
Result: 10
Axınlar hansı sırada işləsə də, kilid sayəsində nəticə həmişə 10-dur. Hər axın Arc-ın öz surətini alır — bu, kölgələmə ilə yazılıb: let counter = Arc::clone(&counter);.

Əsas fikirlər

  • Box<T> qiyməti heap-də saxlayır; rekursiv tiplər və dyn Trait üçün lazımdır.
  • Rc<T> bir axında bir neçə sahib verir; Rc::clone yalnız sayğacı artırır.
  • RefCell<T> borc qaydalarını işləmə zamanı yoxlayır; pozuntu panikaya səbəb olur.
  • thread::spawn(move || ...) axın yaradır, join() nəticəni gözləyir; çapı sabit sırada main-də et.
  • Kanallar mesaj ötürür, Arc<Mutex<T>> isə axınlar arasında ortaq dəyişən vəziyyət verir.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
enum List { Node(i32, List), Empty } niyə kompilyasiya olunmur?