C++ তে কনকারেন্সি (Concurrency) হল একাধিক কাজ বা থ্রেড একসাথে কার্যকরভাবে চলা, যা একটি প্রোগ্রামের কর্মক্ষমতা এবং প্রতিক্রিয়া সময় উন্নত করতে সাহায্য করে। এটি একাধিক থ্রেড বা প্রসেসের মাধ্যমে একাধিক কাজ চালানোর ক্ষমতা প্রদান করে, যার ফলে CPU বা অন্যান্য সিস্টেম রিসোর্সগুলো আরও কার্যকরভাবে ব্যবহার করা যায়।
C++11 থেকে C++ তে কনকারেন্সি সমর্থন শুরু হয়েছে এবং বর্তমানে C++17 ও C++20 তে কনকারেন্সির জন্য অনেক উন্নত ফিচার যোগ করা হয়েছে। এই কনকারেন্সি সাপোর্টের মাধ্যমে C++ প্রোগ্রামাররা মাল্টি-থ্রেডিং এবং প্যারালাল প্রোগ্রামিংয়ের সুবিধা নিতে পারেন।
C++ তে কনকারেন্সি এর মূল উপাদানগুলি
std::threadstd::mutexএবংstd::lock_guardstd::atomicstd::futureএবংstd::asyncstd::condition_variablestd::asyncএবংstd::futurestd::barrier(C++20)
১. std::thread
C++11 এ std::thread ক্লাসটি পরিচিতি পায় যা মাল্টি-থ্রেডিং বাস্তবায়ন করতে ব্যবহৃত হয়। এটি থ্রেড তৈরি এবং পরিচালনা করার জন্য ব্যবহৃত হয়। std::thread ক্লাস ব্যবহার করে একাধিক থ্রেড চালানো যায়।
উদাহরণ (Basic Thread Creation):
#include <iostream>
#include <thread>
// থ্রেড ফাংশন
void print_hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// থ্রেড তৈরি এবং শুরু করা
std::thread t(print_hello);
// মেইন থ্রেডে কাজ চলাকালীন থ্রেডের কাজ শেষ হওয়ার জন্য অপেক্ষা
t.join(); // join() ফাংশন থ্রেডের কাজ শেষ না হওয়া পর্যন্ত অপেক্ষা করবে
return 0;
}ব্যাখ্যা:
std::threadব্যবহার করে নতুন থ্রেড তৈরি করা হয় যাprint_hello()ফাংশন চালায়।join(): এটি থ্রেডের কাজ সম্পন্ন না হওয়া পর্যন্ত মেইন থ্রেডকে অপেক্ষা করায়।
২. std::mutex এবং std::lock_guard
কনকারেন্সিতে রেস কন্ডিশন (race conditions) থেকে রক্ষা পেতে std::mutex এবং std::lock_guard ব্যবহৃত হয়। যখন একাধিক থ্রেড একসাথে একটি শেয়ার্ড রিসোর্সে অ্যাক্সেস করে, তখন mutex ব্যবহার করে সেই রিসোর্সে একসাথে একাধিক থ্রেডের অ্যাক্সেস আটকানো যায়।
উদাহরণ (Mutex and Lock):
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // মিউটেক্স অবজেক্ট
void print_numbers(int id) {
std::lock_guard<std::mutex> lock(mtx); // lock_guard ব্যবহার করে মিউটেক্স লক করা
std::cout << "Thread " << id << " is printing numbers." << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << "Thread " << id << ": " << i << std::endl;
}
}
int main() {
std::thread t1(print_numbers, 1);
std::thread t2(print_numbers, 2);
t1.join();
t2.join();
return 0;
}ব্যাখ্যা:
std::mutex mtx;: এটি একটি মিউটেক্স অবজেক্ট যা থ্রেডের মধ্যে রিসোর্স অ্যাক্সেস সিঙ্ক্রোনাইজ করতে ব্যবহৃত হয়।std::lock_guard<std::mutex> lock(mtx);: এটি একটি স্বয়ংক্রিয়ভাবে মিউটেক্স লক করার উপায়, যা রিসোর্সে একাধিক থ্রেডের অ্যাক্সেস নিয়ন্ত্রণ করে।
৩. std::atomic
C++ তে std::atomic ক্লাস ব্যবহৃত হয় এমন ভেরিয়েবলগুলির জন্য যেগুলো একাধিক থ্রেড দ্বারা অ্যাক্সেস এবং পরিবর্তিত হতে পারে। এটি একটি থ্রেড সেফ অপারেশন নিশ্চিত করে যা সাধারণ std::mutex এর মতো সিঙ্ক্রোনাইজেশন মেকানিজমের থেকে দ্রুত।
উদাহরণ (Atomic Operation):
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; ++i) {
counter++;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter.load() << std::endl;
return 0;
}ব্যাখ্যা:
std::atomic<int> counter(0);: এটি একটি অ্যাটমিক ভেরিয়েবল যা একাধিক থ্রেডের মাধ্যমে নিরাপদভাবে অ্যাক্সেস করা যায়।counter++: এই অ্যাটমিক অপারেশনটি থ্রেডের মধ্যে নিরাপদভাবে বৃদ্ধি পায়, কারণ এটি লক-মুক্ত।
৪. std::future এবং std::async
C++11 এ std::future এবং std::async ফাংশনগুলি সহায়ক ফিচার যা থ্রেড থেকে ভবিষ্যত মান (future value) উদ্ধার করতে ব্যবহৃত হয়। std::async একটি অ্যাসিঙ্ক্রোনাস কাজ চালায় এবং std::future এটি থেকে রিটার্ন মান পাওয়ার জন্য ব্যবহৃত হয়।
উদাহরণ (Async and Future):
#include <iostream>
#include <thread>
#include <future>
int calculate_square(int x) {
return x * x;
}
int main() {
// std::async ব্যবহার করে ফাংশন অ্যাসিঙ্ক্রোনাসভাবে চালানো
std::future<int> result = std::async(std::launch::async, calculate_square, 5);
// থ্রেডের ফলাফল পাওয়া
std::cout << "Square: " << result.get() << std::endl;
return 0;
}ব্যাখ্যা:
std::async: এটি একটি ফাংশন অ্যাসিঙ্ক্রোনাসভাবে চালানোর জন্য ব্যবহৃত হয়, যার ফলে এটি একটিstd::futureঅবজেক্ট রিটার্ন করে।result.get(): এটিfutureঅবজেক্ট থেকে আসন্ন ফলাফল পেতে ব্যবহৃত হয়।
৫. std::condition_variable
std::condition_variable একটি খুব গুরুত্বপূর্ণ কনসেপ্ট যা থ্রেড সিঙ্ক্রোনাইজেশন ব্যবস্থায় ব্যবহৃত হয়। এটি এক বা একাধিক থ্রেডকে একটি নির্দিষ্ট শর্ত পূর্ণ না হওয়া পর্যন্ত অপেক্ষা করতে দেয় এবং একবার শর্ত পূর্ণ হলে তাদের পুনরায় চালু করে।
উদাহরণ (Condition Variable):
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
std::cout << "Thread " << id << std::endl;
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all(); // সব থ্রেডকে জানানো যে তারা কাজ শুরু করতে পারে
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go();
for (auto& th : threads) th.join();
return 0;
}ব্যাখ্যা:
cv.wait(lck): এটি থ্রেডটিকে একটি নির্দিষ্ট শর্ত (যেমনready) পূর্ণ না হওয়া পর্যন্ত অপেক্ষা করায়।cv.notify_all(): এটি সব থ্রেডকে সতর্ক করে যে তারা তাদের কাজ শুরু করতে পারে।
উপসংহার
C++ তে কনকারেন্সি একটি শক্তিশালী কনসেপ্ট যা মাল্টি-থ্রেডিং এবং প্যারালাল প্রোগ্রামিংয়ের মাধ্যমে প্রোগ্রামকে আরও কার্যকরী এবং দ্রুত করতে সাহায্য করে। std::thread, **`std::
mutex**, **std::atomic**, **std::future**, **std::async**, এবং **std::condition_variable`** এর মতো কনকারেন্সি টুলস সি++ এর মধ্যে কার্যকরী সিঙ্ক্রোনাইজেশন এবং থ্রেড পরিচালনা সহজ করে। C++11 থেকে শুরু করে, C++ প্রোগ্রামিংয়ের কনকারেন্সি ফিচারগুলি অনেক উন্নত হয়েছে এবং বর্তমানে একটি অত্যন্ত কার্যকরী মেকানিজম সরবরাহ করছে।
C++ তে কনকারেন্সি (Concurrency) হল একাধিক কাজ বা থ্রেড একসাথে কার্যকরভাবে চলা, যা একটি প্রোগ্রামের কর্মক্ষমতা এবং প্রতিক্রিয়া সময় উন্নত করতে সাহায্য করে। এটি একাধিক থ্রেড বা প্রসেসের মাধ্যমে একাধিক কাজ চালানোর ক্ষমতা প্রদান করে, যার ফলে CPU বা অন্যান্য সিস্টেম রিসোর্সগুলো আরও কার্যকরভাবে ব্যবহার করা যায়।
C++11 থেকে C++ তে কনকারেন্সি সমর্থন শুরু হয়েছে এবং বর্তমানে C++17 ও C++20 তে কনকারেন্সির জন্য অনেক উন্নত ফিচার যোগ করা হয়েছে। এই কনকারেন্সি সাপোর্টের মাধ্যমে C++ প্রোগ্রামাররা মাল্টি-থ্রেডিং এবং প্যারালাল প্রোগ্রামিংয়ের সুবিধা নিতে পারেন।
C++ তে কনকারেন্সি এর মূল উপাদানগুলি
std::threadstd::mutexএবংstd::lock_guardstd::atomicstd::futureএবংstd::asyncstd::condition_variablestd::asyncএবংstd::futurestd::barrier(C++20)
১. std::thread
C++11 এ std::thread ক্লাসটি পরিচিতি পায় যা মাল্টি-থ্রেডিং বাস্তবায়ন করতে ব্যবহৃত হয়। এটি থ্রেড তৈরি এবং পরিচালনা করার জন্য ব্যবহৃত হয়। std::thread ক্লাস ব্যবহার করে একাধিক থ্রেড চালানো যায়।
উদাহরণ (Basic Thread Creation):
#include <iostream>
#include <thread>
// থ্রেড ফাংশন
void print_hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// থ্রেড তৈরি এবং শুরু করা
std::thread t(print_hello);
// মেইন থ্রেডে কাজ চলাকালীন থ্রেডের কাজ শেষ হওয়ার জন্য অপেক্ষা
t.join(); // join() ফাংশন থ্রেডের কাজ শেষ না হওয়া পর্যন্ত অপেক্ষা করবে
return 0;
}ব্যাখ্যা:
std::threadব্যবহার করে নতুন থ্রেড তৈরি করা হয় যাprint_hello()ফাংশন চালায়।join(): এটি থ্রেডের কাজ সম্পন্ন না হওয়া পর্যন্ত মেইন থ্রেডকে অপেক্ষা করায়।
২. std::mutex এবং std::lock_guard
কনকারেন্সিতে রেস কন্ডিশন (race conditions) থেকে রক্ষা পেতে std::mutex এবং std::lock_guard ব্যবহৃত হয়। যখন একাধিক থ্রেড একসাথে একটি শেয়ার্ড রিসোর্সে অ্যাক্সেস করে, তখন mutex ব্যবহার করে সেই রিসোর্সে একসাথে একাধিক থ্রেডের অ্যাক্সেস আটকানো যায়।
উদাহরণ (Mutex and Lock):
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // মিউটেক্স অবজেক্ট
void print_numbers(int id) {
std::lock_guard<std::mutex> lock(mtx); // lock_guard ব্যবহার করে মিউটেক্স লক করা
std::cout << "Thread " << id << " is printing numbers." << std::endl;
for (int i = 0; i < 5; i++) {
std::cout << "Thread " << id << ": " << i << std::endl;
}
}
int main() {
std::thread t1(print_numbers, 1);
std::thread t2(print_numbers, 2);
t1.join();
t2.join();
return 0;
}ব্যাখ্যা:
std::mutex mtx;: এটি একটি মিউটেক্স অবজেক্ট যা থ্রেডের মধ্যে রিসোর্স অ্যাক্সেস সিঙ্ক্রোনাইজ করতে ব্যবহৃত হয়।std::lock_guard<std::mutex> lock(mtx);: এটি একটি স্বয়ংক্রিয়ভাবে মিউটেক্স লক করার উপায়, যা রিসোর্সে একাধিক থ্রেডের অ্যাক্সেস নিয়ন্ত্রণ করে।
৩. std::atomic
C++ তে std::atomic ক্লাস ব্যবহৃত হয় এমন ভেরিয়েবলগুলির জন্য যেগুলো একাধিক থ্রেড দ্বারা অ্যাক্সেস এবং পরিবর্তিত হতে পারে। এটি একটি থ্রেড সেফ অপারেশন নিশ্চিত করে যা সাধারণ std::mutex এর মতো সিঙ্ক্রোনাইজেশন মেকানিজমের থেকে দ্রুত।
উদাহরণ (Atomic Operation):
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; ++i) {
counter++;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter.load() << std::endl;
return 0;
}ব্যাখ্যা:
std::atomic<int> counter(0);: এটি একটি অ্যাটমিক ভেরিয়েবল যা একাধিক থ্রেডের মাধ্যমে নিরাপদভাবে অ্যাক্সেস করা যায়।counter++: এই অ্যাটমিক অপারেশনটি থ্রেডের মধ্যে নিরাপদভাবে বৃদ্ধি পায়, কারণ এটি লক-মুক্ত।
৪. std::future এবং std::async
C++11 এ std::future এবং std::async ফাংশনগুলি সহায়ক ফিচার যা থ্রেড থেকে ভবিষ্যত মান (future value) উদ্ধার করতে ব্যবহৃত হয়। std::async একটি অ্যাসিঙ্ক্রোনাস কাজ চালায় এবং std::future এটি থেকে রিটার্ন মান পাওয়ার জন্য ব্যবহৃত হয়।
উদাহরণ (Async and Future):
#include <iostream>
#include <thread>
#include <future>
int calculate_square(int x) {
return x * x;
}
int main() {
// std::async ব্যবহার করে ফাংশন অ্যাসিঙ্ক্রোনাসভাবে চালানো
std::future<int> result = std::async(std::launch::async, calculate_square, 5);
// থ্রেডের ফলাফল পাওয়া
std::cout << "Square: " << result.get() << std::endl;
return 0;
}ব্যাখ্যা:
std::async: এটি একটি ফাংশন অ্যাসিঙ্ক্রোনাসভাবে চালানোর জন্য ব্যবহৃত হয়, যার ফলে এটি একটিstd::futureঅবজেক্ট রিটার্ন করে।result.get(): এটিfutureঅবজেক্ট থেকে আসন্ন ফলাফল পেতে ব্যবহৃত হয়।
৫. std::condition_variable
std::condition_variable একটি খুব গুরুত্বপূর্ণ কনসেপ্ট যা থ্রেড সিঙ্ক্রোনাইজেশন ব্যবস্থায় ব্যবহৃত হয়। এটি এক বা একাধিক থ্রেডকে একটি নির্দিষ্ট শর্ত পূর্ণ না হওয়া পর্যন্ত অপেক্ষা করতে দেয় এবং একবার শর্ত পূর্ণ হলে তাদের পুনরায় চালু করে।
উদাহরণ (Condition Variable):
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
std::cout << "Thread " << id << std::endl;
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all(); // সব থ্রেডকে জানানো যে তারা কাজ শুরু করতে পারে
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go();
for (auto& th : threads) th.join();
return 0;
}ব্যাখ্যা:
cv.wait(lck): এটি থ্রেডটিকে একটি নির্দিষ্ট শর্ত (যেমনready) পূর্ণ না হওয়া পর্যন্ত অপেক্ষা করায়।cv.notify_all(): এটি সব থ্রেডকে সতর্ক করে যে তারা তাদের কাজ শুরু করতে পারে।
উপসংহার
C++ তে কনকারেন্সি একটি শক্তিশালী কনসেপ্ট যা মাল্টি-থ্রেডিং এবং প্যারালাল প্রোগ্রামিংয়ের মাধ্যমে প্রোগ্রামকে আরও কার্যকরী এবং দ্রুত করতে সাহায্য করে। std::thread, **`std::
mutex**, **std::atomic**, **std::future**, **std::async**, এবং **std::condition_variable`** এর মতো কনকারেন্সি টুলস সি++ এর মধ্যে কার্যকরী সিঙ্ক্রোনাইজেশন এবং থ্রেড পরিচালনা সহজ করে। C++11 থেকে শুরু করে, C++ প্রোগ্রামিংয়ের কনকারেন্সি ফিচারগুলি অনেক উন্নত হয়েছে এবং বর্তমানে একটি অত্যন্ত কার্যকরী মেকানিজম সরবরাহ করছে।
C++ এর mutex এবং locking mechanisms মাল্টি-থ্রেডিং প্রোগ্রামে থ্রেড সেফটি নিশ্চিত করতে গুরুত্বপূর্ণ ভূমিকা পালন করে। যখন একাধিক থ্রেড একসাথে শেয়ারড ডেটার সাথে কাজ করে, তখন mutex (mutual exclusion) ব্যবহৃত হয় যাতে একই সময়ে একাধিক থ্রেড একই ডেটা অ্যাক্সেস করতে না পারে এবং race conditions (যেখানে একাধিক থ্রেড একই ডেটা পরিবর্তন করার চেষ্টা করে) এড়ানো যায়।
C++11-এ std::mutex, std::lock_guard, এবং std::unique_lock সহ থ্রেড সিঙ্ক্রোনাইজেশন এবং ডেটা সুরক্ষার জন্য কার্যকরী মেকানিজম প্রদান করা হয়েছে। এগুলো একে অপরের সাথে কাজ করে এবং থ্রেড সেফ অপারেশন নিশ্চিত করতে সাহায্য করে।
১. std::mutex
std::mutex হল একটি mutex (mutual exclusion) ক্লাস যা থ্রেড সিঙ্ক্রোনাইজেশনের জন্য ব্যবহৃত হয়। এটি একটি রিসোর্স বা ডেটা স্ট্রাকচারকে এক সময়ে একটি থ্রেডের কাছে লক (lock) করে রাখে এবং অন্যান্য থ্রেডগুলিকে সেই রিসোর্স অ্যাক্সেস করতে দেয় না। যখন এক থ্রেড একটি mutex লক করে, তখন অন্য থ্রেডের জন্য এটি আনলক হওয়া পর্যন্ত সেই রিসোর্স অ্যাক্সেস করা সম্ভব হয় না।
উদাহরণ (std::mutex):
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_numbers(int id) {
mtx.lock(); // lock the mutex to ensure exclusive access
std::cout << "Thread " << id << " is printing numbers" << std::endl;
mtx.unlock(); // unlock the mutex after work is done
}
int main() {
std::thread t1(print_numbers, 1);
std::thread t2(print_numbers, 2);
t1.join();
t2.join();
return 0;
}আউটপুট:
Thread 1 is printing numbers
Thread 2 is printing numbersএখানে, std::mutex mtx ব্যবহার করে দুটি থ্রেডের মধ্যে সিঙ্ক্রোনাইজেশন নিশ্চিত করা হয়েছে যাতে তারা একে অপরকে ওভারল্যাপ না করে একই রিসোর্স অ্যাক্সেস করতে পারে।
২. std::lock_guard
std::lock_guard একটি "RAII" (Resource Acquisition Is Initialization) ক্লাস যা স্বয়ংক্রিয়ভাবে mutex লক এবং আনলক করে। যখন আপনি একটি std::lock_guard অবজেক্ট তৈরি করেন, এটি একটি mutex লক করে এবং অবজেক্টটি ডেস্ট্রয় হলে স্বয়ংক্রিয়ভাবে mutex আনলক করে। এটি exception safety নিশ্চিত করতে সহায়ক, কারণ যদি কোনো এক্সসেপশন ঘটে, তখনও mutex আনলক হবে।
উদাহরণ (std::lock_guard):
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_numbers(int id) {
std::lock_guard<std::mutex> lock(mtx); // lock_guard will lock and unlock automatically
std::cout << "Thread " << id << " is printing numbers" << std::endl;
}
int main() {
std::thread t1(print_numbers, 1);
std::thread t2(print_numbers, 2);
t1.join();
t2.join();
return 0;
}আউটপুট:
Thread 1 is printing numbers
Thread 2 is printing numbersএখানে, std::lock_guard স্বয়ংক্রিয়ভাবে mutex লক এবং আনলক করছে। এতে কোডটি সহজ ও নিরাপদ হয়, কারণ আপনার হাতে লক আনলক করার জন্য কোনো বিশেষ ধাপ নিতে হয় না।
৩. std::unique_lock
std::unique_lock একটি আরও উন্নত ক্লাস যা mutex লক করার জন্য ব্যবহৃত হয়, তবে এটি std::lock_guard এর তুলনায় আরও নমনীয়। এটি std::mutex এর সাথে যুক্ত হলে আপনি কিছু অতিরিক্ত সুবিধা পাবেন:
- ডেলেইড লকিং: এটি কিছু সময়ের জন্য লক করতে পারে না, যা
std::lock_guardএর ক্ষেত্রে সম্ভব নয়। - ডাইনামিক লকিং এবং আনলকিং: এটি লক এবং আনলক অপারেশনকে ল্যাজিক্যালি পৃথক করতে দেয়।
- মাল্টিপল মিউটেক্স লকিং: একাধিক mutex লক করতে হলে
std::unique_lockআরও সুবিধাজনক হতে পারে।
উদাহরণ (std::unique_lock):
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_numbers(int id) {
std::unique_lock<std::mutex> lock(mtx); // lock the mutex
std::cout << "Thread " << id << " is printing numbers" << std::endl;
lock.unlock(); // manually unlock the mutex before the function ends
}
int main() {
std::thread t1(print_numbers, 1);
std::thread t2(print_numbers, 2);
t1.join();
t2.join();
return 0;
}আউটপুট:
Thread 1 is printing numbers
Thread 2 is printing numbersএখানে, std::unique_lock একটি mutex লক করেছে, কিন্তু আপনি চাইলে unlock() ফাংশনটি ব্যবহার করে এটি আনলক করতে পারেন আগেই। এটি বিশেষভাবে দরকারী যখন আপনি লক করা রিসোর্সের কিছু অংশে কাজ করতে চান এবং পরে আবার লক করতে চান।
৪. std::lock()
std::lock() ফাংশনটি একাধিক mutex একযোগভাবে লক করতে ব্যবহৃত হয়। এটি একটি ডেডলক এড়াতে সাহায্য করে, কারণ এটি একযোগে সমস্ত mutex লক করে এবং কোনও mutex আনলক করার আগে অন্য কোনও mutex লক করার চেষ্টা করে না।
উদাহরণ (std::lock):
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void func1() {
std::lock(mtx1, mtx2); // Lock both mutexes simultaneously
std::lock_guard<std::mutex> lg1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lg2(mtx2, std::adopt_lock);
std::cout << "func1 has locked both mtx1 and mtx2" << std::endl;
}
void func2() {
std::lock(mtx1, mtx2); // Lock both mutexes simultaneously
std::lock_guard<std::mutex> lg1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lg2(mtx2, std::adopt_lock);
std::cout << "func2 has locked both mtx1 and mtx2" << std::endl;
}
int main() {
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
return 0;
}আউটপুট:
func1 has locked both mtx1 and mtx2
func2 has locked both mtx1 and mtx2এখানে, std::lock ফাংশনটি দুটি mutex (mtx1 এবং mtx2) একযোগভাবে লক করেছে, যা ডেডলক থেকে মুক্ত থাকতে সাহায্য করেছে।
উপসংহার
std::mutex: সাধারণ mutex যা এক থ্রেডকে লক করে অন্য থ্রেডদের লক থেকে বিরত রাখে।std::lock_guard: RAII স্টাইল mutex লকিং ক্লাস যা স্বয়ংক্রিয়ভাবে mutex লক এবং আনলক করে।std::unique_lock: আরও নমনীয় mutex লকিং ক্লাস যা manual unlocking এবং delay locking সমর্থন করে।std::lock: একাধিক mutex একযোগভাবে লক করতে ব্যবহৃত হয় এবং ডেডলক প্রতিরোধে সাহায্য করে।
এই লকিং মেকানিজমগুলোর মাধ্যমে মাল্টি-থ্রেড প্রোগ্রামে সিঙ্ক্রোনাইজেশন সহজ এবং নিরাপদ করা যায়।
Condition Variables এবং Atomic Operations হল C++-এ মাল্টি-থ্রেডিং প্রোগ্রামিংয়ের জন্য অত্যন্ত গুরুত্বপূর্ণ টুলস, যা থ্রেড সমন্বয় এবং মেমরি অ্যাক্সেস সমন্বয়কে সহজতর করে। এগুলি মূলত থ্রেড সিঙ্ক্রোনাইজেশন এবং থ্রেড নিরাপদ অপারেশন নিশ্চিত করতে ব্যবহৃত হয়।
১. Condition Variables
Condition Variable হল একটি সিঙ্ক্রোনাইজেশন টুল যা থ্রেডগুলোকে সমন্বয় করতে সাহায্য করে। এটি থ্রেডগুলোকে এটা জানতে দেয় কখন একটি নির্দিষ্ট শর্ত পূর্ণ হয়েছে, যাতে থ্রেডগুলো পরবর্তী পদক্ষেপে যেতে পারে। এটি বিশেষত তখন ব্যবহৃত হয় যখন এক থ্রেড অন্য থ্রেডের কোন অবস্থার অপেক্ষা করছে, যেমন এক থ্রেড একটি রিসোর্স ফ্রী হওয়ার জন্য অপেক্ষা করছে বা একটি নির্দিষ্ট শর্ত পূর্ণ হওয়ার জন্য অপেক্ষা করছে।
Condition Variable সাধারণত std::mutex এর সাথে ব্যবহৃত হয়, কারণ থ্রেডগুলোর মধ্যে সিঙ্ক্রোনাইজেশন নিশ্চিত করতে মিউটেক্স ব্যবহার করা হয়। এর মূল কাজ হচ্ছে থ্রেডগুলোর মধ্যে সিগন্যাল পাঠানো, যেমন একটি থ্রেড অপেক্ষা করছে এবং অন্য থ্রেড সেই শর্ত পূর্ণ হলে অপেক্ষমান থ্রেডকে অবহিত করবে।
std::condition_variable এর ফাংশন:
wait(): একটি থ্রেড তার লকটি মুক্ত করে এবং অন্য থ্রেডের সিগন্যালের জন্য অপেক্ষা করে।notify_one(): একটি থ্রেডকে সিগন্যাল পাঠায়, যার মাধ্যমে একটি থ্রেড অপেক্ষা করতে থাকা অবস্থায় চলতে শুরু করবে।notify_all(): সব থ্রেডকে সিগন্যাল পাঠায়, যারা শর্ত পূর্ণ হওয়ার জন্য অপেক্ষা করছে।
উদাহরণ:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck); // যখন ready false, থ্রেড অপেক্ষা করবে
std::cout << "Thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true; // ready কে true করতে হবে
cv.notify_all(); // সব থ্রেডকে সিগন্যাল পাঠানো
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i); // থ্রেড তৈরি করা
std::cout << "10 threads ready to race...\n";
go(); // থ্রেডগুলোকে চলতে বলা
for (auto& th : threads) th.join(); // থ্রেডগুলো শেষ হওয়া পর্যন্ত অপেক্ষা করা
return 0;
}আউটপুট:
10 threads ready to race...
Thread 0
Thread 1
Thread 2
Thread 3
Thread 4
Thread 5
Thread 6
Thread 7
Thread 8
Thread 9এখানে, cv.wait(lck) থ্রেডকে অপেক্ষা করতে বলে, যখন ready মানটি true হয়ে যায়, তখন cv.notify_all() তা থ্রেডগুলোকে অবহিত করে চলতে দেয়।
২. Atomic Operations
Atomic Operations হল এমন অপারেশন যেগুলি থ্রেড সেফ এবং একত্রে সমাপ্ত হয়। এর মানে হল যে, একাধিক থ্রেড যখন একটি ভেরিয়েবলের মান পরিবর্তন করতে চেষ্টা করে, তখন এটি শুধুমাত্র এক থ্রেডের দ্বারা সম্পন্ন হবে, অন্যথায় এটি প্রতিযোগিতামূলক অবস্থার সৃষ্টি করবে। C++11 থেকে std::atomic ক্লাসটি যুক্ত করা হয়েছে, যা এই ধরনের নিরাপদ অপারেশন করতে সাহায্য করে।
std::atomic ব্যবহার করে বিভিন্ন ডেটা টাইপ (যেমন, int, bool, pointer ইত্যাদি) অ্যাটমিকভাবে আপডেট করা যায়, এবং এটি সিঙ্ক্রোনাইজেশন সমস্যা এড়াতে সাহায্য করে।
std::atomic এর সাধারণ বৈশিষ্ট্য:
- অ্যাটমিক অপারেশন: একাধিক থ্রেডের মধ্যে ডেটা রেস কন্ডিশন (race condition) এড়ানোর জন্য এটোমিক অপারেশন ব্যবহার করা হয়।
- এটোমিক মান অ্যাক্সেস: এক থ্রেড দ্বারা অ্যাটমিক মান পরিবর্তন করা হলে, অন্য থ্রেড তা দেখতে পাবে (এটি একত্রে ঘটবে)।
উদাহরণ:
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> count(0); // এটোমিক ভেরিয়েবল
void increment() {
for (int i = 0; i < 1000; ++i) {
++count; // এটোমিক ইনক্রিমেন্ট
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Count: " << count.load() << std::endl; // এটোমিক মান লোড করা
return 0;
}আউটপুট:
Count: 2000এখানে, দুটি থ্রেড একসাথে count ভেরিয়েবলটি ইনক্রিমেন্ট করছে, তবে যেহেতু এটি std::atomic<int> টাইপ, এই অপারেশনটি সঠিকভাবে সিঙ্ক্রোনাইজড থাকে এবং কোনও রেস কন্ডিশন সৃষ্টি হয় না।
Atomic Operations এর সাধারণ ফাংশনসমূহ:
load(): এটোমিক ভেরিয়েবলের মান পড়ে।store(): এটোমিক ভেরিয়েবলে একটি নতুন মান সন্নিবেশ করে।fetch_add(): এটোমিক ভেরিয়েবলে একটি মান যোগ করে এবং পুরনো মানটি রিটার্ন করে।fetch_sub(): এটোমিক ভেরিয়েবলে একটি মান বিয়োগ করে এবং পুরনো মানটি রিটার্ন করে।compare_exchange_weak(): এটোমিক ভেরিয়েবলের মান তুলনা করে এবং যদি এটি প্রত্যাশিত মানের সমান হয় তবে নতুন মান সেট করে।
উপসংহার:
- Condition Variables: থ্রেড সমন্বয়ের জন্য ব্যবহৃত হয়, যখন একটি থ্রেড অন্য থ্রেডের শর্ত পূর্ণ হওয়ার জন্য অপেক্ষা করে।
- Atomic Operations: একাধিক থ্রেডের মধ্যে ডেটা রেস কন্ডিশন এড়াতে ব্যবহৃত হয় এবং একসাথে একাধিক থ্রেডের মধ্যে ডেটা অ্যাক্সেস সিঙ্ক্রোনাইজড রাখে।
এই দুটি ধারণা C++-এ মাল্টি-থ্রেডিং কার্যকরভাবে পরিচালনা করতে সহায়ক, এবং সিঙ্ক্রোনাইজেশন এবং সঠিক মেমরি অ্যাক্সেস নিশ্চিত করতে ব্যবহৃত হয়।
সি++ এ ফিউচারস (Futures), প্রমিসেস (Promises) এবং অ্যাসিনক্রোনাস প্রোগ্রামিং একসঙ্গে ব্যবহার করে মাল্টি-থ্রেডিং এবং অ্যাসিনক্রোনাস কার্য সম্পাদন করা যায়। C++11 এ এদের পরিচয় করানো হয়েছিল std::async, std::future, এবং std::promise এর মাধ্যমে, যা সময় নির্ধারণ ও প্রসেসিংয়ে কার্যকারিতা এবং কর্মদক্ষতা বৃদ্ধি করে। এগুলো বিশেষ করে CPU-ইনটেনসিভ টাস্কে একাধিক থ্রেডের মাধ্যমে কার্য সম্পাদন এবং বিভিন্ন সময়ে সম্পন্ন হতে পারে এমন কাজগুলো সম্পাদন করতে সহায়ক।
১. Futures (ফিউচারস)
std::future একটি ক্লাস টেমপ্লেট, যা অ্যাসিনক্রোনাস অপারেশনের মাধ্যমে একটি নির্দিষ্ট টাইপের মান ধরে রাখতে পারে। এটি অ্যাসিনক্রোনাস ফাংশন কলের আউটপুট ধরে রাখে এবং আউটপুট প্রস্তুত হওয়া পর্যন্ত অপেক্ষা করে। future মূলত টাস্ক সম্পন্ন হওয়ার পরবর্তী সময়ে মান পড়তে ব্যবহৃত হয়।
উদাহরণ:
#include <iostream>
#include <future>
int computeSquare(int x) {
return x * x;
}
int main() {
// অ্যাসিনক্রোনাস টাস্ক শুরু করা এবং ফিউচার পেতে async ব্যবহার
std::future<int> result = std::async(std::launch::async, computeSquare, 5);
// ফলাফল পাওয়ার জন্য অপেক্ষা করা এবং আউটপুট দেখানো
std::cout << "Square of 5: " << result.get() << std::endl;
return 0;
}এখানে, std::async ব্যবহার করে computeSquare ফাংশনটি অ্যাসিনক্রোনাসভাবে কল করা হয়েছে, এবং std::future এর মাধ্যমে এর ফলাফল সংগ্রহ করা হয়েছে। result.get() কলটি সেই ফলাফলটি রিটার্ন করে যা টাস্ক সম্পূর্ণ হওয়ার পরে পাওয়া যায়।
২. Promises (প্রমিসেস)
std::promise একটি অবজেক্ট যা অ্যাসিনক্রোনাস অপারেশনের ফলাফল সেট করতে ব্যবহৃত হয়। এটি মূলত std::future এর সঙ্গে কাজ করে। promise একটি মান ধরে রাখে এবং সেই মানকে future এর মাধ্যমে অ্যাসিনক্রোনাস থ্রেডে পৌঁছে দেয়।
উদাহরণ:
#include <iostream>
#include <future>
#include <thread>
void computeSquare(std::promise<int> prom, int x) {
prom.set_value(x * x); // প্রমিস সেট করা
}
int main() {
std::promise<int> prom;
std::future<int> result = prom.get_future(); // প্রমিস থেকে ফিউচার পাওয়া
std::thread t(computeSquare, std::move(prom), 6); // থ্রেড শুরু করা
std::cout << "Square of 6: " << result.get() << std::endl; // ফলাফল দেখানো
t.join();
return 0;
}উপরের উদাহরণে, std::promise computeSquare ফাংশনে প্রেরণ করা হয়েছে, এবং সেই প্রমিসে মান সেট করা হয়েছে যা মূল থ্রেডে future এর মাধ্যমে পাওয়া গেছে।
৩. Asynchronous Programming (অ্যাসিনক্রোনাস প্রোগ্রামিং)
অ্যাসিনক্রোনাস প্রোগ্রামিং এমন একটি পদ্ধতি যেখানে একাধিক কাজ একে অপরের অপেক্ষা না করেই সম্পন্ন হতে পারে। C++ এ অ্যাসিনক্রোনাস প্রোগ্রামিং std::async, std::future, এবং std::promise এর মাধ্যমে করা যায়।
উদাহরণ:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
void performTask(std::promise<int> prom) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2 সেকেন্ড বিলম্ব
prom.set_value(42); // প্রমিসে মান সেট করা
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
// থ্রেডে অ্যাসিনক্রোনাস টাস্ক শুরু করা
std::thread t(performTask, std::move(prom));
std::cout << "Waiting for result..." << std::endl;
std::cout << "Result: " << fut.get() << std::endl; // প্রমিসের ফলাফল দেখা
t.join();
return 0;
}এখানে performTask ফাংশনটি একটি আলাদা থ্রেডে অ্যাসিনক্রোনাসভাবে কাজ করছে, যেখানে ২ সেকেন্ড বিলম্বের পরে প্রমিস সেট করা হচ্ছে। future.get() কলটি সেই ফলাফলের জন্য অপেক্ষা করে যা টাস্কের শেষের দিকে প্রস্তুত হবে।
Futures এবং Promises এর সুবিধা
১. সহজ মাল্টি-থ্রেডিং: future এবং promise ব্যবহার করে থ্রেডের মধ্যে ডেটা আদান-প্রদান সহজ হয়।
২. সিঙ্ক্রোনাইজেশন: future এবং promise একসঙ্গে কাজ করে সিঙ্ক্রোনাইজড ডেটা ট্রান্সমিশন তৈরি করে। future.get() কল করা হলে, এটি অ্যাসিনক্রোনাসভাবে কাজ সম্পন্ন হওয়া পর্যন্ত অপেক্ষা করে।
৩. Error Handling: promise ব্যবহার করে অ্যাসিনক্রোনাস প্রোগ্রামিংয়ে ত্রুটি নির্ণয় সহজ হয়।
সংক্ষেপে
- Futures: অ্যাসিনক্রোনাস ফাংশনের আউটপুট ধরে রাখে এবং কাজ সম্পন্ন হওয়ার পর তা রিটার্ন করে।
- Promises: অ্যাসিনক্রোনাস অপারেশনে মান সেট করার জন্য ব্যবহৃত হয়, যা
futureএ পৌঁছায়। - Asynchronous Programming:
std::async,std::future, এবংstd::promiseব্যবহার করে অ্যাসিনক্রোনাস কার্য সম্পাদন করা হয়, যা একাধিক কাজ একসঙ্গে সম্পন্ন করতে সহায়ক।
Futures, Promises, এবং Asynchronous Programming মাল্টি-থ্রেডিং প্রোগ্রামিংয়ে কার্যকারিতা এবং কর্মদক্ষতা বাড়ায়, যা আধুনিক প্রোগ্রামিংয়ের জন্য অত্যন্ত উপযোগী।
Read more