İçeriğe geç
Educora
İleri22 dk9 / 10

Akıllı işaretçiler ve eşzamanlılık

`Box`, `Rc` ve `RefCell` akıllı işaretçilerini öğren; ardından iş parçacıkları, kanallar ve `Arc<Mutex<T>>` ile veri yarışı olmadan paralel kod yaz.

Kendini test et
Bu derste öğreneceklerin
  • Box ile bir değeri öbekte saklamak ve özyinelemeli bir tür kurmak
  • Rc ile birden çok sahip, RefCell ile iç değiştirilebilirlik oluşturmak
  • İş parçacıklarını move closure'larıyla başlatmak ve sonuçları join ile toplamak
  • Kanallar ve Arc<Mutex<T>> ile iş parçacıkları arasında veri paylaşmak

Şimdiye kadar her değerin tek bir sahibi vardı. Ama bazen başka şeyler gerekir: boyutu önceden bilinmeyen özyinelemeli bir yapı, aynı şarkının birkaç çalma listesinde olması, ortak veriyi değiştirmek ya da işi işlemcinin birkaç çekirdeğine bölmek. Bunun için Rust'ta akıllı işaretçiler ve eşzamanlılık (concurrency) araçları vardır; onlar da sahiplik kurallarına uyar.

`Box<T>`: öbekte bir değer

Box::new(x) bir değeri öbeğe yerleştirir; yığında yalnızca sabit boyutlu bir işaretçi kalır. Kutunun sahibi kapsam dışına çıkınca öbekteki değer de otomatik olarak düşürülür. Box üç durumda gerekir: boyutu derleme sırasında bilinmeyen özyinelemeli türler, Box<dyn Trait> gibi trait nesneleri ve yığında taşınması pahalı olan büyük veriler.

E0072 hatası
enum List {
    Node(i32, List),
    Empty,
}
`Box` ile doğru
enum List {
    Node(i32, Box<List>),
    Empty,
}
Solda List kendi içinde bir List barındırıyor; bu yüzden derleyici şunu yazar: recursive type List has infinite size. Box ise sabit boyutlu bir işaretçidir ve döngüyü kırar.
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));
}
Beklenen çıktı
boxed + 1 = 6
sum = 6

`Rc<T>`: birden çok sahip

Rc (reference counting, referans sayma) bir değerin birden çok sahibi olmasını sağlar. Rc::clone(&x) veriyi kopyalamaz, yalnızca bir sayacı bir artırır; bu çok ucuzdur. Her sahip düşürüldüğünde sayaç azalır, sıfıra ulaşınca değer düşürülür. Rc değeri yalnızca okumaya izin verir ve yalnızca tek bir iş parçacığı içinde çalışır.

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}");
}
Beklenen çıktı
owners: 1
owners: 3
b plays lesson.mp3
owners: 2
a plays lesson.mp3
playlist_b iç blok bitince düşürüldü ve sayaç 3'ten 2'ye indi. Metnin kendisi bellekte yalnızca bir kez saklanır.

`RefCell<T>`: iç değiştirilebilirlik

RefCell, ödünç alma kurallarını derleme sırasında değil, program çalışırken denetler. borrow() okumak için, borrow_mut() değiştirmek için ödünç verir. Bu, değiştirilemez bir referansın arkasındaki değeri değiştirmeyi mümkün kılar. Rc<RefCell<T>> birleşimi tipik bir çözümdür: birkaç sahip ortak veriyi hem okuyabilir hem de değiştirebilir.

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}");
}
Beklenen çıktı
[80, 95, 70, 100]
total = 345
TürSahiplerDeğiştirmeİş parçacıkları arası paylaşım
Box<T>birsahip mut iseevet
Rc<T>birden çokhayırhayır
RefCell<T>birevet, çalışma sırasında denetlenirhayır — başka bir iş parçacığına taşınabilir ama paylaşılamaz (Sync değildir)
Arc<T>birden çokhayırevet
Arc<Mutex<T>>birden çokevet, kilit aracılığıylaevet

İş parçacıkları

thread::spawn, bir closure'ı yeni bir iş parçacığında çalıştırır ve bir JoinHandle döndürür. join(), iş parçacığının bitmesini bekler ve sonucunu verir. Closure'ın önündeki move, kullandığı değişkenlerin sahipliğini iş parçacığına devreder. Dikkat: iş parçacıklarının kendi içinden yazdırdığı satırların sırası her çalıştırmada farklı olabilir. Bu yüzden aşağıda iş parçacıkları sonuç döndürüyor, main ise onları sabit bir sırayla yazdırıyor.

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}");
    }
}
Beklenen çıktı
thread 1: sum = 500500
thread 2: sum = 2001000
thread 3: sum = 4501500
E0373 hatası
use std::thread;

fn main() {
    let names = vec!["Aysel", "Murad"];
    let handle = thread::spawn(|| {
        println!("{:?}", names);
    });
    handle.join().unwrap();
}
`move` ile doğru
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. İş parçacığı main'den uzun yaşayabilir; bu yüzden bir referans yetmez. Sağdaki program ["Aysel", "Murad"] yazdırır.

Kanallar ve `Arc<Mutex<T>>`

İş parçacıkları arasında veri paylaşmanın iki yolu vardır. Birincisi kanaldır: mpsc::channel() bir gönderici (tx) ve bir alıcı (rx) oluşturur; bir iş parçacığı send ile gönderir, diğeri alır. Alıcı for ile dolaşılabilir; tüm göndericiler düşürülünce döngü biter. Tek bir göndericinin mesajları gönderildiği sırayla gelir.

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");
}
Beklenen çıktı
got: download
got: unpack
got: install
done

İkinci yol ortak durumdur. Arc, Rc'nin iş parçacıkları için güvenli (atomik) sürümüdür; Mutex ise bir kilittir: lock() çağıran iş parçacığı veriye tek başına erişir, döndürülen koruyucu (guard) kapsam dışına çıkınca kilit otomatik olarak açılır. Birlikte Arc<Mutex<T>>, birkaç iş parçacığının aynı değeri sırayla değiştirmesini sağlar.

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());
}
Beklenen çıktı
Result: 10
İş parçacıkları hangi sırayla çalışırsa çalışsın, kilit sayesinde sonuç her zaman 10'dur. Her iş parçacığı Arc'ın kendi kopyasını alır; bu, gölgelemeyle yazılmıştır: let counter = Arc::clone(&counter);.

Önemli noktalar

  • Box<T> bir değeri öbekte tutar; özyinelemeli türler ve dyn Trait için gereklidir.
  • Rc<T> tek bir iş parçacığında birden çok sahip sağlar; Rc::clone yalnızca sayacı artırır.
  • RefCell<T> ödünç kurallarını çalışma sırasında denetler; ihlal paniğe yol açar.
  • thread::spawn(move || ...) bir iş parçacığı başlatır, join() sonucunu bekler; yazdırmayı sabit sırayla main içinden yap.
  • Kanallar mesaj iletir, Arc<Mutex<T>> ise iş parçacıkları arasında ortak ve değiştirilebilir durum sağlar.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
enum List { Node(i32, List), Empty } neden derlenmez?