الخيط (Thread) هو نوع معين أو خاص من العمليات، حيث ينشئ نظام التشغيل حيز عناوين جديدًا عند إنشاء عملية، ويتضمن هذا الحيز جزء الشيفرة أو نص البرنامج (text segment) والجزء الساكن (static segment) وجزء الكومة (heap)، وينشئ نظام التشغيل أيضًا خيط تنفيذ (thread of execution) جديدًا يتضمن عداد البرنامج (program counter) وحالة عتاد أخرى واستدعاء المكدس. العمليات التي رأيتها لحد الآن هي عمليات ذات خيط وحيد (single-threaded) أي يوجد خيط تنفيذ واحد فقط يعمل في كل حيز عناوين، وستتعرف على العمليات ذات الخيوط المتعددة (multi-threaded)، أي التي تملك خيوطًا متعددة تعمل في نفس حيز العناوين. تتشارك كل الخيوط بنفس جزء الشيفرة ضمن العملية الواحدة أي أنها تشغّل نفس الشيفرة، ولكن تشغّل هذه الخيوط المختلفة أجزاءً مختلفة من تلك الشيفرة، وتتشارك الخيوط ضمن العملية الواحدة بنفس الجزء الساكن (static segment)، لذلك إذا غيّر أحد الخيوط متغيرًا عامًا (global variable) فإن بقية الخيوط ترى هذا التغيير، ويتشاركون أيضًا بالكومة (heap) لذلك تستطيع الخيوط التشارك بقطع الذاكرة المخصصة ديناميكيًا (dynamically-allocated chunks)، ولكن يكون لكل خيطٍ جزء المكدس الخاص به لذلك تستطيع الخيوط استدعاء دوالٍ دون التداخل مع بعضها البعض، ولا تصل الخيوط عادةً إلى المتغيرات المحلية لخيطٍ آخر، حيث لا تستطيع الوصول إليها في بعض الأحيان.
إنشاء الخيوط (Creating threads)
الخيوط القياسية الأكثر شيوعًا والمستخدمة مع C هي خيوط POSIX أو اختصارًا Pthreads. تعرّف خيوط POSIX القياسية نموذج خيط (thread model) وواجهةًَ (interface) لإنشاء الخيوط والتحكم بها، وتوفّر معظم نسخ UNIX تطبيقًا ل Pthreads. يشبه استخدامُ Pthreads استخدامَ معظم مكتبات لغة C حيث:
- تضمّن ملفات الترويسات (headers files) في بداية برنامجك.
- تكتب الشيفرة التي تستدعي دوالًا معرّفة باستخدام Pthreads.
- تربط (link) البرنامج عند تصريفه (compile) مع مكتبة Pthread.
يضمِّن البرنامج ملفات الترويسات التالية:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h>
أول اثنين من ملفات الترويسات السابقة هما تضمين لمكتبات قياسية، أما ملف الترويسات الثالث فيُستخدم من أجل Pthreads، ويُستخدم ملف الترويسات الرابع من أجل متغيرات تقييد الوصول (semaphores). يمكنك استخدام الخيار -l
في سطر الأوامر لتصريف البرنامج مع مكتبة Pthread باستخدام الأداة gcc
كما يلي:
gcc -g -O2 -o array array.c -lpthread
يصرّف الأمر السابق ملفًا مصدريًا يدعى array.c
مع معلومات تنقيح الأخطاء (debugging info) والتحسين (optimization) ويربطه مع مكتبة Pthread ثم يولّد ملفًا تنفيذيًا يدعى array
.
إنشاء الخيوط (Creating threads)
تدعى دالة Pthread التي تنشئ خيوطًا pthread_create
، وتُظهر الدالة التالية كيفية استخدامها:
pthread_t make_thread(void *(*entry)(void *), Shared *shared) { int n; pthread_t thread; n = pthread_create(&thread, NULL, entry, (void *)shared); if (n != 0) { perror("pthread_create failed"); exit(-1); } return thread; }
الدالة make_thread
هي دالة مغلّفة (wrapper) وكُتبت لجعل الدالة pthread_create
سهلة الاستخدام ولتوفير التحقق من الأخطاء (error-checking). نوع القيمة المعادة من الدالة pthread_create
هو pthread_t
والذي يمكنك التفكير به كمعرّف (id) أو مِقبض (handle) للخيط الجديد. إذا نجح تنفيذ الدالة pthread_create
فستعيد القيمة 0 وتعيد الدالة make_thread
مقبض الخيط الجديد، وإذا ظهر خطأ فتعيد الدالة pthread_create
شيفرة الخطأ وتطبع الدالة make_thread
رسالة خطأ وتنتهي. Shared
المعامل الثاني للدالة make_thread
هو عبارة عن بنية (structure) عُرِّفت لتتضمن القيم المشتركة بين الخيوط، حيث يمكنك تعريف نوع جديد من خلال استخدام عبارة typedef
كما يلي:
typedef struct { int counter; } Shared;
والمتغير المشترك الوحيد في هذه الحالة هو counter
، وتخصص الدالة make_shared
حيّزًا للبنية Shared
وتهيئ محتوياتها كما يلي:
Shared *make_shared() { Shared *shared = check_malloc(sizeof(Shared)); shared->counter = 0; return shared; }
لديك الآن بنية بيانات مشتركة وإذا عدتَ إلى الدالة make_thread
وتحديدًا المعامل الأول الذي هو عبارة عن مؤشر (pointer) إلى دالة، وتأخذ هذه الدالة مؤشر void
وتعيد مؤشر void
أيضًا. إذا أصبح نظرك مشوشًا بسبب صيغة تصريح هذا النوع فلست الوحيد في ذلك، على كل حال إن الهدف الأساسي من هذا المعامل هو أن يحدد للدالة مكان بدء تنفيذ الخيط الجديد، وتدعى هذه الدالة entry
:
void *entry(void *arg) { Shared *shared = (Shared *)arg; child_code(shared); pthread_exit(NULL); }
يجب أن يُصرَّح عن معامل الدالة entry
كمؤشر void
، ولكنه في هذا البرنامج مؤشرٌ إلى بنية Shared
لذلك يمكن تبديل نوعه (typecast) ثم تمريره إلى الدالة child_code
التي تقوم بالعمل الحقيقي، حيث تطبع الدالة child_code
قيمة المتغير المشترك counter
ثم تزيد قيمته كما يلي:
void child_code(Shared *shared) { printf("counter = %d\n", shared->counter); shared->counter++; }
تستدعي الدالةُ entry
الدالةََ pthread_exit
بعد أن تنتهي الدالة child code
وتعيد قيمةً، حيث يمكن أن تُستخدم الدالة pthread_exit
لتمرير قيمة إلى الخيط الذي يُضم (join) مع الخيط الحالي، وبالتالي في هذه الحالة لا يبقى شيء للخيط الابن لعمله فتُمرَّر القيمة الخالية NULL
، وأخيرًا تنشئ الشيفرة التالية الخيوط الأبناء (child threads) كما يلي:
int i; pthread_t child[NUM_CHILDREN]; Shared *shared = make_shared(1000000); for (i = 0; i < NUM_CHILDREN; i++) { child[i] = make_thread(entry, shared); }
NUM_CHILDREN
هو ثابت وقت التصريف (compile-time constant) الذي يحدد عدد الخيوط الأبناء، و child
هي مصفوفة مقابض الخيوط (thread handles).
ضم الخيوط (Joining threads)
إذا أراد خيطٌ انتظار خيطٍ آخر ليكتمل فإنه يستدعي الدالة pthread_join
، وتجد فيما يلي الدالة المغلّفة للدالة pthread_join
:
void join_thread(pthread_t thread) { int ret = pthread_join(thread, NULL); if (ret == -1) { perror("pthread_join failed"); exit(-1); } }
معامل الدالة المغلّفة هو مقبض الخيط الذي تنتظره ليكتمل، وعمل الدالة المغلّفة هو فقط استدعاء الدالة pthread_join
والتحقق من النتيجة. يستطيع أي خيط أن يضم أي خيطٍ آخر، ولكن في النماذج الأكثر شيوعًا ينشئ الخيط الأب (parent thread) كل الخيوط الأبناء ويضمها (join). تجد فيما يلي الشيفرة التي تنتظر الخيوط الأبناء بها:
for (i = 0; i < NUM_CHILDREN; i++) { join_thread(child[i]); }
تنتظر هذه الحلقات أحد الخيوط الأبناء في كل مرة وذلك حسب ترتيب إنشائها، ولا يوجد ضمان أن تكتمل الخيوط الأبناء في هذا الترتيب ولكن تعمل هذه الحلقة بصورة صحيحة حتى في حال لم يحدث ذلك، فإذا تأخر أحد الخيوط الأبناء فيجب أن تنتظر الحلقة، ويمكن أن تكتمل الخيوط الأبناء الأخرى خلال وقت الانتظار هذا، حيث لا يمكن أن تنتهي هذه الحلقة إلا في حال اكتمال جميع الخيوط الأبناء. يمكنك الاطلاع على المثال ضمن counter/counter.c ثم تصريفه وتشغيله كما يلي:
$ make counter gcc -Wall counter.c -o counter -lpthread $ ./counter
فعند تشغيله مع 5 خيوط أبناء سينتج الخرج التالي:
counter = 0 counter = 0 counter = 1 counter = 0 counter = 3
وسينتج خرج آخر عندما تشغله على حاسوبك، وإذا شغلته مرة أخرى سينتج خرج مختلف أيضًا، فماذا يحدث؟
الأخطاء المتزامنة (Synchronization errors)
مشكلة البرنامج السابق أن الخيوط الأبناء تستطيع الوصول إلى المتغير المشترك counter
بدون تزامن، لذلك تستطيع عدة خيوط قراءة نفس قيمة المتغير counter
قبل أن يزيد أي خيطٍ قيمته. يمكن أن تشرح سلسلة الأحداث التالية الخرج الذي حصلت عليه سابقًا:
Child A reads 0 Child B reads 0 Child C reads 0 Child A prints 0 Child B prints 0 Child A sets counter=1 Child D reads 1 Child D prints 1 Child C prints 0 Child A sets counter=1 Child B sets counter=2 Child C sets counter=3 Child E reads 3 Child E prints 3 Child D sets counter=4 Child E sets counter=5
يمكن أن تُقاطَع الخيوط في أماكن مختلفة في كل مرة تشغّل فيها البرنامج، أو قد يختار المجدول خيوطًا مختلفة ليشغّلها، لذلك ستكون سلسلة الأحداث والنتائج مختلفة. افترض أنك تريد فرض بعض الترتيب، أي مثلًا تريد أن يقرأ كل خيط قيمةً مختلفة للمتغير counter
ثم يزيدها، وبالتالي تُظهر قيمة المتغير counter
عدد الخيوط التي نفّذت الدالة child_code
، ويمكنك استخدام كائن المزامنة (mutex) لتطبيق ذلك، حيث كائن المزامنة (mutex) هو عبارة عن كائن (object) يضمن حدوث إقصاء متبادل (mutual exclusion) لكتلة من الشيفرة، أي ينفّذ خيطٌ واحد فقط كتلة الشيفرة في نفس الوقت. كتبتُ، يقول الكاتب، نموذجًا يدعى mutex.c
يوفر كائنات المزامنة، ستجد فيما يلي نسخةً من الدالة child_code
التي تستخدم كائن المزامنة لتأمين تزامن الخيوط:
void child_code(Shared *shared) { mutex_lock(shared->mutex); printf("counter = %d\n", shared->counter); shared->counter++; mutex_unlock(shared->mutex); }
حيث يجب على كل خيط أن يقفل (lock) كائن المزامنة قبل أن يصل أي خيطٍ آخر إلى المتغير المشترك counter
، وهذا يؤدي إلى حظر كل الخيوط الأخرى من الوصول إلى هذا المتغير. افترض أن الخيط A قفل كائن المزامنة وهو في منتصف الدالة child_code
، فإذا وصل الخيط B ونفّذ الدالة mutex_lock
يتوقف تنفيذ الخيط B. ينفّذ الخيط A الدالة mutex_unlock
عندما ينتهي، وبالتالي يسمح للخيط B متابعة تنفيذه، أي تنفّذ الخيوط الدالة child_code
على التوالي بحيث ينفّذها خيطٌ واحدٌ فقط في نفس الوقت، وبالتالي لا يتعارض أي خيط مع الخيوط الأخرى، وإذا شغّلت الشيفرة مع 5 خيوط أبناء سينتج:
counter = 0 counter = 1 counter = 2 counter = 3 counter = 4
وهذا هو المطلوب. يجب إضافة كائن المزامنة Mutex إلى البنية Shared لكي يعمل هذا الحل بالصورة الصحيحة:
typedef struct { int counter; Mutex *mutex; } Shared;
وتهيئته في الدالة make_shared
:
Shared *make_shared(int end) { Shared *shared = check_malloc(sizeof(Shared)); shared->counter = 0; shared->mutex = make_mutex(); //-- هذا السطر جديد return shared; }
كائن المزامنة (Mutex)
تعريفي، يقول الكاتب، ل Mutex
هو مغلّف لنوعٍ يدعى pthread_mutex_t
وهو معرّفٌ في واجهة برمجة التطبيقات للخيوط POSIX، ولإنشاء كائن مزامنة POSIX يجب تخصيص حيزٍ للنوع pthread_mutex_t
ثم استدعاء الدالة pthread_mutex_init
. إحدى مشاكل واجهة برمجة التطبيقات هذه أن النوع pthread_mutex_t
يتصرف كبنية (structure)، لذلك إذا مررته كوسيط سينشئ نسخةً تجعل كائن المزامنة يتصرف بصورة غير صحيحة، ويمكنك تجنب ذلك من خلال تمرير النوع pthread_mutex_t
باستخدام عنوانه. تجعل الشيفرة التي كتبتها، يقول الكاتب، الأمور أسهل من خلال تعريف نوعٍ هو النوع Mutex
الذي هو عبارة عن اسم للنوع pthread_mutex_t
يمكن قراءته بطريقة أسهل:
#include <pthread.h> typedef pthread_mutex_t Mutex;
ثم تعريف دالةٍ هي الدالة make_mutex
التي تخصص حيّزًا لكائن المزامنة وتهيئته:
Mutex *make_mutex() { Mutex *mutex = check_malloc(sizeof(Mutex)); int n = pthread_mutex_init(mutex, NULL); if (n != 0) perror_exit("make_lock failed"); return mutex; }
القيمة المعادة هي مؤشر يمكن أن تمرره كوسيط دون أن يسبب نسخًا غير مرغوبة. الدوال التي تستخدم لقفل وفك قفل كائن المزامنة هي دوالٌ مغلّفة بسيطة لدوال POSIX:
void mutex_lock(Mutex *mutex) { int n = pthread_mutex_lock(mutex); if (n != 0) perror_exit("lock failed"); } void mutex_unlock(Mutex *mutex) { int n = pthread_mutex_unlock(mutex); if (n != 0) perror_exit("unlock failed"); }
ترجمة -وبتصرّف- للفصل Threads من كتاب Think OS A Brief Introduction to Operating Systems
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.