در توسینسو تدریس کنید

و

با دانش خود درآمد کسب کنید

کار با threadها در لینوکس - قسمت سوم

4. همگام سازی


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

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


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


برای کار کردن با سمافورها دو مجموعه جداگانه از توابع وجود دارد: یکی از آن ها برگرفته شده از 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 باشد، هر کدام از نتایج زیر ( و همچنین نتایج دیگر) می توانند رخ دهند:

 کار با threadها در لینوکس - قسمت سوم

 کار با threadها در لینوکس - قسمت سوم

 کار با threadها در لینوکس - قسمت سوم

همانطور که مشاهده می کنید در دو مورد مقدار نهایی 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 را برمی گردانند.


4.2. همگام سازی به وسیله 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 از رفتار پیش فرض استفاده کند.


5. لغو یک نخ (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.


نویسنده : رامین غلامی تقی زاده

منبع : انجمن تخصصی فناوری اطلاعات ایران

هرگونه نشر و کپی برداری بدون ذکر منبع و نام نویسنده دارای اشکال اخلاقی می باشد

#قفل_های_mutex #مسائل_کلاسیک_همگام_سازی #سمافورها
عنوان
1 کار با threadها در لینوکس - قسمت اول رایگان
2 کار با threadها در لینوکس - قسمت دوم رایگان
3 کار با threadها در لینوکس - قسمت سوم رایگان
4 کار با threadها در لینوکس - قسمت چهارم رایگان
زمان و قیمت کل 0″ 0
0 نظر

هیچ نظری ارسال نشده است! اولین نظر برای این مطلب را شما ارسال کنید...

نظر شما
برای ارسال نظر باید وارد شوید.
از سرتاسر توسینسو
تنظیمات حریم خصوصی
تائید صرفنظر
×

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