একই সাথে কোড চালানোর জন্য থ্রেড ব্যবহার করা
বেশিরভাগ আধুনিক অপারেটিং সিস্টেমে, একটি প্রোগ্রামের কোড একটি প্রসেস (process) এর মধ্যে চলে এবং অপারেটিং সিস্টেম একসাথে একাধিক প্রসেস পরিচালনা করে। একটি প্রোগ্রামের মধ্যেও, আপনার স্বাধীন অংশ থাকতে পারে যা একই সাথে চলে। এই স্বাধীন অংশগুলো চালানোর ফিচারগুলোকে থ্রেড (threads) বলা হয়। উদাহরণস্বরূপ, একটি ওয়েব সার্ভারে একাধিক থ্রেড থাকতে পারে যাতে এটি একই সময়ে একাধিক অনুরোধের উত্তর দিতে পারে।
আপনার প্রোগ্রামের কম্পিউটেশনকে একাধিক থ্রেডে বিভক্ত করে একই সময়ে একাধিক কাজ চালানো পারফরম্যান্স উন্নত করতে পারে, তবে এটি জটিলতাও বাড়ায়। কারণ থ্রেডগুলো একই সাথে চলতে পারে, আপনার কোডের বিভিন্ন অংশের কোন অংশ কোন ক্রমে চলবে তার কোনো অন্তর্নিহিত গ্যারান্টি নেই। এটি বিভিন্ন সমস্যার কারণ হতে পারে, যেমন:
- রেস কন্ডিশন (Race conditions), যেখানে থ্রেডগুলো অসংগত ক্রমে ডেটা বা রিসোর্স অ্যাক্সেস করে।
- ডেডলক (Deadlocks), যেখানে দুটি থ্রেড একে অপরের জন্য অপেক্ষা করে, উভয় থ্রেডকে চলতে বাধা দেয়।
- এমন বাগ যা শুধুমাত্র নির্দিষ্ট পরিস্থিতিতে ঘটে এবং নির্ভরযোগ্যভাবে পুনরুৎপাদন এবং ঠিক করা কঠিন।
Rust থ্রেড ব্যবহারের নেতিবাচক প্রভাবগুলো কমানোর চেষ্টা করে, কিন্তু একটি মাল্টিথ্রেডেড প্রেক্ষাপটে প্রোগ্রামিং করার জন্য এখনও সতর্ক চিন্তাভাবনা এবং একটি কোড কাঠামোর প্রয়োজন যা একটি একক থ্রেডে চলা প্রোগ্রামগুলোর থেকে ভিন্ন।
প্রোগ্রামিং ল্যাঙ্গুয়েজগুলো বিভিন্ন উপায়ে থ্রেড ইমপ্লিমেন্ট করে, এবং অনেক অপারেটিং সিস্টেম একটি API সরবরাহ করে যা প্রোগ্রামিং ল্যাঙ্গুয়েজ নতুন থ্রেড তৈরি করার জন্য কল করতে পারে। Rust স্ট্যান্ডার্ড লাইব্রেরি থ্রেড ইমপ্লিমেন্টেশনের জন্য একটি 1:1 মডেল ব্যবহার করে, যেখানে একটি প্রোগ্রাম প্রতিটি ল্যাঙ্গুয়েজ থ্রেডের জন্য একটি অপারেটিং সিস্টেম থ্রেড ব্যবহার করে। এমন কিছু ক্রেট আছে যা থ্রেডিংয়ের অন্যান্য মডেল ইমপ্লিমেন্ট করে যা 1:1 মডেলের থেকে ভিন্ন ট্রেড-অফ করে। (Rust-এর অ্যাসিঙ্ক সিস্টেম, যা আমরা পরবর্তী অধ্যায়ে দেখব, কনকারেন্সির জন্য আরেকটি পদ্ধতি প্রদান করে।)
spawn
দিয়ে একটি নতুন থ্রেড তৈরি করা
একটি নতুন থ্রেড তৈরি করতে, আমরা thread::spawn
ফাংশনটি কল করি এবং এটিকে একটি ক্লোজার (closure) পাস করি (আমরা অধ্যায় ১৩-এ ক্লোজার নিয়ে আলোচনা করেছি) যেখানে আমরা নতুন থ্রেডে যে কোডটি চালাতে চাই তা থাকে। তালিকা ১৬-১ এর উদাহরণটি একটি প্রধান থ্রেড থেকে কিছু টেক্সট এবং একটি নতুন থ্রেড থেকে অন্য টেক্সট প্রিন্ট করে।
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)); } }
লক্ষ্য করুন যে যখন একটি Rust প্রোগ্রামের প্রধান থ্রেড শেষ হয়ে যায়, তখন সমস্ত স্পন (spawned) করা থ্রেড বন্ধ হয়ে যায়, তারা তাদের কাজ শেষ করুক বা না করুক। এই প্রোগ্রামের আউটপুট প্রতিবার কিছুটা ভিন্ন হতে পারে, তবে এটি নিম্নলিখিতর মতো দেখাবে:
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!
thread::sleep
এর কলগুলো একটি থ্রেডকে তার এক্সিকিউশন অল্প সময়ের জন্য থামাতে বাধ্য করে, যা অন্য একটি থ্রেডকে চলার সুযোগ দেয়। থ্রেডগুলো সম্ভবত পর্যায়ক্রমে চলবে, তবে এর কোনো নিশ্চয়তা নেই: এটি আপনার অপারেটিং সিস্টেম কীভাবে থ্রেডগুলোকে সময় নির্ধারণ করে তার উপর নির্ভর করে। এই রানে, প্রধান থ্রেডটি প্রথমে প্রিন্ট করেছে, যদিও স্পন করা থ্রেডের প্রিন্ট স্টেটমেন্টটি কোডে প্রথমে দেখা যায়। এবং যদিও আমরা স্পন করা থ্রেডটিকে i
9
না হওয়া পর্যন্ত প্রিন্ট করতে বলেছিলাম, প্রধান থ্রেড বন্ধ হয়ে যাওয়ার আগে এটি কেবল 5
পর্যন্ত পৌঁছেছে।
আপনি যদি এই কোডটি চালান এবং শুধুমাত্র প্রধান থ্রেডের আউটপুট দেখতে পান, বা কোনো ওভারল্যাপ না দেখেন, তাহলে থ্রেডগুলোর মধ্যে অপারেটিং সিস্টেমকে স্যুইচ করার জন্য আরও সুযোগ তৈরি করতে রেঞ্জের সংখ্যাগুলো বাড়ানোর চেষ্টা করুন।
join
হ্যান্ডেল ব্যবহার করে সমস্ত থ্রেডের শেষ হওয়ার জন্য অপেক্ষা করা
তালিকা ১৬-১ এর কোডটি কেবল প্রধান থ্রেড শেষ হওয়ার কারণে বেশিরভাগ সময় স্পন করা থ্রেডটিকে অকালে থামিয়ে দেয় না, বরং থ্রেডগুলো কোন ক্রমে চলবে তার কোনো গ্যারান্টি না থাকায়, আমরা এটাও গ্যারান্টি দিতে পারি না যে স্পন করা থ্রেডটি überhaupt চলার সুযোগ পাবে!
আমরা thread::spawn
এর রিটার্ন ভ্যালু একটি ভ্যারিয়েবলে সংরক্ষণ করে স্পন করা থ্রেডটি না চলার বা অকালে শেষ হয়ে যাওয়ার সমস্যাটি সমাধান করতে পারি। thread::spawn
এর রিটার্ন টাইপ হলো JoinHandle<T>
। একটি JoinHandle<T>
হলো একটি ওনড ভ্যালু যা, যখন আমরা এর উপর join
মেথড কল করি, তখন তার থ্রেড শেষ হওয়ার জন্য অপেক্ষা করবে। তালিকা ১৬-২ দেখায় কীভাবে আমরা তালিকা ১৬-১ এ তৈরি করা থ্রেডের JoinHandle<T>
ব্যবহার করতে পারি এবং main
এক্সিট করার আগে স্পন করা থ্রেডটি শেষ হয়েছে তা নিশ্চিত করতে join
কল করতে পারি।
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(); }
হ্যান্ডেলের উপর join
কল করা বর্তমানে চলমান থ্রেডটিকে ব্লক করে যতক্ষণ না হ্যান্ডেল দ্বারা প্রতিনিধিত্ব করা থ্রেডটি শেষ হয়। একটি থ্রেডকে ব্লক করার অর্থ হলো সেই থ্রেডটিকে কাজ করা বা এক্সিট করা থেকে বিরত রাখা। যেহেতু আমরা join
এর কলটি প্রধান থ্রেডের for
লুপের পরে রেখেছি, তালিকা ১৬-২ চালালে এই ধরনের আউটপুট তৈরি হওয়া উচিত:
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!
দুটি থ্রেড পর্যায়ক্রমে চলতে থাকে, কিন্তু প্রধান থ্রেডটি handle.join()
কলের কারণে অপেক্ষা করে এবং স্পন করা থ্রেডটি শেষ না হওয়া পর্যন্ত শেষ হয় না।
কিন্তু আসুন দেখি কী হয় যখন আমরা handle.join()
কে main
এর for
লুপের আগে নিয়ে যাই, এইভাবে:
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)); } }
প্রধান থ্রেডটি স্পন করা থ্রেডটি শেষ হওয়ার জন্য অপেক্ষা করবে এবং তারপর তার for
লুপ চালাবে, তাই আউটপুট আর ইন্টারলিভড হবে না, যেমনটি এখানে দেখানো হয়েছে:
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!
ছোটখাটো বিবরণ, যেমন join
কোথায় কল করা হয়েছে, তা আপনার থ্রেডগুলো একই সাথে চলে কিনা তা প্রভাবিত করতে পারে।
থ্রেডের সাথে move
ক্লোজার ব্যবহার করা
আমরা প্রায়ই thread::spawn
-এ পাস করা ক্লোজারের সাথে move
কীওয়ার্ড ব্যবহার করব কারণ ক্লোজারটি তখন পরিবেশ থেকে ব্যবহৃত ভ্যালুগুলোর ওনারশিপ নিয়ে নেবে, এইভাবে সেই ভ্যালুগুলোর ওনারশিপ এক থ্রেড থেকে অন্য থ্রেডে স্থানান্তর করবে। অধ্যায় ১৩ এর "Capturing References or Moving Ownership" বিভাগে, আমরা ক্লোজারের প্রেক্ষাপটে move
নিয়ে আলোচনা করেছি। এখন আমরা move
এবং thread::spawn
-এর মধ্যেকার মিথস্ক্রিয়ার উপর আরও বেশি মনোযোগ দেব।
তালিকা ১৬-১ এ লক্ষ্য করুন যে আমরা thread::spawn
-এ যে ক্লোজারটি পাস করি তা কোনো আর্গুমেন্ট নেয় না: আমরা প্রধান থ্রেড থেকে কোনো ডেটা স্পন করা থ্রেডের কোডে ব্যবহার করছি না। প্রধান থ্রেড থেকে ডেটা স্পন করা থ্রেডে ব্যবহার করার জন্য, স্পন করা থ্রেডের ক্লোজারটিকে প্রয়োজনীয় ভ্যালুগুলো ক্যাপচার করতে হবে। তালিকা ১৬-৩ প্রধান থ্রেডে একটি ভেক্টর তৈরি করে এবং এটি স্পন করা থ্রেডে ব্যবহার করার একটি প্রচেষ্টা দেখায়। যাইহোক, এটি এখনও কাজ করবে না, যেমনটি আপনি এক মুহূর্তের মধ্যে দেখতে পাবেন।
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
ক্লোজারটি v
ব্যবহার করে, তাই এটি v
কে ক্যাপচার করবে এবং এটিকে ক্লোজারের পরিবেশের অংশ করে তুলবে। যেহেতু thread::spawn
এই ক্লোজারটিকে একটি নতুন থ্রেডে চালায়, তাই আমাদের সেই নতুন থ্রেডের ভিতরে v
অ্যাক্সেস করতে সক্ষম হওয়া উচিত। কিন্তু যখন আমরা এই উদাহরণটি কম্পাইল করি, আমরা নিম্নলিখিত এররটি পাই:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
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
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {v:?}");
8 | | });
| |______^
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 || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust অনুমান (infers) করে কীভাবে v
কে ক্যাপচার করতে হবে, এবং যেহেতু println!
শুধুমাত্র v
-এর একটি রেফারেন্সের প্রয়োজন, ক্লোজারটি v
-কে ধার করার চেষ্টা করে। যাইহোক, একটি সমস্যা আছে: Rust বলতে পারে না যে স্পন করা থ্রেডটি কতক্ষণ চলবে, তাই এটি জানে না যে v
-এর রেফারেন্সটি সর্বদা বৈধ থাকবে কিনা।
তালিকা ১৬-৪ এমন একটি পরিস্থিতি প্রদান করে যেখানে v
-এর একটি রেফারেন্স অবৈধ হওয়ার সম্ভাবনা বেশি।
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();
}
যদি Rust আমাদের এই কোডটি চালানোর অনুমতি দিত, তবে একটি সম্ভাবনা ছিল যে স্পন করা থ্রেডটি überhaupt না চালিয়েই অবিলম্বে ব্যাকগ্রাউন্ডে রাখা হবে। স্পন করা থ্রেডের ভিতরে v
-এর একটি রেফারেন্স রয়েছে, কিন্তু প্রধান থ্রেডটি অবিলম্বে v
-কে ড্রপ করে দেয়, drop
ফাংশনটি ব্যবহার করে যা আমরা অধ্যায় ১৫-এ আলোচনা করেছি। তারপর, যখন স্পন করা থ্রেডটি এক্সিকিউট করা শুরু করে, v
আর বৈধ থাকে না, তাই এটির একটি রেফারেন্সও অবৈধ। ওহ নো!
তালিকা ১৬-৩ এর কম্পাইলার এররটি ঠিক করার জন্য, আমরা এরর মেসেজের পরামর্শ ব্যবহার করতে পারি:
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 || {
| ++++
ক্লোজারের আগে move
কীওয়ার্ড যোগ করে, আমরা ক্লোজারটিকে তার ব্যবহৃত ভ্যালুগুলোর ওনারশিপ নিতে বাধ্য করি, Rust-কে অনুমান করতে দেওয়ার পরিবর্তে যে এটি ভ্যালুগুলো ধার করবে। তালিকা ১৬-৩ এর পরিবর্তন যা তালিকা ১৬-৫ এ দেখানো হয়েছে, তা কম্পাইল হবে এবং আমাদের উদ্দেশ্য অনুযায়ী চলবে।
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(); }
আমরা হয়তো তালিকা ১৬-৪ এর কোডটি ঠিক করার জন্য একই জিনিস চেষ্টা করতে প্রলুব্ধ হতে পারি যেখানে প্রধান থ্রেড drop
কল করেছিল একটি move
ক্লোজার ব্যবহার করে। যাইহোক, এই সমাধানটি কাজ করবে না কারণ তালিকা ১৬-৪ যা করার চেষ্টা করছে তা একটি ভিন্ন কারণে নিষিদ্ধ। যদি আমরা ক্লোজারে move
যোগ করি, আমরা v
-কে ক্লোজারের পরিবেশে নিয়ে যাব, এবং আমরা আর প্রধান থ্রেডে এটির উপর drop
কল করতে পারব না। আমরা পরিবর্তে এই কম্পাইলার এররটি পাব:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust-এর ওনারশিপের নিয়ম আমাদের আবার বাঁচিয়েছে! আমরা তালিকা ১৬-৩ এর কোড থেকে একটি এরর পেয়েছিলাম কারণ Rust রক্ষণশীল ছিল এবং থ্রেডের জন্য শুধুমাত্র v
কে ধার করছিল, যার মানে প্রধান থ্রেড তাত্ত্বিকভাবে স্পন করা থ্রেডের রেফারেন্সকে অবৈধ করতে পারত। Rust-কে v
-এর ওনারশিপ স্পন করা থ্রেডে স্থানান্তর করতে বলে, আমরা Rust-কে গ্যারান্টি দিচ্ছি যে প্রধান থ্রেড আর v
ব্যবহার করবে না। যদি আমরা তালিকা ১৬-৪ কে একই ভাবে পরিবর্তন করি, তাহলে আমরা প্রধান থ্রেডে v
ব্যবহার করার চেষ্টা করার সময় ওনারশিপের নিয়ম লঙ্ঘন করছি। move
কীওয়ার্ড Rust-এর রক্ষণশীল ডিফল্ট ধার করাকে ওভাররাইড করে; এটি আমাদের ওনারশিপের নিয়ম লঙ্ঘন করতে দেয় না।
এখন যেহেতু আমরা থ্রেড কী এবং থ্রেড API দ্বারা সরবরাহ করা মেথডগুলো আলোচনা করেছি, আসুন কিছু পরিস্থিতি দেখি যেখানে আমরা থ্রেড ব্যবহার করতে পারি।