کاملترین آموزش کار با Thread ها در لینوکس به زبان ساده

به فرآیند ایجاد و مدیریت واحدهای اجرایی چندگانه درون یک پردازه threading گفته می شود. وقتی که صحبت از threading می شود، باید مفاهیم دیگری مانند شرایط مسابقه بین داده ها و بن بست نیز مطرح شود. سر فصل threading آنقدر گسترده است که به راحتی می تواند کل یک کتاب را پوشش دهد. در اینجا سعی می کنیم موارد پایه ای مربوط Linux threading API را توضیح دهیم.

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران

مطالب پیشرو از شش بخش تشکیل شده است. در بخش اول نگاهی بسیار اجمالی به استاندارد POSIX می اندازیم. در بخش دوم مفهوم نخ را بیان کرده و مزایا و معایب آن بررسی می شود. در بخش سوم توابع پایه ای مورد نیاز برای کنترل و مدیریت نخ ها آورده شده است. بخش چهارم مخصوص مسائل همگام سازی نخ ها است و در آن جا دو ابزار همزمانی پردازش یعنی سمافورها و قفل های mutex توضیح داده شده است. در پایان، در بخش های پنجم و ششم دو مسئله کلاسیک همگام سازی پیاده سازی شده است.

استاندارد POSIX چیست؟

در اواسط دهه 1980، انستیتوی مهندسین برق و الکترونیک (IEEE) تصمیم به استاندارد سازی system-level interfaceها برای سیستم های یونیکس گرفت. ریچارد استالمن، موسس Free Software movement پیشنهاد کرد که نام این استاندارد POSIX باشد. این نام مخفف Portable Operating System Interface است.اولین نتیجه این تلاش که در سال 1988 منتشر شد، IEEE Std 1003.1-1988 بود (که به اختصار از آن به عنوان POSIX 1988 یاد می شود).

در سال 1990، IEEE استاندارد POSIX را مورد تجدید نظر قرار داد و نتیجه آن IEEE Std 1003.1-1990 (یا همان POSIX 1990) شد. پشتیبانی اختیاری از threading و real-time به ترتیب در استانداردهای IEEE Std 1003.1b-1993 (که به آن POSIX 1993 یا POSIX.1b نیز گفته می شود) و IEEE Std 1003.1c-1995 (که به آن POSIX 1995 یا POSIX.1c نیز میگویند) مستند سازی شد. در سال 2001، استاندارهای اختیاری با محوریت POSIX 1990 با هم دیگر ادغام و یک استاندارد واحد به نام IEEE Std 1003.1-2001 (یا POSIX 2001) به وجود آمد. آخرین تجدید نظر IEEE Std 1003.1-2008 (به آن POSIX 2008 نیز می گویند) بود که در دسامبر سال 2008 بیرون داده شد.

نخ یا Thread چیست؟

نخ یا ریسمان کوچک ترین واحد کاری است که می توان وقت پردازنده را به آن اختصاص داد. در سیستم عامل های امروزی یک پردازه می تواند دارای چندین نخ باشد که هر کدام کار خاصی را انجام می دهند. به این نخ ها «پردازه های سبک وزن» می گویند. این پردازه های سبک وزن دارای ویژگی های زیر هستند:

  • گروهی از نخ های مربوط به یک پردازه سنگین وزن دارای کد برنامه، فضای آدرس دهی و منابع سیستم عامل مشترک هستند.
  • اجرای یک نخ در داخل یک task انجام می شود.
  • هر نخ دارای ثبات ها و انباره خاص خود است.
  • جا به جا شدن در بین نخ در مقابل تعویض متن خیلی راحت تر است.
  • ایجاد یک نخ جدید در داخل یک فرآیند کم هزینه است.
  • نخ ها می توانند همگام یا ناهمگام با یکدیگر کار کنند.

نخ ها از بسیاری جهات نظیر فرآیندها عمل می کنند و می توانند در یکی از حالات آماده، مسدود و اجرا باشند. یک نخ درون یک فر آیند به طور طبیعی اجرا می شود و هر یک شمارنده برنامه و پشته ی خود را داراست.مفهوم نخ ها برای مدتی مطرح بود، اما قبل از اینکه کمیته IEEE POSIX استانداردهایی را در این زمینه منتشر بکند، این مفاهیم به طور گشترده در سیستم عامل های UNIX-like عرضه نشده بودند. هم چنین فروشنده های مختلف پیاده سازی های متفاوتی از تخ ها را ارائه کرده بودند.

با ظهور استاندارد POSIX 1003.1c همه چیز فرق کرد. استانداردسازی نخ ها بهتر شد و این مفهوم در بسیاری از توزیع های لینوکس عرضه شد. هم اکنون بسیاری از کامپیوترها دارای پردازنده های چند هسته ای و سخت افزارهایی هستند که به نخ ها این اجازه را می دهد که به طور فیزکی، همزمان اجرا شوند.قبل از ظهور پردازنده های چند هسته ای، پردازنده های تک هسته ای، اجرای همزمان نخ ها را با سویچ کردن های پی در پی پیاده سازی می کردند.

لینوکس برای اولین بار در سال 1996، با کتابخانه ای که از آن با نام «LinuxThreads» یاد می شود، پشتیبانی از نخ را آغاز کرد. این کتابخانه بسیار نزدیک به استاندارد POSIX بود و برای اولین بار برنامه نویسان لینوکس را قادر می کرد که از نخ ها در برنامه های خود استفاده کنند. با این وجود، یکسری اختلافات جزیی بین پیاده سازی لینوکس و استاندارد POSIX وجود داشت. این اختلافات بیشتر مربوط به اداره کردن سیگنال ها (signal handling) بود.

پروژه های مختلفی درباره ی چگونگی بهبود پشتیبانی از نخ در لینوکس انجام شده است. این پروژه ها نه تنها به دنبال رفع اختلافات جزیی میان استاندارد POSIX و نحوه پیاده سازی در لینوکس بودند، بلکه به دنبال این بودند که چگونه می توان کارایی را افزایش داد و محدودیت های غیر ضروری را نیز رفع کرد. بیشتر تمرکز بر روی این بود که چگونه نخ های سطح کاربر باید به نخ های سطح کرنل نگاشت شود.

دو پروژه اصلی در این زمینه New Generation POSIX Threads (به اختصار NGPT) و Native POSIX Thread Library (به اختصار NPTL) نام داشت. هر دوی این پروژه ها تغییراتی را در کرنل لینوکس ایجاد کردند، به گونه ای که از کتابخانه های جدید پشتیبانی کند.در سال 2002، تیم NGPT اعلام کرد که اضافه کردن ویژگی های جدید به NGPT را متوقف می کنند اما همچنان به تحقیق برای پشتیبانی از نخ در لینوکس ادامه می دهند. در نهایت NPTL با عرضه خود در توزیع Red Hat Linux 9، تبدیل به یک استاندارد جدید برای نخ ها در لینوکس شد.

مزایا و معایب نخ ها چه هستند؟

ایجاد یک نخ جدید در بعضی از شرایط خاص مزیت های بارزی نسبت به ایجاد یک پردازه ی جدید دارد. سربار و هزینه ایجاد یک نخ جدید به طرز قابل توجهی کم تر از ایجاد یک پردازه جدید است.در ادامه به بعضی از مزیت های استفاده از نخ ها اشاره شده است:

  • گاهی اوقات نیاز داریم که یک برنامه در یک زمان دو کار را انجام دهد. یک مثال کلاسیک از این موضوع شمارش کلمات یک سند به هنگام ویرایش متن آن است. یک نخ می تواند ورودی کاربر را مدیریت کرده و کار ویرایش را انجام دهد. نخ دیگر می تواند محتوای همان سند را ببیند و به طور پیوسته یک متغیر را که تعداد کلمات را نگه داری می کند، بروز رسانی کند.
  • کارایی یک برنامه که ترکیبی از ورودی، کار محاسباتی و خروجی است را می توان با اجرای هر یک از این کارها به عنوان نخ های جداگانه افزایش داد. وقتی نخ ورودی یا نخ خروجی در انتظار برقراری یک اتصال است، نخ دیگر می تواند کار محاسباتی خود را ادامه دهد.
  • هم اکنون که پردازنده های چند هسته ای متداول شده اند، استفاده از چندین نخ درون یک پردازه می تواند سبب استفاده ی بهتر از منابع سخت افزاری شود (البته در صورتی که برنامه این قابلیت را داشته باشد).
  • به طور کلی سیستم عامل به هنگام سویچ کردن بین نخ ها کار کم تری را نسبت به وقتی که می خواهد بین پردازه ها سویچ کند، انجام می دهد. همچنین تقاضای چندین نخ برای به دست گرفتن یک منبع نسبت تقاضاهایی که چندین پردازه برای یک منبع دارند، بسیار کم تر است.

نخ ها همچنین دارای اشکالاتی نیز هستند:

  • دیباگ کردن یک برنامه چند نخی بسار بسیار سخت تر از دیباگ کردن یک برنامه تک نخی است، زیرا کنترل تعامل بین نخ ها بسیار مشکل است.
  • یک برنامه که یک کار محاسباتی سنگین را به دو قسمت تقسیم کرده و هر کدام را بر روی نخ های جداگانه اجرا می کند، لزوما بر روی یک ماشین تک پردازنده ای سریع تر اجرا نمی شود. مگر اینکه کار محاسباتی به گونه ای باشد که واقعا به قسمت های مختلف اجازه دهد که به طور همزمان با هم محاسبه شوند و ماشینی که آن را اجرا می کند نیز دارای چندین هسته پردازشی باشد تا به طور واقعی از پردازش چندگانه پشتیبانی کند.

اولین کاربرد Thread ها در لینوکس چیست؟

در حال حاضر مجموعه ی کاملی از فراخوانی های کتابخانه ای مرتبط با نخ ها وجود دارد که که نام بیشتر آن ها با pthread شروع می شود. برای استفاده ار این فراخوانی های کتابخانه ای، باید ابتدا ماکروی REENTRANT_ را تعریف کنیم، سپس فایل pthread.h را ضمیمه کرده و با کتابخانه ی نخ ها با استفاده از lpthread- یک پیوند برقرار کنیم.

وقتی روتین های کتابخانه ای ابتدایی POSIX و UNIX طراحی می شد، اینطور در نظر گرفته شد که فقط یک تک نخ اجرایی درون هر پردازه وجود دارد. یک مثال واضح errno است. از این متغیر برای بازیابی اطلاعات خطا بعد از به شکست انجامیدن یک فراخوانی استفاده می شود. در یک برنامه چند نخه به طور پیش فرض یک متغیر errno وجود دارد که بین همه ی نخ ها به اشتراک گذاشته شده است.

قبل از اینکه یک نخ قادر به بازیابی اطلاعات خطا از این منتغیر باشد، نخ دیگر می تواند به راحتی محتویات این متغیر را بروز رسانی کند (در نتیجه اطلاعات آن را از بین ببرد). توابعی مانند fputs نیز چنین مشکلاتی را دارند. این تابع به طور نرمال از یک فضای عمومی برای بافر کردن خروجی استفاده می کند.

شما به روتین های خاصی به نام روتین های re-entrant نیاز دارید. کد Re-entrant را می توانیم چه به وسیله ی نخ های متفاوت و چه از طریق فراخوانی های تو در تو، چندین بار فراخوانی کنیم به گونه ای که مشکلی پیش نیاید و عملکرد درستی را شاهد باشیم. با این وجود بخش بازگشتی کد معمولا باید از متغیرهای محلی تنها به طریقی استفاده کند که هر فراخونی، کپیِ یکتای مخصوص به خودش را از داده ها به دست آورد. (reentrant : روالى که مى تواند توسط چندين برنامه مستقل بطور همزمان بکار برده شود).

در برنامه های چند نخی، شما با تعریف ماکروی REENTRANT_ قبل از هر یک از خطوط include# درون برنامه، به کامپایلر می گویید که به این ویژگی نیاز دارید. این کار سبب رخ دادن سه چیز می شود:

  • نمونه های امن بازگشتی برای بعضی از توابع به دست می آید. نام این توابع به صورت قبل است، با این تفاوت که r- به آنها اضافه می شود. مثلا تابع gethostbyname به صورت gethostbyname_r تغییر می کند.
  • بعضی ار توابع stdio.h که در حالت نرمال به صورت ماکرو پیاده سازی شده اند، تبدیل به توابع امن بازگشتی مناسبی می شوند.
  • متغیر errno از فایل errno.h به گونه ای تغییر می کند که یک تابع را صدا بزند. این تابع قادر است به طریقی امن، مقدار واقعی errno را در یک برنامه ی چند نخی تعیین کند.

ضمیمه کردن فایل pthread.h برای شما تعاریف و نمونه های دیگری را فراهم می کند که در کد برنامه به آن ها نیاز پیدا خواهید کرد؛ بسیار شبیه به stdio.h برای روتین های استاندارد ورودی و خروجی. در پایان شما باید مطمئن شوید که فایل های سرآیند مناسبی را برای کار با نخ ها ضمیمه کرده اید و همچنین با کتابخانه نخ مناسبی که توابع pthread.h را پیاده سازی می کنند، پیوند برقرار کرده اید.

در ادامه به معرفی توابع پایه ای برای مدیریت نخ ها می پردازیم.

تابع pthread_create چیست؟

تابع pthread_create مثل تابع fork که یک فرآیند جدید را ایجاد می کند، سبب به وجود آمدن یک نخ جدید می شود.

#include <pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

اولین آرگومان یک اشاره گر به pthread_t می باشد. شناسه نخ به وجود آمده درون حافظه ای که این آرگومان به آن اشاره می کند نوشته می شود. به وسیله این شناسه می توانید به نخ رجوع کنید.دومین آرگومان صفات نخ را تنظیم می کند. معمولا نیازی به تعیین صفت خاصی برای نخ نیست، به همین دلیل به جای این آرکومان می توانید مقدار NULL را بفرستید.سومین و چهارمین آرگومان به ترتیب تابع و آرگومان های تابعی است که نخ باید شروع به اجرای آن کند.

void (start_routine)(void *)

خط قبل می گوید که شما باید آدرس یک تابع را ارسال کنید که یک اشاره گر به void را به عنوان پارامتر گرفته و تابع یک اشاره گر به void را برمی گرداند. شما می توانید هر نوعی را به عنوان آرگومان فرستاده و اشاره گری به هر نوع دلخواه را برگردانید.در صورت موفقیت، مقدار بازگشتی این تابع 0 می باشد. در صورت بروز مشکل شماره های دیگری برگردانده می شود.تابع pthread_create مانند بسیاری از توابع pthread-، جز محدود توابع لینوکس هستند که از قرارداد بازگرداندن مقدار 1 در صورت برورز خطا پیروی نمی کند. با این وجود بهتر است که همیشه صفحات راهنما را برای آگاهی از مقادیر بازگشتی بررسی کنیم.

تابع pthread_exit چیست؟

وقتی یک نخ خاتمه پیدا می کند، تابع pthread_exit را صدا می زند، درست مثل پردازه ها که به هنگام خاتمه تابع exit را فراخوانی می کنند. این تابع نخ را خاتمه می دهد و یک اشاره گر به یک شی را برمی گرداند. هیچ وقت از این برای برگرداندن یک اشاره گر به یک متغیر محلی استفاده نکنید، زیرا متغیرهای محلی در صورت از بین رفتن نخ ها دیگر وجود نخواهند داشت و همین سبب به وجود آمدن باگ های جدی خواهند شد. pthread-exit به صورت زیر اعلان می شود:

#include <pthread.h>
void pthread_exit(void *retval);

تابع pthread_join چیست؟

تابع دیگری که به معرفی آن می پردازیم تابع pthread_join است. کار این تابع مانند تابع wait می باشد. همانطور که می دانید در صورت استفاده از تابع wait، پردازه والد در انتظار خاتمه پردازه های فرزند می ماند. این تابع به صورت زیر اعلان می شود:

#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);

اولین آرگومان شناسه نخی است که باید برای آن صبر کنیم.دومین آرگومان اشاره گری به یک اشاره گر است که خود به مقدار بازگشتی نخ اشاره می کند. (به زبان ساده، مقدار بازگشتی نخ، یعنی همان مقداری که توسط تابع pthread_exit مشخص می شود، درون این آرگومان قرار می گیرد).مثل تابع pthread_create این تابع نیز در صورت موفقیت مقدار 0 و در صورت شکست کد خطای دیگری را برمی گرداند.در ادامه با یک مثال، نحوه به کارگیری توابع ذکر شده در بالا را نشان می دهیم.

#include "stdlib.h"
#include "stdio.h"
#include "unistd.h"
#include "string.h"
#include "pthread.h"

void *MyThreadFunc(void *args){
	printf("from thread:: thread is running.....\nfrom thread:: function args: %s\n",(char *)args);
	srand(time(NULL));
	int rnd=rand()%6;
	sleep(rnd);
	printf("from thread:: thread execution lasts %d second(s)\n",rnd);
	pthread_exit("good by world");
}

void main(){
	pthread_t thread_id;
	void *thread_result;
	if(pthread_create(&thread_id,NULL,MyThreadFunc,"Hello World")==0){
		printf("from main:: waiting for thread %d  .....\n\n",thread_id);
		if(pthread_join(thread_id,&thread_result)==0){
			printf("\nfrom main:: thread return value: %s\n",(char *)thread_result);
		}
	}
}

به فرض اینکه نام برنامه thread1 باشد، برای کامپایل آن باید به صورت زیر عمل کنیم:

cc -D_REENTRANT thread1.c -o thread1
  • نمونه اجرای برنامه
وب سایت توسینسو


همگام سازی چیست؟

مجموعه ای از توابع وجود دارند که به طور خاص برای کنترل اجرای نخ ها و دسترسی به نواحی بحرانی کد طراحی شده اند.در اینجا ما بر روی دو متد پایه تمرکز می کنیم: اولین مورد سمافورها هستند که عملکردشان مثل یک دروازه بان برای یک قطعه کد است.

دومین مورد mutexها می باشند. mutexها وسیله ای برای فراهم کردن انحصار متقابل هستند و به واسطه ی آن ها می توانیم بخش هایی از کد را حفاظت کنیم. این دو متد مشابه هم هستند؛ به این معنی که هر یک از آن ها را می توانیم به وسیله دیگری پیاده سازی کنیم. با این وجود معمولا ما توجه به معنای مسئله داده شده تصمیم می گیریم که از کدام یک از این دو متد استفاده کنیم و در واقع یکی را بر دیگری ترجیح می دهیم.

همگام سازی به وسیله سمافورها

برای کار کردن با سمافورها دو مجموعه جداگانه از توابع وجود دارد: یکی از آن ها برگرفته شده از POSIX Realtime Extensions هستند و برای کار با نخ ها استفاده می شوند. دیگری مشهور به System V Semaphores هستند که به طور متداول برای همگام سازی پردازه ها مورد استفاده قرار می گیرند. این دو مجموعه بسیار شبیه به همدیگر هستند اما از جا که نحوه پیاده سازیشان فرق می کنند هیچ تضمینی وجود ندارد که این دو مجموعه را بتوان به جای همدیگر استفاده کرد.دایجسترا برای اولین بار مفهوم سمافورها را ارائه کرد. سمافور نوع خاصی از متغیر می باشد که می توان آن را افزایش یا کاهش داد. دسترسی های بحرانی به این متغیر حتی در برنامه های چند نخی به صورت اتمیک رخ می دهد.

در اینجا ما بر روی ساده ترین نوع از سمافورها یعنی سمافورهای باینری تمرکز می کنیم. سمافورهای باینری تنها می توانند مقدار 0 و 1 را به خود بگیرند. نوع دیگری از سمافورها به نام سمافورهای شمارشی نیز وجود دارد که بازه ی وسیع تری از مقادیر را می تواند به خود بگیرند. در کد برنامه ممکن است بخشهایی وجود داشته باشد که تنها در هر زمان یکی از نخ های اجرایی اجازه اجرای آن (یا به عبارت دیگر اجازه ورود به آن) را داشته باشد. معمولا برای حافاظت از چنین بخش هایی از سمافورهای باینری استفاده می شود.

گاهی اوقات می خواهید تنها به تعداد محدودی از نخ ها اجازه ی اجرای یک قطعه کد را بدهید. در این گونه موارد از سمافورهای شمارشی استفاده می شود. سمافورهای شمارشی زیاد متداول نیستند و تنها می توان گفت که آنها یک بسط منطقی از سمافورهای باینری می باشند.بر خلاف بسیاری از توابع خاص منظوره که برای کار با نخ ها به وجود آمده اند، توابع مربوط به سمافورها با pthread_ شروع نشده و به جای آن با sem_ شروع می شوند. چهار تابع پایه وجود دارد که از آنها می توانیم برای کار با سمافورها درون نخ ها استفاده کنیم:

تابع sem_init چیست؟

برای ایجاد یک سمافور از تابع sem_init استفاده می شود. اعلان این تابع به صورت زیر است:

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

پارامترهای تابع شامل چه چیزهایی می شوند؟

  • sem : بعد از ایجاد و مقدار دهی اولیه سمافور، پارامتر sem به سمافور به وجود آمده اشاره خواهد کرد. در نتیجه از این پارامتر می توان برای رجوع به سمافور استفاده کرد.
  • pshared : این پارامتر نوع سمافور را کنترل می¬کند. اگر مقدار این پارامتر را 0 قرار دهیم، سمافور برای پردازه کنونی محلی خواهد بود. در غیر این صورت سمافور بین پردازه های مختلف به اشتراک گذاشته خواهد شد.
  • value : این پارامتر مقدار اولیه سمافور می باشد.

توابع sem_wait و sem_post چه کاری انجام می دهند؟

دو تابع sem_wait و sem_post مقدار سمافور را کنترل می کنند. نحوه اعلان این دو تابع در زیر آمده آمده است:

#include <semaphore.h>
int sem_wait(sem_t * sem);
int sem_post(sem_t * sem);

این دو تابع یک اشاره گر به سمافور (که به وسیله فراخوانی sem_init مقدار دهی اولیه شده است) را به عنوان پارامتر دریافت می کنند.تابع sem_post به صورت اتمیک مقدار سمافور را یک عدد افزایش می دهد. اتمیک به این معناست که اگر دو نخ به طور همزمان بخواهند مقدار سمافور را افزایش دهند، این عمل افزایش دادن در یک مرحله انجام شود در نتیجه اگر مقدار اولیه سمافور 0 باشد، مقدار نهایی سمافور بعد از این عمل افزایش همیشه 2 خواهد بود.

توجه کنید که در صورت استفاده از متغیرهای عادی چنین تضمینی وجود ندارد که بعد از دسترسی همزمان دو نخ به یک متغیر و افزایش آن، مقدار نهایی آن همیشه یکی باشد. دلیل این است که برای متغیرهای عادی، این عمل افزایش در سطح ثباتی در چند مرحله رخ می دهد و همین سبب به وجود آمدن شرایط مسابقه می شود. در زیر مثالی برای نشان دادن این موضوع آورده شده است.فرض کنید که که دو پردازه به طور همزمان می خواهند دستور ++x را اجرا کنند (x یک متغیر مشترک بین دو پردازه هستند). همچنین فرض کنید که این دستور در سطح ثباتی به صورت زیر اجرا می شوند.

load x into register

add 1 to register

store register in x

اگر در ابتدا x=5 باشد، هر کدام از نتایج زیر ( و همچنین نتایج دیگر) می توانند رخ دهند:

وب سایت توسینسو
وب سایت توسینسو
وب سایت توسینسو

همانطور که مشاهده می کنید در دو مورد مقدار نهایی x برابر 7 و در مورد آخر برابر 6 شده است.تابع sem_wait مقدار سمافور را به صورت اتمیک یکی کاهش می دهد، اما تا زمانی که سمافور یک مقدار غیر صفر نداشته باشد، wait می کند. یعنی اینکه اگر شما تابع sem-wait را برای یک سمافور با مقدار 2 صدا بزنید، نخ همچنان به اجرای خود ادامه می دهد اما مقدار سمافور به 1 کاهش پیدا می کند.

اگر تابع sem-wait بر روی یک سمافور با مقدار 0 صدا زده شود، تابع تا زمانی که نخ دیگری مقدار سمافور را افزایش ندهد (و در نتیجه مقدار مقدار سمافور غیر صفر نباشد)، صبر خواهد کرد. اگر دو نخ درون تابع sem-wait در انتظار سمافور باشند تا مقدار آن غیر صفر شود و نخ سومی مقدار سمافور را افزایش دهد، تنها یکی از دو نخ در حال انتظار قادر به کاهش مقدار سمافور و ادامه کار خود خواهد بود. نخ دیگر همچنان در حال انتظار می ماند. این قابلیت اتمیک «test and set» درون یک تابع چیزی است که استفاده از سمافورها را بسیار ارزشمند کرده است.

تابع sem_destroy چیست؟

آخرین تابع برای کار با سمافورها، تابع sem_destroy می باشد. وقتی کارتان با سمافور تمام شد، می توانید با استفاده از این تابع سمافور را پاک کنید. اعلان این تابع به صورت زیر است:

#include <semaphore.h>
int sem_destroy(sem_t * sem);

این تابع یک اشاره گر به سمافور را دریافت و تمام منابعی را که در اختیار گرفته است را آزاد می کند. اگر نخی در انتظار به دست گرفتن یک سمافور باشد و شما اقدام به تخریب آن کنید، با یک خطا مواجه خواهید شد.مانند بسیاری از توابع لینوکس، این توابع نیز در صورت موفقیت مقدار 0 را برمی گردانند.

همگام سازی به وسیله mutexها چگونه انجام می شود؟

تکنیک های زیادی برای اتمیک کردن نواحی بحرانی وجود دارد. این تکنیک ها می توانند تنها شامل یک خط دستور یا بلوک های زیادی از کد شوند. متداول ترین تکنیک استفاده از قفل می باشد. این تکنیک مکانیزمی برای اطمینان از وجود انحصار متقابل درون نواحی بحرانی است و این نواحی را به صورت اتمیک رندر می کند. قفل ها سبب اجرای مفهوم انحصار متقابل (mutual exclusion ) می شوند، به همین دلیل از آن ها به عنوان mutex یاد می کنند.

کار قفل ها در اینجا دقیقا مشابه کارشان در دنیای واقعی است. یک اتاق را به عنوان ناحیه بحرانی فرض کنید. بدون وجود قفل مردم (نخ ها) هر موقع که بخواهند می توانند به اتاق (ناحیه بحرانی) رفت و آمد داشته باشند. در این حالت، چند نفر می توانند به طور همزمان درون اتاق قرار بگیرند. حال فرض کنید که برای اتاق یک درب قرار می دهیم و بر روی آن درب یک قفل می گذرایم. هم چنین فرض کنید که تنها یک کلید برای این درب ساخته شده است و کنار درب گذاشته شده است.

وقتی یک فرد (نخ) به مقابل درب می رسد، کلید قفل را پیدا کرده، درب را باز می کند و وارد اتاق می شود. فرد پس از ورد به اتاق درب را از داخل قفل می کند. در این حالت هیچ کس دیگری نمی تواند وارد اتاق شود. همانطور که مشاهده می کنید، چند نفر نمی توانند به طور همزمان اتاق را اشغال کنند، یعنی اتاق تبدیل به منبعی شده است که انحصار متقابل برای آن رعایت می شود. وقتی فرد کارش با اتاق تمام شد، قفل درب را باز می کند و از اتاق خارج می شود. همچنین کلید را بیرون درب رها می کند. سپس فرد دیگری می تواند وارد اتاق شود، درب را پشت سرش قفل کند و مراحل قبل را تکرار کند.

عملکرد قفل به هنگام کار کردن با نخ ها بسیار مشابه مثال ذکر شده در بالا است. برنامه نویس قفل را تعریف کرده و مطمئن می شود که قبل از وارد شدن به ناحیه بحرانی آن را به دست می آورد. پیاده سازی قفل تضمین می کند که در هر زمان فقط یک نخ می تواند قفل را در اختیار بگیرد. اگر قفل در اختیار نخ دیگری باشد، نخ جدید باید قبل از ادامه کار خود برای به دست گرفتن قفل صبر کند. وقتی کارتان با یک ناحیه بحرانی تمام شد، باید قفل را آزاد کنید. با این کار شما به نخ دیگری که در حال انتظار برای قفل است اجازه می دهید که قفل را در اختیار گرفته و به کار خود ادامه دهد.

ما تنها قسمت هایی از یک تابع را قفل می کنیم که امکان دارد برای آن ها شرایط مسابقه اتفاق بیفتد. بهتر است تا آن جا که می توانید قسمت های بحرانی را کوچک تر کنید، چون قفل ها مانع همزمانی شده و در نتیجه باعث خنثی شدن مزیت های threading می شوند.توابع پایه ای مورد نیاز برای استفاده از mutexها بسیار شبیه به توابع استفاده شده برای سمافورها هستند. این توابع در زیر لیست شده اند.

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex));
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

مثل همیشه در صورت اجرای موفقیت آمیز این توابع عدد 0 و در صورت شکست کد خطا برگردانده می شود. در صورت بروز خطا چیزی در متغیر errno قرار نمی گیرد و شما برای پی بردن به نوع خطا باید مقدار برگشت داده شده را امتحان کنید.مثل سمافورها همه ی این توابع یک اشاره گر به یک شی از قبل تعریف شده را دریافت می کنند. در این جا این اشاره گر باید از نوع pthread_mutex-t باشد. دومین پارامتر تابع pthread-mutex-init به شما اجازه می دهد که برای کنترل رفتار mutex یکسری ویژگی برای آن تعریف کنید. نوع ویژگی پیش فرض «fast» می باشد.

این ویژگی یک اشکال ناچیر دارد، به این صورت که اگر برنامه سعی به فراخوانی pthread-mutex-lock بر روی یک mutex از قبل قفل شده بکند، برنامه بلوکه خواهد شد. زیرا نخی که قفل را در اختیار دارد هم اکنون بلوکه شده است و در نتیجه mutex هیچ نمی تواند از حالت قفل دربیاید و برنامه وارد بن بست می شود. این امکان وجود دارد که ویژگی های mutex را به گونه ای تغییر دهیم که در صورت بروز چنین اتفاقی یک کد خطا برگشت داده شود یا به نخ اجازه دهد که چندین بار عمل قفل کردن را انجام دهد. در اینجا به چگونگی تنظیم این ویژگی ها نمی پردازیم و به جای آن مقدار NULL را قرار می دهیم تا mutex از رفتار پیش فرض استفاده کند.

لغو یک نخ (thread cancellation) چگونه انجام می شود؟

یک نخ می تواند به نخ دیگر یک درخواست بفرستد و از طریق آن، خاتمه نخ را خواستار شود. اعلان تابعی که این کار را انجام می دهد در زیر آورده شده است

#include <pthread.h>
int pthread_cancel(pthread_t thread);

با فرستادن شناسه یک نخ به این تابع، می توانید خاتمه ی آن نخ را درخواست کنید.نخی که درخواست خاتمه را دریافت می کند، می تواند به وسیله تابع زیر آن را قبول و یا رد کند.

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);

اولین پارامتر دو مقدار PTHREAD_CANCEL_ENABLE و PTHREAD_CANCEL_DISABLE را می تواند به خود بگیرد. PTHREAD_CANCEL_ENABLE سبب دریافت درخواست لغو و PTHREAD_CANCEL_DISABLE سبب نادیده گرفتن درخواست لغو می شود. اشاره گر oldstate به شما اجازه ی بازیابی حالت قبل را می دهد. درصورت پذیرش درخواست لغو نخ، سطح دیگری از کنترل به وسیله تابع pthread_setcanceltype قابل اعمال است. اعلان این تابع در آورده شده است.

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);

پارامتر type می تواند یکی از دو مقدار PTHREAD_CANCEL_ASYNCHRONOUS و PTHREAD_CANCEL_DEFERRED را به خود بگیرد. PTHREAD_CANCEL_ASYNCHRONOUS باعث می شود که درخواست های لغو، بلافاصله اجرا شوند. PTHREAD_CANCEL_DEFERRED باعث می شود که تا زمانی که نخ یکی از توابع زیر را اجرا نکرده، درخواست های لغو در حال انتظار باقی بمانند.

pthread-join
 pthread-cond-wait 
pthread-cond-timedwait 
pthread-testcancel
sem-wait
sigwait

به طور پیش فرض، نخ ها با حالت لغو PTHREAD_CANCEL_ENABLE و نوع لغو PTHREAD_CANCEL_DEFERRED شروع به کار می کنند.در مقاله بعد، دو مسئله کلاسیک همگام سازی را پیاده سازی می کنیم.برای تهیه قسمت های 1 الی 3 از این سری مقالات از مراجع زیر استفاده شده است.

References

1. Matthew, Neil and Stones, Richard. Beginning Linux Programing. s.l. : Wrox. pp. 495-524. 978-0-470-14762-7.

2. Love, Robert. Linux System Programming. s.l. : O'Reilly. pp. 222-224. 978-1-449-33953-1.

3. POSIX. [Online] http://en.wikipedia.orgwikiPOSIX.

پیاده سازی مسئله کلاسیک تولید کننده-مصرف کننده

در زیر کد مسئله تولید کننده-مصرف کننده با در نظر گرفتن اندازه بافر 1 آورده شود. برای پیاده سازی این مسئله از سمافور استفاده شده است.

#include "stdlib.h"
#include "stdio.h"
#include "unistd.h"
#include "string.h"
#include "pthread.h"
#include "semaphore.h"

int sharedVar;
sem_t mutex;
sem_t full;
sem_t empty;
int counter;

//	کد مصرف کننده
void *ConvertToBinary(void *args)
{
	while(1){
		sem_wait(&full);
		sem_wait(&mutex);
		printf("thread 2:: converting '%d' to binary ....\n",sharedVar);
		int temp[128];
		int i=-1;
		while(sharedVar!=0){
			i++;
			temp[i]=sharedVar%2;
			sharedVar=sharedVar/2;
		}
		printf("thread 2:: result is: ");
		while(i>=0){
			printf("%d",temp[i]);
			i--;
		}
		printf("\n\n");
		sem_post(&mutex);
		sem_post(&empty);
	}
	pthread_exit(NULL);
}
//	کد تولید کننده
void main(){

	pthread_t thread_id;
	void *thread_result;

	printf("how many numbers do you want to display in their binary formats:");
	scanf("%d",&counter);

	if(sem_init(&mutex,0,1)==0)printf("'mutex' semaphore created\n");
	if(sem_init(&empty,0,1)==0)printf("'empty' semaphore created\n");
	if(sem_init(&full,0,0)==0)printf("'full' semaphore created\n");

	pthread_create(&thread_id,NULL,ConvertToBinary,NULL);
	int i=0;
	for(i=0;i<counter;i++)
	{
		printf("thread 1:: producing a random number (try to acquire semaphore)....\n\n");
		sleep(1);
		srand(time(NULL));
		sem_wait(&empty);
		sem_wait(&mutex);
		sharedVar=rand()%20+1;
		printf("thread 1::(semaphores acquired) rand number is: %d\n",sharedVar);
		sem_post(&mutex);
		sem_post(&full);
	}
	printf("nothing to do with semaphore...\n");
	sem_destroy(&mutex);
	sem_destroy(&full);
	sem_destroy(&empty);
	printf("we destroyed all semaphores");
	exit(1);
}
  • نمونه اجرای برنامه
وب سایت توسینسو

 پیاده سازی مسئله کلاسیک نویسنده - خوانندگان

در زیر سورس مسئله نویسنده - خوانندگان آورده شده است. در این برنامه خوانندگان دارای اولویت هستند؛ یعنی تا زمانی که خواننده ای مشغول خواندن از متغیر مشترک است، نویسنده اجازه تغییر در متغیر مشترک را نخواهد داشت. در این برنامه تعداد خوانندگان سه عدد است. برای پیاده سازی این مسئله از قفل های mutex استفاده شده است.

#include "stdlib.h"
#include "stdio.h"
#include "unistd.h"
#include "string.h"
#include "pthread.h"
#include "semaphore.h"

char sharedVar[1024];
pthread_mutex_t rw_mutex;
pthread_mutex_t mutex;
int read_count=0;
//	کد خواننده
void *ReaderFunc(void *args)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		read_count++;
		if(read_count==1)
			pthread_mutex_lock(&rw_mutex);
		pthread_mutex_unlock(&mutex);
		printf("%s:: shared variable content==> %s\n",(char*)args,sharedVar);

		pthread_mutex_lock(&mutex);
		read_count--;
		if(read_count==0){
			pthread_mutex_unlock(&rw_mutex);
		}
		pthread_mutex_unlock(&mutex);
		sleep(1);
	}
}
/*==========================================================*/
//	کد نویسنده
void *WriterFunc(void *args)
{
	while(1)
	{
		printf("writer:: producing contents\n");
		int i=0;
		char bf[5];
		for(i=0;i<5;i++)
		{
			while(1)
			{
				sleep(1);
				srand(time(NULL));
				int rnd=rand()%91;
				if(rnd>64 && rnd<91)
				{
					bf[i]=rnd;
					break;	
				}
			}
		}
		printf("writer:: try to acquire lock...\n");
		pthread_mutex_lock(&rw_mutex);
		strcpy(sharedVar,bf);
		printf("writer:: shared variable is edited....\n");
		pthread_mutex_unlock(&rw_mutex);
	}
}
/*==========================================================*/

void main(){

	void *thread_result;
	pthread_t thread_id[3];

	if(pthread_mutex_init(&rw_mutex,NULL)==0)printf("'rw_mutex' has been initialized\n");
	if(pthread_mutex_init(&mutex,NULL)==0)printf("'mutex' has been initialized\n");

	sprintf(sharedVar,"%s","reader_writer problem");
	printf("'shared variable' has been initialized to '%s'\n",sharedVar);

	pthread_create(&thread_id[0],NULL,WriterFunc,NULL);

	pthread_create(&thread_id[1],NULL,ReaderFunc,"reader1");
	pthread_create(&thread_id[2],NULL,ReaderFunc,"reader2");
	pthread_create(&thread_id[3],NULL,ReaderFunc,"reader3");

	pthread_join(thread_id[0],&thread_result);
}
  • نمونه اجرای برنامه
وب سایت توسینسو


نظرات