শেয়ারড-স্টেট কনকারেন্সি (Shared-State Concurrency)
মেসেজ পাসিং কনকারেন্সি পরিচালনার একটি চমৎকার উপায়, কিন্তু এটিই একমাত্র উপায় নয়। আরেকটি পদ্ধতি হল একাধিক থ্রেড একই শেয়ার করা ডেটা অ্যাক্সেস করবে। Go ল্যাংগুয়েজ ডকুমেন্টেশনের স্লোগানের এই অংশটি আবার বিবেচনা করুন: "মেমরি শেয়ার করে যোগাযোগ করবেন না।" ("do not communicate by sharing memory.")
মেমরি শেয়ার করে যোগাযোগ দেখতে কেমন হবে? এছাড়াও, মেসেজ-পাসিং উৎসাহীরা কেন মেমরি শেয়ারিং ব্যবহার না করার জন্য সতর্ক করেন?
একভাবে, যেকোনো প্রোগ্রামিং ল্যাংগুয়েজের চ্যানেলগুলি single ownership এর মতো, কারণ একবার আপনি একটি চ্যানেলের মাধ্যমে একটি value পাঠিয়ে দিলে, আপনার আর সেই value টি ব্যবহার করা উচিত নয়। শেয়ারড মেমরি কনকারেন্সি অনেকটা multiple ownership এর মতো: একাধিক থ্রেড একই সময়ে একই মেমরি লোকেশন অ্যাক্সেস করতে পারে। আপনি যেমনটি Chapter 15 এ দেখেছেন, যেখানে smart pointer গুলি multiple ownership সম্ভব করেছে, multiple ownership জটিলতা বাড়াতে পারে কারণ এই বিভিন্ন owner দের পরিচালনা করতে হয়। Rust এর type system এবং ownership নিয়ম এই পরিচালনা সঠিকভাবে করতে ব্যাপকভাবে সহায়তা করে। একটি উদাহরণের জন্য, আসুন mutex দেখি, শেয়ারড মেমরির জন্য আরও সাধারণ কনকারেন্সি প্রিমিটিভগুলির মধ্যে একটি।
ডেটাতে একবারে একটি থ্রেড থেকে অ্যাক্সেসের অনুমতি দিতে Mutex ব্যবহার করা
Mutex হল mutual exclusion এর সংক্ষিপ্ত রূপ, যেমন, একটি mutex যেকোনো সময়ে শুধুমাত্র একটি থ্রেডকে কিছু ডেটা অ্যাক্সেস করার অনুমতি দেয়। একটি mutex-এর ডেটা অ্যাক্সেস করার জন্য, একটি থ্রেডকে প্রথমে mutex এর lock অ্যাকোয়ার করার জন্য অনুরোধ করে অ্যাক্সেসের ইচ্ছা জানাতে হবে। lock হল একটি ডেটা স্ট্রাকচার যা mutex-এর অংশ, যা বর্তমানে ডেটাতে কার exclusive access আছে তার ট্র্যাক রাখে। অতএব, mutex কে বর্ণনা করা হয় লকিং সিস্টেমের মাধ্যমে ডেটা guard (রক্ষা) করছে।
Mutex ব্যবহারের ক্ষেত্রে দুটি নিয়ম মনে রাখতে হয়, তাই এগুলি ব্যবহার করা কঠিন:
- ডেটা ব্যবহার করার আগে আপনাকে lock অ্যাকোয়ার করার চেষ্টা করতে হবে।
- যখন আপনার mutex দ্বারা সুরক্ষিত ডেটার কাজ শেষ হয়ে যাবে, তখন আপনাকে অবশ্যই ডেটা unlock করতে হবে যাতে অন্য থ্রেডগুলি lock অ্যাকোয়ার করতে পারে।
বাস্তব দুনিয়ায় mutex-এর একটি উদাহরণ হিসেবে, একটি কনফারেন্সে একটি প্যানেল আলোচনা কল্পনা করুন যেখানে কেবল একটি মাইক্রোফোন রয়েছে। কোনও প্যানেলিস্ট কথা বলার আগে, তাদের জিজ্ঞাসা করতে হবে বা সংকেত দিতে হবে যে তারা মাইক্রোফোনটি ব্যবহার করতে চায়। যখন তারা মাইক্রোফোনটি পায়, তারা যতক্ষণ চায় ততক্ষণ কথা বলতে পারে এবং তারপরে পরবর্তী প্যানেলিস্ট যিনি কথা বলতে অনুরোধ করেছেন তাকে মাইক্রোফোনটি দিতে পারে। যদি কোনও প্যানেলিস্ট তার কাজ শেষ হয়ে গেলে মাইক্রোফোনটি দিতে ভুলে যায়, তবে অন্য কেউ কথা বলতে পারবে না। যদি শেয়ার্ড মাইক্রোফোনের পরিচালনা ভুল হয়ে যায়, তাহলে প্যানেলটি পরিকল্পনা অনুযায়ী কাজ করবে না!
Mutex-এর পরিচালনা সঠিকভাবে করা অবিশ্বাস্যভাবে কঠিন হতে পারে, যে কারণে অনেকেই চ্যানেলের প্রতি উৎসাহী। যাইহোক, Rust-এর type system এবং ownership নিয়মের কারণে, আপনি locking এবং unlocking ভুল করতে পারবেন না।
Mutex<T>
এর API
কিভাবে একটি mutex ব্যবহার করতে হয় তার উদাহরণ হিসাবে, আসুন Listing 16-12 এ দেখানো single-threaded প্রসঙ্গে একটি mutex ব্যবহার করে শুরু করি:
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {m:?}"); }
অনেক type এর মতোই, আমরা new
অ্যাসোসিয়েটেড ফাংশন ব্যবহার করে একটি Mutex<T>
তৈরি করি। mutex এর ভেতরের ডেটা অ্যাক্সেস করতে, আমরা lock অ্যাকোয়ার করার জন্য lock
মেথড ব্যবহার করি। এই কলটি current thread কে ব্লক করবে যাতে lock পাওয়ার আগ পর্যন্ত এটি কোনও কাজ করতে না পারে।
যদি lock ধরে রাখা অন্য কোনো থ্রেড প্যানিক করে তাহলে lock
-এ কল fail করবে। সেক্ষেত্রে, কেউই কখনও lock টি পেতে সক্ষম হবে না, তাই আমরা unwrap
বেছে নিয়েছি এবং যদি আমরা সেই পরিস্থিতিতে থাকি তবে এই থ্রেডটিকে প্যানিক করাব।
আমরা lock অ্যাকোয়ার করার পরে, আমরা return value টিকে, এক্ষেত্রে num
নামের, ভেতরের ডেটার একটি mutable reference হিসাবে ব্যবহার করতে পারি। type system নিশ্চিত করে যে আমরা m
-এর value ব্যবহার করার আগে একটি lock অ্যাকোয়ার করি। m
-এর type হল Mutex<i32>
, i32
নয়, তাই i32
value ব্যবহার করতে সক্ষম হওয়ার জন্য আমাদের অবশ্যই lock
কল করতে হবে। আমরা ভুলতে পারি না; type system অন্যথায় আমাদের ভেতরের i32
অ্যাক্সেস করতে দেবে না।
আপনি যেমন সন্দেহ করতে পারেন, Mutex<T>
একটি smart pointer। আরও সঠিকভাবে বলতে গেলে, lock
-এর কলটি MutexGuard
নামক একটি smart pointer রিটার্ন করে, একটি LockResult
-এর মধ্যে wrap করা যা আমরা unwrap
-এর কলের মাধ্যমে হ্যান্ডেল করেছি। MutexGuard
smart pointer টি আমাদের ভেতরের ডেটার দিকে পয়েন্ট করার জন্য Deref
ইমপ্লিমেন্ট করে; smart pointer-টিতে একটি Drop
ইমপ্লিমেন্টেশনও রয়েছে যা MutexGuard
scope-এর বাইরে চলে গেলে স্বয়ংক্রিয়ভাবে lock ছেড়ে দেয়, যা inner scope-এর শেষে ঘটে। ফলস্বরূপ, আমরা lock ছেড়ে দিতে ভুলে যাওয়ার এবং mutex-কে অন্য থ্রেড দ্বারা ব্যবহৃত হওয়া থেকে ব্লক করার ঝুঁকি নেই, কারণ lock রিলিজ স্বয়ংক্রিয়ভাবে ঘটে।
lock ড্রপ করার পরে, আমরা mutex value প্রিন্ট করতে পারি এবং দেখতে পারি যে আমরা ভেতরের i32
কে 6-এ পরিবর্তন করতে সক্ষম হয়েছি।
একাধিক থ্রেডের মধ্যে একটি Mutex<T>
শেয়ার করা
এখন, আসুন Mutex<T>
ব্যবহার করে একাধিক থ্রেডের মধ্যে একটি value শেয়ার করার চেষ্টা করি। আমরা 10 টি থ্রেড চালু করব এবং তাদের প্রত্যেককে একটি counter value 1 করে বৃদ্ধি করতে বলব, যাতে counter-টি 0 থেকে 10 পর্যন্ত যায়। Listing 16-13-এর পরবর্তী উদাহরণে একটি compiler error থাকবে এবং আমরা সেই error-টি Mutex<T>
ব্যবহার সম্পর্কে আরও জানতে এবং কীভাবে Rust আমাদের এটি সঠিকভাবে ব্যবহার করতে সহায়তা করে তা শিখতে ব্যবহার করব।
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());
}
আমরা Listing 16-12 এর মতো একটি Mutex<T>
এর ভিতরে একটি i32
রাখার জন্য একটি counter
variable তৈরি করি। এরপর, আমরা সংখ্যার একটি range-এর উপর iterate করে 10 টি থ্রেড তৈরি করি। আমরা thread::spawn
ব্যবহার করি এবং সমস্ত থ্রেডকে একই ক্লোজার দিই: একটি যা counter-টিকে থ্রেডের মধ্যে move করে, lock
মেথড কল করে Mutex<T>
-তে একটি lock অ্যাকোয়ার করে এবং তারপর mutex-এর value-তে 1 যোগ করে। যখন একটি থ্রেড তার ক্লোজার চালানো শেষ করে, num
scope-এর বাইরে চলে যাবে এবং lock ছেড়ে দেবে যাতে অন্য থ্রেড এটি অ্যাকোয়ার করতে পারে।
main থ্রেডে, আমরা সমস্ত join handle সংগ্রহ করি। তারপর, Listing 16-2-তে যেমন করেছি, আমরা প্রতিটি হ্যান্ডেলে join
কল করি যাতে সমস্ত থ্রেড শেষ হয়। সেই সময়ে, main থ্রেড lock অ্যাকোয়ার করবে এবং এই program-এর result প্রিন্ট করবে।
আমরা ইঙ্গিত দিয়েছিলাম যে এই উদাহরণটি compile হবে না। এখন দেখা যাক কেন!
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`
--> src/main.rs:21:29
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
8 | for _ in 0..10 {
| -------------- inside of this loop
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value borrowed here after move
|
help: consider moving the expression out of the loop so it is only moved once
|
8 ~ let mut value = counter.lock();
9 ~ for _ in 0..10 {
10 | let handle = thread::spawn(move || {
11 ~ let mut num = value.unwrap();
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
Error মেসেজ বলছে যে counter
value-টি লুপের পূর্ববর্তী ইটারেশনে move করা হয়েছিল। Rust আমাদের বলছে যে আমরা counter
-এর ownership একাধিক থ্রেডে move করতে পারি না। আসুন Chapter 15-এ আলোচনা করা একটি multiple-ownership পদ্ধতি দিয়ে compiler error ঠিক করি।
একাধিক থ্রেড সহ Multiple Ownership
Chapter 15-এ, আমরা একটি reference counted value তৈরি করতে smart pointer Rc<T>
ব্যবহার করে একটি value-কে একাধিক owner দিয়েছিলাম। আসুন এখানে একই কাজ করি এবং দেখি কী হয়। আমরা Listing 16-14-তে Mutex<T>
-কে Rc<T>
-তে wrap করব এবং ownership থ্রেডে move করার আগে Rc<T>
ক্লোন করব।
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 করি এবং… ভিন্ন error পাই! compiler আমাদের অনেক কিছু শেখাচ্ছে।
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
| | |
| | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/std/src/thread/mod.rs:731:8
|
728 | pub fn spawn<F, T>(f: F) -> JoinHandle<T>
| ----- required by a bound in this function
...
731 | F: Send + 'static,
| ^^^^ required by this bound in `spawn`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
বাহ, সেই error মেসেজটি খুব শব্দবহুল! ফোকাস করার জন্য এখানে গুরুত্বপূর্ণ অংশটি হল: `Rc<Mutex<i32>>` cannot be sent between threads safely
। compiler আমাদের কারণটিও বলছে: the trait `Send` is not implemented for `Rc<Mutex<i32>>`
। আমরা পরবর্তী বিভাগে Send
সম্পর্কে কথা বলব: এটি এমন একটি trait যা নিশ্চিত করে যে আমরা থ্রেডের সাথে যে type গুলি ব্যবহার করি সেগুলি concurrent পরিস্থিতিতে ব্যবহারের জন্য উপযুক্ত।
দুর্ভাগ্যবশত, Rc<T>
থ্রেড জুড়ে শেয়ার করা নিরাপদ নয়। যখন Rc<T>
reference count পরিচালনা করে, তখন এটি প্রতিটি clone
কলের জন্য count-এ যোগ করে এবং প্রতিটি ক্লোন ড্রপ হওয়ার সময় count থেকে বিয়োগ করে। কিন্তু এটি count-এর পরিবর্তনগুলি অন্য থ্রেড দ্বারা বাধাগ্রস্ত হতে পারে না তা নিশ্চিত করার জন্য কোনও concurrency প্রিমিটিভ ব্যবহার করে না। এটি ভুল count-এর দিকে পরিচালিত করতে পারে—সূক্ষ্ম বাগ যা memory leak বা আমাদের কাজ শেষ হওয়ার আগেই একটি value ড্রপ হওয়ার কারণ হতে পারে। আমাদের যা দরকার তা হল একটি type যা Rc<T>
-এর মতোই কিন্তু যা reference count-এ পরিবর্তনগুলি thread-safe উপায়ে করে।
Arc<T>
এর সাথে অ্যাটমিক রেফারেন্স কাউন্টিং
সৌভাগ্যবশত, Arc<T>
হল Rc<T>
এর মতো একটি type যা concurrent পরিস্থিতিতে ব্যবহার করা নিরাপদ। a মানে atomic, অর্থাৎ এটি একটি atomically reference-counted type। অ্যাটমিক হল এক ধরনের concurrency প্রিমিটিভ যা আমরা এখানে বিস্তারিতভাবে আলোচনা করব না: আরও বিস্তারিত জানার জন্য std::sync::atomic
এর স্ট্যান্ডার্ড লাইব্রেরি ডকুমেন্টেশন দেখুন। এই সময়ে, আপনাকে শুধু জানতে হবে যে অ্যাটমিকগুলি primitive type-এর মতো কাজ করে তবে থ্রেড জুড়ে শেয়ার করা নিরাপদ।
আপনি তখন ভাবতে পারেন কেন সমস্ত primitive type অ্যাটমিক নয় এবং কেন স্ট্যান্ডার্ড লাইব্রেরি type গুলি ডিফল্টভাবে Arc<T>
ব্যবহার করার জন্য ইমপ্লিমেন্ট করা হয় না। কারণ হল থ্রেড নিরাপত্তার সাথে একটি পারফরম্যান্স পেনাল্টি আসে যা আপনি তখনই দিতে চান যখন আপনার সত্যিই প্রয়োজন হয়। আপনি যদি শুধুমাত্র একটি single thread-এর মধ্যে value-গুলির উপর অপারেশন সম্পাদন করেন, তাহলে আপনার কোড আরও দ্রুত চলতে পারে যদি এটিকে অ্যাটমিকদের দেওয়া গ্যারান্টিগুলি প্রয়োগ করতে না হয়।
আসুন আমাদের উদাহরণে ফিরে আসি: Arc<T>
এবং Rc<T>
-এর একই API রয়েছে, তাই আমরা use
লাইন, new
-এর কল এবং clone
-এর কল পরিবর্তন করে আমাদের program ঠিক করি। Listing 16-15-এর কোডটি অবশেষে compile এবং রান করবে:
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
আমরা পেরেছি! আমরা 0 থেকে 10 পর্যন্ত গণনা করেছি, যা খুব চিত্তাকর্ষক নাও মনে হতে পারে, তবে এটি আমাদের Mutex<T>
এবং থ্রেড নিরাপত্তা সম্পর্কে অনেক কিছু শিখিয়েছে। আপনি একটি counter বৃদ্ধি করার চেয়ে আরও জটিল অপারেশন করার জন্য এই program-এর গঠন ব্যবহার করতে পারেন। এই কৌশলটি ব্যবহার করে, আপনি একটি calculation-কে স্বাধীন অংশে বিভক্ত করতে পারেন, সেই অংশগুলিকে থ্রেড জুড়ে বিভক্ত করতে পারেন এবং তারপর প্রতিটি থ্রেডকে তার অংশ দিয়ে final result আপডেট করার জন্য একটি Mutex<T>
ব্যবহার করতে পারেন।
মনে রাখবেন যে আপনি যদি সাধারণ numerical অপারেশন করেন, তাহলে স্ট্যান্ডার্ড লাইব্রেরির std::sync::atomic
মডিউল দ্বারা প্রদত্ত Mutex<T>
type-এর চেয়ে সহজ type রয়েছে। এই type গুলি primitive type গুলিতে নিরাপদ, concurrent, অ্যাটমিক অ্যাক্সেস সরবরাহ করে। আমরা এই উদাহরণের জন্য একটি primitive type-এর সাথে Mutex<T>
ব্যবহার করতে বেছে নিয়েছি যাতে আমরা Mutex<T>
কীভাবে কাজ করে তাতে মনোযোগ দিতে পারি।
RefCell<T>
/Rc<T>
এবং Mutex<T>
/Arc<T>
এর মধ্যে মিল
আপনি হয়তো লক্ষ্য করেছেন যে counter
হল immutable, কিন্তু আমরা এর ভেতরের value-তে একটি mutable reference পেতে পারি; এর মানে হল Mutex<T>
ইন্টেরিয়র mutability প্রদান করে, যেমন Cell
পরিবার করে। একইভাবে আমরা Chapter 15-এ Rc<T>
-এর ভেতরের contents পরিবর্তন করার অনুমতি দেওয়ার জন্য RefCell<T>
ব্যবহার করেছি, আমরা Arc<T>
-এর ভেতরের contents পরিবর্তন করতে Mutex<T>
ব্যবহার করি।
আরেকটি বিষয় লক্ষণীয় যে Rust আপনাকে Mutex<T>
ব্যবহার করার সময় সব ধরনের লজিক error থেকে রক্ষা করতে পারে না। Chapter 15 থেকে মনে রাখবেন যে Rc<T>
ব্যবহার করার সময় রেফারেন্স সাইকেল তৈরি হওয়ার ঝুঁকি ছিল, যেখানে দুটি Rc<T>
value একে অপরকে রেফার করে, যার ফলে memory leak হয়। একইভাবে, Mutex<T>
-এর deadlock তৈরি হওয়ার ঝুঁকি রয়েছে। এগুলি ঘটে যখন একটি অপারেশনের দুটি রিসোর্স lock করা প্রয়োজন হয় এবং দুটি থ্রেড প্রতিটি একটি করে lock অ্যাকোয়ার করে, যার ফলে তারা একে অপরের জন্য চিরকাল অপেক্ষা করে। আপনি যদি ডেডলকগুলিতে আগ্রহী হন তবে একটি Rust program তৈরি করার চেষ্টা করুন যাতে একটি ডেডলক রয়েছে; তারপর যেকোনো ল্যাংগুয়েজের জন্য mutex-এর ডেডলক প্রশমন কৌশলগুলি নিয়ে রিসার্চ করুন এবং Rust-এ সেগুলি ইমপ্লিমেন্ট করার চেষ্টা করুন। Mutex<T>
এবং MutexGuard
-এর জন্য স্ট্যান্ডার্ড লাইব্রেরি API ডকুমেন্টেশন দরকারী তথ্য সরবরাহ করে।
আমরা এই চ্যাপ্টারটি Send
এবং Sync
trait এবং কীভাবে আমরা সেগুলি custom type-এর সাথে ব্যবহার করতে পারি সে সম্পর্কে কথা বলে শেষ করব।