- প্রকাশিত
পোসিক্স থ্রেড (pthreads) সহ মাল্টি-থ্রেডিং পার্ট ১ (৬টির মধ্যে) অ্যাডভান্সড সি টপিকস
- লেখক
- লেখক
- নাম
- মো: নাসিম শেখ
- টুইটার
- টুইটার
- @nasimStg
আপনার সি প্রোগ্রামিং দক্ষতা পরবর্তী স্তরে নিয়ে যেতে প্রস্তুত? কীভাবে আপনার প্রোগ্রামগুলিকে একই সাথে একাধিক কাজ করতে হয় তা শিখুন, যা পারফরম্যান্স এবং প্রতিক্রিয়াশীলতা বৃদ্ধি করে, বিশেষ করে মাল্টি-কোর প্রসেসরগুলিতে। এই গাইডটি মাল্টি-থ্রেডিং সহ পোসিক্স থ্রেড (pthreads) ব্যবহার করে আপনার সি অ্যাপ্লিকেশনগুলিতে pthreads ব্যবহারের প্রয়োজনীয় বিষয়গুলির মাধ্যমে আপনাকে নিয়ে যাবে।
সুচিপত্র
মাল্টি-থ্রেডিং কি?
আপনার প্রোগ্রামকে একটি বৃহৎ প্রকল্প ধাপে ধাপে মোকাবেলা করা একক কর্মী হিসাবে কল্পনা করুন। মাল্টি-থ্রেডিং হল একাধিক কর্মী (থ্রেড) নিয়োগ করার মতো যারা একই ওয়ার্কস্পেসের (আপনার প্রোগ্রামের প্রসেস) মধ্যে প্রকল্পের বিভিন্ন অংশে যুগপৎভাবে কাজ করতে পারে। প্রতিটি থ্রেডের নিজস্ব এক্সিকিউশনের ফ্লো থাকে তবে প্রসেসের অন্যান্য থ্রেডের সাথে একই মেমরি স্পেস (গ্লোবাল ভ্যারিয়েবল, হিপ মেমরি) শেয়ার করে।
মাল্টি-থ্রেডিং এর সুবিধা:
- প্যারালালিজম: মাল্টি-কোর সিপিইউগুলিতে, বিভিন্ন থ্রেড বিভিন্ন কোরে সত্যিকার অর্থে একই সাথে চলতে পারে, যা সিপিইউ-বাউন্ড টাস্কগুলির গতি উল্লেখযোগ্যভাবে বাড়িয়ে তোলে।
- প্রতিক্রিয়াশীলতা: GUI প্রোগ্রাম বা সার্ভারগুলির মতো অ্যাপ্লিকেশনগুলিতে, একটি থ্রেড ব্যবহারকারীর ইন্টারঅ্যাকশন বা নেটওয়ার্ক অনুরোধগুলি পরিচালনা করতে পারে যখন অন্য থ্রেডগুলি ব্যাকগ্রাউন্ড টাস্ক সম্পাদন করে, যা অ্যাপ্লিকেশনটিকে ফ্রিজ হওয়া থেকে রক্ষা করে।
- রিসোর্স শেয়ারিং: একই প্রসেসের মধ্যে থ্রেডগুলি মেমরি এবং রিসোর্স শেয়ার করে, যা ইন্টার-প্রসেস কমিউনিকেশন (IPC) এর তুলনায় তাদের মধ্যে যোগাযোগকে তুলনামূলকভাবে কার্যকর করে তোলে।
- দক্ষতা: থ্রেড তৈরি এবং তাদের মধ্যে স্যুইচ করা সাধারণত পৃথক প্রসেস তৈরি এবং পরিচালনা করার চেয়ে কম রিসোর্স-ইনটেনসিভ।
পোসিক্স থ্রেড (pthreads) পরিচিতি
পোসিক্স থ্রেড, যা সাধারণত pthreads নামে পরিচিত, থ্রেড তৈরি এবং পরিচালনার জন্য একটি স্ট্যান্ডার্ডাইজড সি ভাষা প্রোগ্রামিং ইন্টারফেস (API)। এটি ইউনিউক্স-সদৃশ অপারেটিং সিস্টেমগুলিতে (Linux, macOS, Solaris, ইত্যাদি) ব্যাপকভাবে উপলব্ধ।
pthreads ব্যবহার করতে, আপনার প্রয়োজন:
- হেডার ফাইল অন্তর্ভুক্ত করা:
c #include <pthread.h>
- pthreads লাইব্রেরি লিঙ্ক করা: কম্পাইল করার সময়, সাধারণত আপনার
-lpthread
বা-pthread
ফ্ল্যাগ যুক্ত করার প্রয়োজন হয়:bash gcc your_program.c -o your_program -lpthread # or gcc your_program.c -o your_program -pthread
pthread_create
ব্যবহার করে থ্রেড তৈরি করা
একটি নতুন থ্রেড তৈরি করার জন্য মূল ফাংশন হল pthread_create
।
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
আর্গুমেন্টগুলি ব্যাখ্যা করা যাক:
pthread_t *thread
: একটিpthread_t
ভ্যারিয়েবলের পয়েন্টার। সফলভাবে তৈরি হওয়ার পর নতুন তৈরি হওয়া থ্রেডের ID এখানে সংরক্ষণ করা হবে।const pthread_attr_t *attr
: থ্রেড অ্যাট্রিবিউটগুলির পয়েন্টার (যেমন, স্ট্যাকের আকার, শিডিউলিং নীতি)। ডিফল্ট অ্যাট্রিবিউট ব্যবহার করার জন্যNULL
পাস করা সাধারণ মৌলিক ব্যবহারের জন্য।void *(*start_routine) (void *)
: এটি মূল অংশ – নতুন থ্রেড যে ফাংশনটি সম্পাদন করবে তার একটি পয়েন্টার। এই ফাংশনটি অবশ্যই একটিvoid *
আর্গুমেন্ট হিসাবে গ্রহণ করবে এবং একটিvoid *
রিটার্ন করবে।void *arg
:start_routine
ফাংশনে পাস করার জন্য আর্গুমেন্ট। যদি আপনার একাধিক আর্গুমেন্ট পাস করার প্রয়োজন হয়, তবে আপনি সাধারণত সেগুলিকে একটিstruct
-এ র্যাপ করবেন।
রিটার্ন ভ্যালু: pthread_create
সফল হলে 0
এবং ব্যর্থ হলে একটি ত্রুটি নম্বর রিটার্ন করে।
সহজ উদাহরণ:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> // For sleep()
// Function that will be executed by the new thread
void *print_message_function(void *ptr) {
char *message;
message = (char *) ptr;
printf("%s \n", message);
sleep(1); // Simulate some work
printf("Thread finished its work.\n");
return NULL; // Thread exits
}
int main() {
pthread_t thread1; // Thread identifier
const char *message1 = "Hello from Thread 1!";
int iret1;
printf("Main: Creating Thread 1...\n");
// Create the first thread, passing message1 as argument
iret1 = pthread_create(&thread1, NULL, print_message_function, (void*) message1);
if(iret1) {
fprintf(stderr, "Error - pthread_create() return code: %d\n", iret1);
exit(EXIT_FAILURE);
}
printf("Main: Thread 1 created successfully.\n");
// Main thread continues executing...
printf("Main: Doing some other work...\n");
sleep(2); // Let the thread run for a bit
printf("Main: Program finished.\n"); // Note: Main might finish before the thread!
// We need pthread_join for proper waiting.
// exit(EXIT_SUCCESS); // Exit immediately - might kill the thread prematurely
pthread_exit(NULL); // Better way for main to exit and let other threads continue
// until they finish, but join is usually preferred for waiting.
}
pthread_join
ব্যবহার করে থ্রেডগুলির জন্য অপেক্ষা করা
প্রায়শই, ফলাফল সংগ্রহ করতে বা ক্লিনআপ নিশ্চিত করতে মূল থ্রেডের অন্যান্য থ্রেডগুলির এক্সিকিউশন সম্পন্ন হওয়ার জন্য অপেক্ষা করার প্রয়োজন হয়। এটি pthread_join
ব্যবহার করে করা হয়।
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
pthread_t thread
: যে থ্রেডের জন্য অপেক্ষা করতে হবে তার ID।void **retval
: একটিvoid *
এর পয়েন্টার। যদি জয়েন করা থ্রেড একটি মান রিটার্ন করে (return
বাpthread_exit
ব্যবহার করে), তবে সেই মানের একটি পয়েন্টার এখানে সংরক্ষণ করা হবে। যদি আপনি রিটার্ন মান সম্পর্কে চিন্তা না করেন, তবেNULL
পাস করুন।
রিটার্ন ভ্যালু: pthread_join
সফল হলে 0
এবং ব্যর্থ হলে একটি ত্রুটি নম্বর রিটার্ন করে। একটি সাধারণ ত্রুটি হল ESRCH
যদি প্রদত্ত ID সহ কোনও থ্রেড বিদ্যমান না থাকে।
pthread_join
ব্যবহার করে উদাহরণ:
চলুন থ্রেডের জন্য অপেক্ষা করার জন্য পূর্ববর্তী উদাহরণটি পরিবর্তন করি।
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *print_message_function(void *ptr) {
char *message;
message = (char *) ptr;
printf("Thread: Received message: %s \n", message);
sleep(1);
printf("Thread: Work finished.\n");
// Example of returning a value (can be more complex, e.g., struct*)
long thread_result = 42;
return (void*) thread_result;
}
int main() {
pthread_t thread1;
const char *message1 = "Work for Thread 1";
int iret1;
void *thread_return_value;
printf("Main: Creating Thread 1...\n");
iret1 = pthread_create(&thread1, NULL, print_message_function, (void*) message1);
if(iret1) {
fprintf(stderr, "Error - pthread_create() return code: %d\n", iret1);
exit(EXIT_FAILURE);
}
printf("Main: Thread 1 created. ID: %lu\n", (unsigned long)thread1);
// *** Wait for thread1 to complete ***
printf("Main: Waiting for Thread 1 to finish...\n");
iret1 = pthread_join(thread1, &thread_return_value);
if(iret1) {
fprintf(stderr, "Error - pthread_join() return code: %d\n", iret1);
exit(EXIT_FAILURE);
}
printf("Main: Thread 1 finished and joined.\n");
printf("Main: Thread 1 returned value: %ld\n", (long)thread_return_value);
printf("Main: Program finished successfully.\n");
exit(EXIT_SUCCESS);
}
সিনক্রোনাইজেশন: শেয়ার্ড রিসোর্সের সমস্যা
যখন একাধিক থ্রেড একই সাথে শেয়ার্ড ডেটা অ্যাক্সেস এবং সংশোধন করে, তখন আপনি রেস কন্ডিশন নামক সমস্যায় পড়তে পারেন। কল্পনা করুন দুটি থ্রেড একই গ্লোবাল কাউন্টার বৃদ্ধি করার চেষ্টা করছে:
- থ্রেড A কাউন্টারের মান পড়ে (যেমন, ৫)।
- থ্রেড B কাউন্টারের মান পড়ে (যেমন, ৫)।
- থ্রেড A নতুন মান গণনা করে (৫ + ১ = ৬)।
- থ্রেড B নতুন মান গণনা করে (৫ + ১ = ৬)।
- থ্রেড A নতুন মান (৬) কাউন্টারে আবার লিখে।
- থ্রেড B নতুন মান (৬) কাউন্টারে আবার লিখে।
কাউন্টারটি দুবার বৃদ্ধি করা হলেও, চূড়ান্ত মান প্রত্যাশিত ৭ নয়, বরং ৬। এটি ঘটে কারণ রিড-সংশোধন-রাইট অপারেশনটি অ্যাটমিক (অবিভাজ্য) নয়।
রেস কন্ডিশন প্রতিরোধ করার জন্য, আমাদের সিঙ্ক্রোনাইজেশন মেকানিজমের প্রয়োজন। সবচেয়ে সাধারণ হল মিউটেক্স (পারস্পরিক এক্সক্লুশন)।
pthread_mutex_t
) ব্যবহার করা
মিউটেক্স (একটি মিউটেক্স একটি লকের মতো কাজ করে। যে কোনো সময় শুধুমাত্র একটি থ্রেড মিউটেক্স "হোল্ড" করতে পারে। যদি একটি থ্রেড একটি শেয়ার্ড রিসোর্স অ্যাক্সেস করতে চায়, তবে এটিকে প্রথমে মিউটেক্স লক অর্জন করতে হবে। যদি লকটি অন্য কোনো থ্রেড দ্বারা ইতিমধ্যে হোল্ড করা থাকে, তবে অনুরোধকারী থ্রেড ব্লক করবে (অপেক্ষা করবে) যতক্ষণ না লকটি রিলিজ হয়।
মূল মিউটেক্স ফাংশন:
- ইনিশিয়ালাইজেশন:
c pthread_mutex_t my_mutex; int ret = pthread_mutex_init(&my_mutex, NULL); // NULL for default attributes // Or static initialization: // pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
- লক করা:
c int ret = pthread_mutex_lock(&my_mutex); // Blocks if mutex is locked // Access shared resource here...
- আনলক করা:
c int ret = pthread_mutex_unlock(&my_mutex); // Releases the lock
- নষ্ট করা: (মিউটেক্সের সাথে যুক্ত রিসোর্স মুক্ত করা)
c int ret = pthread_mutex_destroy(&my_mutex); // Should be done when mutex is no longer needed
উদাহরণ: একটি শেয়ার্ড কাউন্টার রক্ষা করা
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_THREADS 5
#define ITERATIONS 1000000
long long shared_counter = 0; // The shared resource
pthread_mutex_t counter_mutex; // Mutex to protect the counter
void *increment_counter(void *arg) {
int thread_id = *((int*)arg); // Get thread ID passed as argument
printf("Thread %d starting...\n", thread_id);
for (int i = 0; i < ITERATIONS; ++i) {
// --- Critical Section Start ---
pthread_mutex_lock(&counter_mutex);
shared_counter++; // Access shared resource safely
pthread_mutex_unlock(&counter_mutex);
// --- Critical Section End ---
}
printf("Thread %d finished.\n", thread_id);
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
int ret;
// Initialize the mutex
ret = pthread_mutex_init(&counter_mutex, NULL);
if (ret != 0) {
perror("Mutex initialization failed");
exit(EXIT_FAILURE);
}
printf("Mutex initialized.\n");
printf("Creating %d threads...\n", NUM_THREADS);
for (int i = 0; i < NUM_THREADS; ++i) {
thread_ids[i] = i + 1; // Assign unique ID (1 to NUM_THREADS)
ret = pthread_create(&threads[i], NULL, increment_counter, &thread_ids[i]);
if (ret) {
fprintf(stderr, "Error creating thread %d: %d\n", i + 1, ret);
exit(EXIT_FAILURE);
}
}
printf("Waiting for threads to complete...\n");
for (int i = 0; i < NUM_THREADS; ++i) {
pthread_join(threads[i], NULL);
}
// Destroy the mutex
pthread_mutex_destroy(&counter_mutex);
printf("Mutex destroyed.\n");
// Calculate expected value
long long expected_value = (long long)NUM_THREADS * ITERATIONS;
printf("\nAll threads finished.\n");
printf("Final counter value: %lld\n", shared_counter);
printf("Expected counter value: %lld\n", expected_value);
if (shared_counter == expected_value) {
printf("Success! The counter value is correct.\n");
} else {
printf("Error! Race condition likely occurred (or other issue).\n");
printf("Difference: %lld\n", expected_value - shared_counter);
}
exit(EXIT_SUCCESS);
}
কম্পাইল এবং রান:
gcc multithread_counter.c -o multithread_counter -lpthread
./multithread_counter
pthread_mutex_lock
এবং pthread_mutex_unlock
কলগুলি ছাড়া কাউন্টার উদাহরণটি চালানোর চেষ্টা করুন। রেস কন্ডিশনের কারণে আপনি সম্ভবত দেখবেন যে চূড়ান্ত shared_counter
মান প্রত্যাশিত মানের চেয়ে কম।
অন্যান্য সিঙ্ক্রোনাইজেশন প্রিমিটিভ
মিউটেক্সগুলি মৌলিক হলেও, pthreads আরও জটিল পরিস্থিতির জন্য অন্যান্য সিঙ্ক্রোনাইজেশন সরঞ্জাম সরবরাহ করে:
_ কন্ডিশন ভ্যারিয়েবলস (pthread_cond_t
): নির্দিষ্ট শর্ত সত্য হওয়ার জন্য থ্রেডগুলিকে কার্যকরভাবে অপেক্ষা করার অনুমতি দেয়। মিউটেক্সের সাথে একত্রে ব্যবহৃত হয়। মূল ফাংশন: pthread_cond_wait
, pthread_cond_signal
, pthread_cond_broadcast
। প্রডিউসার-কনজিউমার সমস্যার জন্য কার্যকর। _ সেমাফোরস (semaphore.h
): কঠোরভাবে মূল pthreads API-এর অংশ না হলেও, POSIX সেমাফোরস প্রায়শই থ্রেডগুলির সাথে একাধিক ইউনিট সহ একটি রিসোর্স পুল অ্যাক্সেস নিয়ন্ত্রণ করতে ব্যবহৃত হয়। মূল ফাংশন: sem_init
, sem_wait
, sem_post
, sem_destroy
। -lpthread
বা কখনও কখনও -lrt
এর সাথে লিঙ্ক করা প্রয়োজন। * রিড-রাইট লকস (pthread_rwlock_t
): একাধিক থ্রেডকে একটি রিসোর্স যুগপৎভাবে পড়ার অনুমতি দেয় কিন্তু লেখার জন্য এক্সক্লুসিভ অ্যাক্সেসের প্রয়োজন হয়। যদি লেখাগুলির চেয়ে পড়াগুলি অনেক বেশি ঘন ঘন হয় তবে পারফরম্যান্স উন্নত করতে পারে। মূল ফাংশন: pthread_rwlock_rdlock
, pthread_rwlock_wrlock
, pthread_rwlock_unlock
।
সম্ভাব্য ঝুঁকি এবং বিবেচ্য বিষয়
মাল্টি-থ্রেডেড প্রোগ্রামিং শক্তিশালী হলেও জটিলতা নিয়ে আসে:
- ডেডলক: ঘটে যখন দুই বা ততোধিক থ্রেড চিরতরে ব্লক হয়ে যায়, প্রতিটি অন্যটির দ্বারা ধারণ করা রিসোর্সের জন্য অপেক্ষা করে। উদাহরণ: থ্রেড A মিউটেক্স 1 লক করে, তারপর মিউটেক্স 2 লক করার চেষ্টা করে। থ্রেড B মিউটেক্স 2 লক করে, তারপর মিউটেক্স 1 লক করার চেষ্টা করে। সতর্ক লক অর্ডারিং অত্যন্ত গুরুত্বপূর্ণ।
- জটিলতা: নন-ডিটারমিনিস্টিক এক্সিকিউশন অর্ডারের কারণে মাল্টি-থ্রেডেড প্রোগ্রাম ডিবাগ করা সিঙ্গেল-থ্রেডেড প্রোগ্রাম ডিবাগ করার চেয়ে উল্লেখযোগ্যভাবে কঠিন। রেস কন্ডিশন এবং ডেডলকগুলি শুধুমাত্র নির্দিষ্ট টাইমিং পরিস্থিতিতে দেখা যেতে পারে। GDB (থ্রেড সাপোর্ট সহ) এবং বিশেষায়িত ডিবাগার (Helgrind, ThreadSanitizer) অমূল্য টুল।
- ওভারহেড: থ্রেড তৈরি এবং সিনক্রোনাইজ করার ওভারহেড আছে। খুব সহজ টাস্কের জন্য, ওভারহেড প্যারালালিজমের সুবিধা ছাড়িয়ে যেতে পারে।
- ফ্ল্যাশ শেয়ারিং: ক্যাশ সহ মাল্টি-কোর সিস্টেমগুলিতে, যদি বিভিন্ন কোরের থ্রেডগুলি ঘন ঘন এমন ভ্যারিয়েবল পরিবর্তন করে যা একই ক্যাশ লাইনে থাকে, তবে এটি অতিরিক্ত ক্যাশ ইনভ্যালিডেশন ঘটাতে পারে, যা পারফরম্যান্সকে আঘাত করে এমনকি যদি ভ্যারিয়েবলগুলি লজিক্যালি সরাসরি শেয়ার না হয়।
উপসংহার
পোসিক্স থ্রেড সহ মাল্টি-থ্রেডিং উচ্চ-পারফরম্যান্স, প্রতিক্রিয়াশীল সি অ্যাপ্লিকেশন তৈরির সম্ভাবনা খুলে দেয়। থ্রেড তৈরি (pthread_create
), অপেক্ষা করা (pthread_join
), এবং মিউটেক্স (pthread_mutex_t
) ব্যবহার করে সিনক্রোনাইজেশন বোঝার মাধ্যমে, আপনি কনকারেন্সির শক্তি ব্যবহার করতে পারেন।
মনে রাখবেন যে দুর্দান্ত শক্তির সাথে দুর্দান্ত দায়িত্ব আসে। সঠিক, কার্যকর এবং শক্তিশালী মাল্টি-থ্রেডেড কোড লেখার জন্য সতর্ক ডিজাইন, সিনক্রোনাইজেশন বিবরণে মনোযোগ এবং রেস কন্ডিশন এবং ডেডলকের মতো ঝুঁকি এড়াতে পুঙ্খানুপুঙ্খ টেস্টিং প্রয়োজন। এই পরিচিতিটি আরও অ্যাডভান্সড থ্রেডিং কনসেপ্ট অন্বেষণ করতে এবং সি-তে শক্তিশালী কনকারেন্ট অ্যাপ্লিকেশন তৈরি করার জন্য একটি দৃঢ় ভিত্তি সরবরাহ করে।
প্রস্তাবিত পাঠ: _ (ডিবাগিং আর্টিকেলের লিঙ্ক): GDB সহ কার্যকরভাবে সি প্রোগ্রাম ডিবাগ করা সম্পর্কে জানুন (মাল্টি-থ্রেডেড কোডের জন্য অপরিহার্য!)। _ (নেটওয়ার্ক প্রোগ্রামিং এর লিঙ্ক): সকেট ব্যবহার করে সি-তে নেটওয়ার্ক প্রোগ্রামিং-এ থ্রেডিং কীভাবে ব্যবহৃত হয় তা অন্বেষণ করুন। * (অ্যাডভান্সড সি টপিকসের লিঙ্ক): আরও অ্যাডভান্সড সি টপিকসের জন্য, আমাদের অ্যাডভান্সড সি প্রোগ্রামিং সিরিজ দেখুন।