পাঠ ১৬.১

Thread দিয়ে code একসাথে চালানো

Using Threads to Run Code Simultaneously

আধুনিক operating system-এ একটা program একটা process-এর ভেতরে চলে; OS অনেক process একসাথে handle করে। কিন্তু একটা program-এর ভেতরেও আলাদা আলাদা অংশ একসাথে চালানো যায় — এদের বলে thread। যেমন একটা web server একাধিক thread-এ একসাথে অনেক request serve করতে পারে।

কেন thread, কেন সাবধান

Thread দিয়ে কাজ ভাগ করে দিলে performance বাড়ে — কিন্তু complexity-ও বাড়ে। Thread-এর order নিশ্চিত নয়, এর থেকে কিছু সাধারণ সমস্যা:

  • Race condition — thread-গুলো inconsistent order-এ data access করছে।
  • Deadlock — দু'টা thread একে অপরের জন্য অপেক্ষা করছে, কেউই এগোতে পারছে না।
  • শুধু কিছু পরিস্থিতিতে দেখা দেওয়া bug — reliable-ভাবে reproduce করা কঠিন।

Rust এই সমস্যাগুলো অনেকটাই compile time-এ ধরে। তবে multithreaded code-এ এখনো সাবধানে চিন্তা করা দরকার।

Rust-এর standard library 1:1 thread model ব্যবহার করে — প্রতিটা language-level thread-এর জন্য একটা OS thread। অন্য model (যেমন green thread) চাইলে crate ecosystem-এ পাওয়া যায়।

নতুন thread — thread::spawn

নতুন thread তৈরি করতে thread::spawn-এ একটা closure পাস করো — যেই code আলাদা thread-এ চলবে।

src/main.rsrust
use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}

সম্ভাব্য output — চালালে প্রতিবার একটু আলাদা হতে পারে:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

গুরুত্বপূর্ণ ব্যাপার — main thread শেষ হয়ে গেলে spawned thread-ও বন্ধ হয়ে যায়, তার কাজ শেষ হোক বা না হোক। তাই উপরের output-এ spawned thread 1..10 পর্যন্ত print করতে চাইলেও 5-এ থেমে গেছে।

thread::sleep current thread-কে অল্প সময়ের জন্য থামায় — অন্য thread-কে চালানোর সুযোগ দেয়। তবে কোন thread কখন চলবে — সেটা OS scheduler ঠিক করে, guarantee নেই।

সবগুলো thread শেষ হওয়ার অপেক্ষা — join

উপরের code-এ spawned thread পুরোপুরি চলবে কি না — সেটার guarantee নেই। Spawn-এর return-করা JoinHandle<T> সংরক্ষণ করে, তার join method কল করলে current thread block হয়ে অপেক্ষা করবে যতক্ষণ না spawned thread শেষ হয়:

src/main.rsrust
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

এখন main thread তার নিজের loop শেষ হলেই থামে না — spawned thread শেষ হওয়া পর্যন্ত অপেক্ষা করে:

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

join-এর জায়গা matter করে

handle.join()-কে main-এর loop-এর আগে বসালে interleave হবে না:

src/main.rsrust
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

Main thread আগে অপেক্ষা করছে — spawned thread পুরোপুরি শেষ হওয়ার পরে নিজের loop শুরু করে। ছোট্ট পার্থক্য — কিন্তু concurrency বদলে যায়।

move closure থ্রেডে

Spawned thread-এ main thread-এর data ব্যবহার করতে হলে closure সেগুলো capture করবে। কিন্তু borrow করে capture করলে borrow checker ঝামেলায় পড়ে — কতদিন ওই reference valid থাকবে সেটা spawn জানে না।

src/main.rsrust
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}
compile errortext
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {v:?}");
  |                                     - `v` is borrowed here
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

ভাবলেও বোঝা যায় কেন এটা risky — main thread যদি এর মধ্যে v drop করে দেয়:

src/main.rsrust
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    drop(v); // oh no!

    handle.join().unwrap();
}

সমাধান — move keyword। Closure-কে force করে ব্যবহৃত value-গুলোর ownership নিতে:

src/main.rsrust
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}

এখন v-এর ownership spawned thread-এ চলে যায়। Main thread আর সেটা ব্যবহার করতে পারবে না — drop-ও করতে পারবে না। Ownership rule এই situation-কেও safe রাখে।

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

  • thread::spawn(closure) দিয়ে নতুন OS thread।
  • Main thread শেষ হলে সব spawned thread-ও থেমে যায় — যদি না তুমি JoinHandle-এ join করো।
  • join current thread-কে block করে রাখে target thread শেষ না হওয়া পর্যন্ত।
  • Spawn-এ পাঠানো closure environment-এর reference নিতে পারে না — move দিয়ে ownership transfer করতে হয়।
  • Rust-এর 1:1 thread model — language-level thread = OS thread।
  • Race, deadlock, ordering — concurrency-র সাধারণ pitfall; thread::sleep-এ scheduling নিয়ন্ত্রণ করো না, শুধু সুযোগ দাও।