পাঠ ১৬.৩

Shared-state concurrency

Shared-State Concurrency

Channel single ownership-এর মতো — value পাঠানোর পর তুমি আর use করতে পারো না। কিন্তু কখনো একাধিক thread একই memory access করতে চায় — সেটাই shared-state concurrency। এতে data race-এর ঝুঁকি বেশি, তবে Rust-এর type system আর ownership rule অনেকটাই সাহায্য করে।

Mutex<T> — একসাথে একজন

Mutex = mutual exclusion। যেকোনো সময়ে শুধু একটাই thread data access করতে পারবে। প্রতিটা thread-কে দুটো নিয়ম মানতে হয়:

  1. Data ব্যবহারের আগে lock acquire করতে হবে।
  2. কাজ শেষ হলে unlock — নাহলে অন্য কেউ পাবে না।

Real-world analogy — panel discussion-এ একটাই microphone। কেউ কথা বলতে চাইলে সিগন্যাল দেয়, mic পায়, কথা শেষে পরের জনের হাতে দেয়। কেউ ভুলে গেলে আর কারও কথা বলা হবে না।

Single-threaded Mutex API

src/main.rsrust
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {m:?}");
}

Mutex::new দিয়ে value wrap; m.lock() call করলে current thread block হয়ে অপেক্ষা করে যতক্ষণ না lock পায়।

lock একটা LockResult return করে — wrap করা থাকে MutexGuard। MutexGuard:

  • Deref implement করে — তাই *num দিয়ে inner data access।
  • Drop implement করে — scope ছাড়লে automatic unlock। ভুলে unlock করার ভয় নেই।

Lock-হোল্ডিং thread panic করলে lock() poisoned হয়ে error দেয় — তাই এখানে unwrap

একাধিক thread-এ Mutex share — সমস্যা ১

src/main.rsrust
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}
compile errortext
error[E0382]: borrow of moved value: `counter`

প্রথম iteration-এই counter closure-এ move হয়ে যায়। দ্বিতীয় iteration-এ আবার move করতে গিয়ে error। দরকার — multiple ownership

Rc<T> দিয়ে চেষ্টা — ব্যর্থ

src/main.rsrust
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Rc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Rc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}
compile errortext
error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
  = help: within the closure, the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`

Rc<T> thread-safe না। এর reference count update concurrency primitive ছাড়া হয় — দু'টা thread একসাথে modify করলে count ভুল, leak, বা early drop হতে পারে।

সমাধান — Arc<T>

Arc-এর "a" মানে atomic — atomically reference-counted। API Rc<T>-এর মতোই, কিন্তু count-update thread-safe atomic operation দিয়ে।

src/main.rsrust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}
Result: 10

কেন তবে সরাসরি default-এ Arc না? কারণ atomic operation-এর কিছু overhead আছে — single-threaded code-এ সেই খরচ দেওয়ার মানে নেই। যেখানে thread safety দরকার, শুধু সেখানেই Arc

RefCell/Rc বনাম Mutex/Arc

লক্ষ করো — counter immutable ছিল, কিন্তু আমরা ভেতরের value modify করেছি। এটাই interior mutabilityRefCell<T> Rc<T>-এর ভেতরে যেমন কাজ করে, Mutex<T> Arc<T>-এর ভেতরে তেমনি।

  • Rc/RefCell — single-threaded; reference cycle থেকে memory leak-এর ঝুঁকি।
  • Arc/Mutex — multi-threaded; deadlock-এর ঝুঁকি (দু'টা thread একে অপরের lock-এর জন্য চিরকাল অপেক্ষা)।

Deadlock যেমন reference cycle, তেমনি — Rust সরাসরি ধরতে পারে না; logic সাবধানে structure করতে হয়।

Note: simple counter-এর জন্য std::sync::atomic module-এর atomic type (যেমন AtomicUsize) প্রায়শই Mutex-এর চেয়ে সস্তা।

এই পাঠ থেকে যা শিখলে

  • Mutex<T> shared data-কে exclusive access-এ guard করে; lock() ছাড়া access অসম্ভব।
  • MutexGuard-এর Drop scope শেষে automatic unlock।
  • একাধিক thread-এ Mutex share-এর জন্য Arc<Mutex<T>> pattern; Rc thread-safe না।
  • Mutex/Arc interior mutability দেয় — immutable wrapper-এর ভেতরের data বদলানো যায়।
  • Deadlock — দু'টা thread পরস্পরের lock-এর জন্য চিরকাল wait; logic সাবধানে design।
  • Counter-জাতীয় কাজে atomic type প্রায়ই Mutex-এর চেয়ে সাশ্রয়ী।