7. خيوط التنفيذ
7.1. مقدمة
عند تشغيل تطبيق ما، يتم تنفيذه في تدفق تنفيذ يُسمى مؤشر ترابط. الفئة التي تمثل thread هي الفئة java.lang.Thread، وفيما يلي بعض خصائصها وأساليبها:
يعطي الخيط قيد التنفيذ حاليًا | |
تحدد اسم الخيط | |
اسم الخيط | |
يحدد ما إذا كان الخيط نشطًا (صحيح) أم لا (خطأ) | |
يبدأ تشغيل مؤشر ترابط | |
طريقة يتم تنفيذها تلقائيًا بعد تنفيذ الطريقة start السابقة | |
توقف تنفيذ مؤشر ترابط لمدة n مللي ثانية | |
عملية معطلة - تنتظر انتهاء الخيط للانتقال إلى التعليمات التالية |
المنشئات الأكثر استخدامًا هي التالية:
يُنشئ مرجعًا لمهمة غير متزامنة. هذه المهمة لا تزال غير نشطة. يجب أن تحتوي المهمة التي تم إنشاؤها على طريقة run: غالبًا ما يتم استخدام فئة مشتقة من Thread. | |
نفس الشيء، لكن الكائن Runnable الذي تم تمريره كمعلمة هو الذي ينفذ الطريقة run. |
لنلقِ نظرة على تطبيق أول يسلط الضوء على وجود مؤشر ترابط رئيسي للتنفيذ، وهو المؤشر الذي يتم فيه تنفيذ الدالة main لفئة ما:
// استخدام الخيوط
import java.io.*;
import java.util.*;
public class thread1{
public static void main(String[] arg)throws Exception {
// تهيئة مؤشر الترابط الحالي
Thread main=Thread.currentThread();
// العرض
System.out.println("Thread courant : " + main.getName());
// تغيير الاسم
main.setName("myMainThread");
// التحقق
System.out.println("Thread courant : " + main.getName());
// حلقة لا نهائية
while(true){
// استرداد الوقت
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// العرض
System.out.println(main.getName() + " : " +H);
// إيقاف مؤقت
Thread.sleep(1000);
}//while
}//main
}//فئة
نتائج الشاشة:
Thread courant : main
Thread courant : myMainThread
myMainThread : 15:34:9
myMainThread : 15:34:10
myMainThread : 15:34:11
myMainThread : 15:34:12
Terminer le programme de commandes (O/N) ? o
يوضح المثال السابق النقاط التالية:
- تُنفَّذ الدالة main بشكل جيد في مؤشر ترابط
- يمكن الوصول إلى خصائص هذا الخيط من خلال Thread.currentThread()
- دور الطريقة sleep. هنا، يدخل مؤشر الترابط الذي ينفذ main في حالة سكون بانتظام لمدة ثانية واحدة بين عرضين.
7.2. إنشاء خيوط التنفيذ
من الممكن أن تكون هناك تطبيقات يتم فيها تنفيذ أجزاء من الكود "بشكل متزامن" في خيوط تنفيذ مختلفة. عندما نقول أن threads يتم تنفيذها بشكل متزامن، فإننا غالبًا ما نستخدم مصطلحًا غير دقيق. إذا كان الجهاز يحتوي على معالج واحد فقط، كما هو الحال في كثير من الأحيان، فإن threads تتشارك هذا المعالج: فهي تتناوب عليه، كل واحدة على حدة، لفترة قصيرة (بضع ميلي ثوانٍ). وهذا ما يعطي انطباعًا زائفًا بالتوازي في التنفيذ. يعتمد الوقت المخصص لـ thread على عوامل مختلفة، منها أولويته التي لها قيمة افتراضية ولكن يمكن تحديدها أيضًا عن طريق البرمجة. عندما يتوفر المعالج لـ thread، فإنه يستخدمه عادةً طوال الوقت المخصص له. ومع ذلك، يمكنه تحريره قبل انتهاء المدة:
- بالانتظار لحدث ما (wait, join)
- بالانتقال إلى وضع السكون لفترة محددة (sleep)
- يمكن إنشاء مؤشر ترابط T بعدة طرق
- عن طريق اشتقاق الفئة Thread وإعادة تعريف الطريقة run الخاصة بها.
- عن طريق تنفيذ واجهة Runnable في فئة واستخدام منشئ new Thread(Runnable). Runnable هي واجهة لا تحدد سوى طريقة واحدة: public void run(). وبالتالي، فإن وسيطة المنشئ السابق هي أي مثيل لفئة تنفذ هذه الطريقة run.
في المثال التالي، يتم إنشاء الخيوط باستخدام فئة مجهولة مشتقة من فئة Thread:
// يتم إنشاء مؤشر الترابط i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//تعريف المهام[i]
تكتفي الطريقة run هنا بإحالة إلى الطريقة affiche.
- يتم تشغيل مؤشر الترابط T بواسطة T.start(): تنتمي هذه الطريقة إلى فئة Thread وتقوم بعدد من عمليات التهيئة ثم تطلق تلقائيًا الطريقة run الخاصة بـ Thread أو الواجهة Runnable. لا ينتظر البرنامج الذي ينفذ الأمر T.start() انتهاء المهمة T: بل ينتقل فورًا إلى الأمر التالي. وبذلك يكون لدينا مهمتان يتم تنفيذهما بالتوازي. وغالبًا ما يتعين عليهما التواصل مع بعضهما البعض لمعرفة مدى تقدم العمل المشترك المطلوب إنجازه. وهذه هي مشكلة تزامن الخيوط.
- بمجرد بدء تشغيله، يتم تنفيذ thread بشكل مستقل. وسيتوقف عندما تنتهي الوظيفة run التي ينفذها من عملها.
- يمكننا انتظار انتهاء تنفيذ الخيط T بواسطة T.join(). لدينا هنا تعليمات حظر: يتم حظر البرنامج الذي ينفذها حتى تنتهي المهمة T من عملها. هذه أيضًا طريقة للتزامن.
دعونا ندرس البرنامج التالي:
// استخدام الخيوط
import java.io.*;
import java.util.*;
public class thread2{
public static void main(String[] arg) {
// تهيئة الخيط الحالي
Thread main=Thread.currentThread();
// تسمية الخيط الحالي
main.setName("myMainThread");
// بداية الدالة
System.out.println("début du thread " +main.getName());
// إنشاء خيوط التنفيذ
Thread[] tâches=new Thread[5];
for(int i=0;i<tâches.length;i++){
// إنشاء الخيط i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//مهام [i]
// تحديد اسم الخيط
tâches[i].setName(""+i);
// يتم تشغيل مؤشر الترابط i
tâches[i].start();
}//for
// نهاية main
System.out.println("fin du thread " +main.getName());
}//Main
public static void affiche() {
// استرداد الوقت
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// عرض بداية التنفيذ
System.out.println("Début d'exécution de la méthode affiche dans le Thread " +
Thread.currentThread().getName()+ " : " + H);
// وضع السكون لمدة 1 ثانية
try{
Thread.sleep(1000);
}catch (Exception ex){}
// استرداد الوقت
calendrier=Calendar.getInstance();
H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// عرض نهاية التنفيذ
System.out.println("Fin d'exécution de la méthode affiche dans le Thread "
+Thread.currentThread().getName()+ " : " + H);
}// عرض
}//الفئة
يُنشئ الخيط الرئيسي، وهو الذي يُنفذ الدالة main، 5 خيوط أخرى مكلفة بتنفيذ الطريقة الثابتة affiche. والنتائج هي كما يلي:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 15:48:3
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 1 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 2 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 3 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 4 : 15:48:3
Fin d'exécution de la méthode affiche dans le Thread 0 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 1 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 2 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 3 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 4 : 15:48:4
هذه النتائج مفيدة للغاية:
- نرى أولاً أن بدء تنفيذ مؤشر ترابط لا يؤدي إلى توقف العملية. فقد بدأت الطريقة main تنفيذ 5 مؤشرات ترابط بالتوازي وأنهت تنفيذها قبلها. العملية
تشغيل مؤشر الترابط tâches[i] ولكن بمجرد القيام بذلك، يستمر التنفيذ فورًا مع التعليمات التالية دون انتظار انتهاء تنفيذ مؤشر الترابط.
- يجب أن تنفذ جميع الخيوط التي تم إنشاؤها الطريقة affiche. ترتيب التنفيذ غير متوقع. حتى لو بدا في المثال أن ترتيب التنفيذ يتبع ترتيب بدء تشغيل الخيوط، فلا يمكن استخلاص قواعد عامة من ذلك. يحتوي نظام التشغيل هنا على 6 خيوط ومعالج واحد. وسيقوم بتوزيع المعالج على هذه الخيوط الست وفقًا لقواعده الخاصة.
- نرى في النتائج نتيجة للطريقة sleep. في المثال، الخيط 0 هو الذي ينفذ أولاً الطريقة affiche. يتم عرض رسالة بدء التنفيذ ثم يقوم بتنفيذ الطريقة sleep التي تعلقه لمدة ثانية واحدة. عندها يفقد المعالج الذي يصبح متاحًا لخيط آخر. يوضح المثال أن الخيط 1 هو الذي سيحصل عليه. سيتبع الخيط 1 نفس المسار وكذلك الخيوط الأخرى. عندما تنتهي ثانية السكون للخيط 0، يمكن أن يستأنف تنفيذه. يمنحه النظام المعالج ويمكنه إنهاء تنفيذ الطريقة affiche.
لنعدل برنامجنا لإنهاء الطريقة main باستخدام التعليمات:
// نهاية main
System.out.println("fin du thread " +main.getName());
// إيقاف التطبيق
System.exit(0);
يؤدي تنفيذ البرنامج الجديد إلى:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 1 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 2 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 3 : 16:5:45
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 4 : 16:5:45
بمجرد أن تقوم الطريقة main بتنفيذ الأمر:
تقوم بإيقاف جميع مؤشرات الترابط الخاصة بالتطبيق وليس مؤشر الترابط main فقط. قد ترغب الطريقة main في انتظار انتهاء تنفيذ الخيوط التي أنشأتها قبل أن تنتهي هي نفسها. يمكن القيام بذلك باستخدام الطريقة join من الفئة Thread:
// في انتظار جميع الخيوط
for(int i=0;i<tâches.length;i++){
// في انتظار الخيط i
tâches[i].join();
}//for
// نهاية الدالة
System.out.println("fin du thread " +main.getName());
// إيقاف التطبيق
System.exit(0);
ونحصل عندئذ على النتائج التالية:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 1 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 2 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 3 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 4 : 16:11:9
Fin d'exécution de la méthode affiche dans le Thread 0 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 1 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 2 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 3 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 4 : 16:11:10
fin du thread myMainThread
7.3. أهمية الخيوط
الآن بعد أن أوضحنا وجود مؤشر ترابط افتراضي، وهو الذي ينفذ الطريقة Main، وبعد أن عرفنا كيفية إنشاء مؤشرات ترابط أخرى، دعونا نتوقف عند فائدة مؤشرات الترابط بالنسبة لنا والسبب الذي دفعنا إلى عرضها هنا. هناك نوع من التطبيقات التي تتناسب جيدًا مع استخدام الخيوط، وهي تطبيقات العميل-الخادم على الإنترنت. في مثل هذا التطبيق، يستجيب خادم موجود على جهاز S1 لطلبات العملاء الموجودين على أجهزة بعيدة C1، C2، ...، Cn.

نستخدم يوميًا تطبيقات الإنترنت التي تتوافق مع هذا المخطط: خدمات الويب، والبريد الإلكتروني، وتصفح المنتديات، ونقل الملفات... في المخطط أعلاه، يجب أن يخدم الخادم S1 العملاء Ci في وقت واحد. إذا أخذنا مثالاً على خادم FTP (بروتوكول نقل الملفات) الذي يزود عملائه بالملفات، فإننا نعلم أن نقل الملفات قد يستغرق أحياناً عدة ساعات. وبالطبع، من المستحيل أن يحتكر عميل واحد الخادم بمفرده لمدة طويلة كهذه. ما يحدث عادةً هو أن الخادم ينشئ عددًا من مؤشرات الترابط (threads) يساوي عدد العملاء. ثم يتولى كل مؤشر ترابط مسؤولية التعامل مع عميل معين. ونظرًا لأن المعالج يتم تقاسمه بشكل دوري بين جميع مؤشرات الترابط النشطة على الجهاز، فإن الخادم يقضي بعض الوقت مع كل عميل، مما يضمن توفير الخدمة بشكل متزامن.

7.4. ساعة رسومية
لننظر إلى التطبيق التالي الذي يعرض نافذة تحتوي على ساعة وزر لإيقاف الساعة أو إعادة تشغيلها:
![]() | ![]() | ![]() |
لكي تعمل الساعة، يجب أن تتولى إحدى العمليات تغيير الوقت كل ثانية. وفي الوقت نفسه، يجب مراقبة الأحداث التي تحدث في النافذة: عندما ينقر المستخدم على زر "إيقاف"، يجب إيقاف الساعة. لدينا هنا مهمتان متوازيتان وغير متزامنتان: يمكن للمستخدم النقر في أي وقت.
لننظر إلى اللحظة التي لم يتم فيها تشغيل الساعة بعد ويضغط المستخدم على زر "تشغيل". لدينا هنا حدث كلاسيكي وقد نعتقد أن إحدى طرق الخيط الذي يتم فيه تشغيل النافذة يمكنها عندئذ إدارة الساعة. ولكن عندما يتم تنفيذ إحدى طرق التطبيق الرسومي، لا يكون مؤشر الترابط الخاص به مستمعًا لأحداث واجهة المستخدم الرسومية. تحدث هذه الأحداث ويتم وضعها في قائمة انتظار ليتم معالجتها بواسطة التطبيق عند انتهاء الطريقة قيد التنفيذ حاليًا. في مثالنا الخاص بالساعة، ستظل الطريقة قيد التنفيذ لأن النقر على زر "إيقاف" هو وحده الذي يمكنه إيقافها. لكن هذا الحدث لن تتم معالجته إلا عند انتهاء الطريقة. نحن ندور في حلقة مفرغة.
الحل لهذه المشكلة هو أنه عندما ينقر المستخدم على زر "تشغيل"، يتم تشغيل مهمة لإدارة الساعة، ولكن يظل التطبيق قادراً على الاستماع إلى الأحداث التي تحدث في النافذة. وبذلك يكون لدينا مهمتان منفصلتان يتم تنفيذهما بالتوازي:
- إدارة الساعة
- الاستماع إلى أحداث النافذة
لنعد إلى ساعتنا الرسومية:
![]() |
|
الرمز المفيد للتطبيق الذي تم إنشاؤه باستخدام JBuilder هو التالي:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
public class interfaceHorloge extends JFrame {
JPanel contentPane;
JTextField txtHorloge = new JTextField();
JButton btnGoStop = new JButton();
// سمات المثيل
boolean finHorloge=true;
//بناء الإطار
public interfaceHorloge() {
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
private void runHorloge(){
// نستمر في التكرار حتى يُطلب منا التوقف
while( ! finHorloge){
// نسترد الوقت
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// نعرضها في الحقل T
txtHorloge.setText(H);
// الانتظار لمدة ثانية
try{
Thread.sleep(1000);
} catch (Exception e){
// إخراج مع خطأ
System.exit(1);
}//محاولة
}// بينما
}// runHorloge
//تهيئة المكون
private void jbInit() throws Exception {
...................
}
//تم استبداله، وبالتالي يمكننا الخروج عند إغلاق النافذة
protected void processWindowEvent(WindowEvent e) {
.............
}
void btnGoStop_actionPerformed(ActionEvent e) {
// تشغيل/إيقاف الساعة
// نسترد تسمية الزر
String libellé=btnGoStop.getText();
// تشغيل؟
if(libellé.equals("Lancer")){
// يتم إنشاء مؤشر الترابط الذي سيتم فيه تشغيل الساعة
Thread thHorloge=new Thread(){
public void run(){
runHorloge();
}
};//تعريف الخيط
// السماح لخيط التشغيل بالبدء
finHorloge=false;
// تغيير نص الزر
btnGoStop.setText("Arrêter");
// يتم تشغيل الخيط
thHorloge.start();
// نهاية
return;
}//if
// إيقاف
if(libellé.equals("Arrêter")){
// يُطلب من الخيط التوقف
finHorloge=true;
// يتم تغيير نص الزر
btnGoStop.setText("Lancer");
// نهاية
return;
}//if
}
}
عندما ينقر المستخدم على زر "تشغيل"، يتم إنشاء مؤشر ترابط باستخدام فئة مجهولة:
تقوم طريقة run الخاصة بالخيط بالإحالة إلى طريقة runHorloge الخاصة بالتطبيق. وبذلك، يتم تشغيل الخيط:
ثم يتم تنفيذ الطريقة runHorloge:
private void runHorloge(){
// يتم التكرار حتى يتم إيقافنا
while( ! finHorloge){
// نسترد الوقت
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// نعرضها في الحقل T
txtHorloge.setText(H);
// الانتظار لمدة ثانية
try{
Thread.sleep(1000);
} catch (Exception e){
// تم الإخراج مع وجود خطأ
System.exit(1);
}//محاولة
}// بينما
}// runHorloge
مبدأ هذه الطريقة هو كما يلي:
- يعرض الوقت الحالي في مربع النص txtHorloge
- توقف لمدة ثانية واحدة
- يستأنف الخطوة 1 بعد التأكد مسبقًا من اختبار المتغير المنطقي finHorloge الذي سيتم تعيينه على "صحيح" عندما ينقر المستخدم على الزر Arrêter.
7.5. برنامج الساعة الصغير
نقوم بتحويل التطبيق الرسومي السابق إلى تطبيق صغير بالطريقة المعتادة وننشئ المستند HTML appletHorloge.htm التالي:
<html>
<head>
<title>Applet Horloge</title>
</head>
<body>
<h2>Une applet horloge</h2>
<applet
code="appletHorloge.class"
width="150"
height="130"
></applet>
</center>
</body>
</html>
عندما نقوم بتحميل هذا المستند مباشرة في IE بالنقر المزدوج عليه، نحصل على العرض التالي:

جميع العناصر اللازمة للتطبيق الصغير موجودة في هذا المثال في نفس المجلد:
E:\data\serge\Jbuilder\horloge\1>dir
13/06/2002 12:17 3 174 appletHorloge.class
13/06/2002 12:17 658 appletHorloge$1.class
13/06/2002 12:17 512 appletHorloge$2.class
13/06/2002 12:20 245 appletHorloge.htm
يمكن تحسين تطبيقنا الصغير. ذكرنا أنه عند تحميل التطبيق الصغير، يتم تنفيذ الطريقة init ثم الطريقة start إذا كانت موجودة. بالإضافة إلى ذلك، عند مغادرة المستخدم للصفحة، يتم تنفيذ الطريقة stop إذا كانت موجودة. وعند عودته إلى صفحة التطبيق الصغير، يتم استدعاء الطريقة start مرة أخرى. عندما تنفذ التطبيقات الصغيرة خيوط الرسوم المتحركة المرئية، غالبًا ما يتم استخدام الطرق start و stop الخاصة بالتطبيق الصغير لتشغيل وإيقاف الخيوط. فمن غير الضروري أن تستمر خيط الرسوم المتحركة المرئية في العمل في الخلفية بينما تكون الرسوم المتحركة مخفية.
لذلك نضيف إلى التطبيق الصغير الخاص بنا الطريقتين التاليتين start و stop:
public void stop(){
// الصفحة مخفية
// متابعة
System.out.println("Page stop");
// الصفحة مخفية - يتم إيقاف مؤشر الترابط
finHorloge=true;
}
public void start(){
// الصفحة تظهر مرة أخرى
// متابعة
System.out.println("Page start");
// يتم إعادة تشغيل مؤشر ترابط ساعة جديد إذا لزم الأمر
if(btnGoStop.getText().equals("Arrêter")){
// يتم تغيير العنوان
btnGoStop.setText("Lancer");
// ونتصرف كما لو أن المستخدم قد نقر عليها
btnGoStop_actionPerformed(null);
}//if
}//start
بالإضافة إلى ذلك، أضفنا تتبعًا في طريقة run للخيط لمعرفة متى يبدأ ويتوقف:
private void runHorloge(){
// متابعة
System.out.println("Thread horloge lancé");
// نكرر العملية حتى يُطلب منا التوقف
while( ! finHorloge){
// نسترد الوقت
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// نعرضها في الحقل T
txtHorloge.setText(H);
// ننتظر لمدة ثانية
try{
Thread.sleep(1000);
} catch (Exception e){
// إخراج مع خطأ
return;
}//محاولة
}// بينما
// متابعة
System.out.println("Thread horloge terminé");
}// runHorloge
الآن نقوم بتشغيل التطبيق الصغير باستخدام AppletViewer:
E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start // تم تشغيل التطبيق الصغير - تم عرض الصفحة
Thread horloge lancé // تم تشغيل مؤشر الترابط وفقًا لذلك
Page stop // تم تحويل التطبيق الصغير إلى أيقونة
Thread horloge terminé // تم إيقاف مؤشر الترابط وفقًا لذلك
Page start // إعادة عرض التطبيق الصغير
Thread horloge lancé // تم إعادة تشغيل الخيط
Thread horloge terminé // الضغط على زر الإيقاف
Thread horloge lancé // الضغط على زر التشغيل
Page stop // التطبيق المصغر في شكل أيقونة e
Thread horloge terminé // تم إيقاف مؤشر الترابط نتيجة لذلك
Page start // إعادة عرض التطبيق الصغير
Thread horloge lancé // إعادة تشغيل الخيط وفقًا لذلك
مع AppletViewer، يحدث الحدث start عندما تكون نافذة AppletViewer مرئية، ويحدث الحدث stop عند تصغيرها إلى أيقونة. تُظهر النتائج أعلاه أنه عند إخفاء المستند HTML، يتم إيقاف مؤشر الترابط بشكل صحيح إذا كان نشطًا.
7.6. مزامنة المهام
في المثال السابق، كانت هناك مهمتان:
- المهمة الرئيسية التي يمثلها التطبيق نفسه
- المهمة المسؤولة عن الساعة
كان التنسيق بين المهمتين يتم بواسطة المهمة الرئيسية التي كانت تضع قيمة منطقية لإيقاف مؤشر ترابط الساعة. نتناول الآن مشكلة الوصول المتزامن للمهام إلى الموارد المشتركة، وهي مشكلة تُعرف أيضًا باسم "مشاركة الموارد". لتوضيح ذلك، سنقوم أولاً بدراسة مثال.
7.6.1. عد غير متزامن
لننظر إلى واجهة المستخدم الرسومية التالية:

رقم | النوع | الاسم | الدور |
1 | JTextField | txtAGénérer | يشير إلى عدد الخيوط المطلوب إنشاؤها |
2 | JTextfield (غير قابل للتعديل) | txtGénéres | يشير إلى عدد الخيوط التي تم إنشاؤها |
3 | JTextField (غير قابل للتحرير) | txtStatus | يقدم معلومات عن الأخطاء التي تمت مواجهتها وعن التطبيق نفسه |
4 | JButton | btnGénérer | يبدأ إنشاء الخيوط |
طريقة عمل التطبيق هي كما يلي:
- يحدد المستخدم عدد الخيوط المراد إنشاؤها في الحقل 1
- يبدأ إنشاء هذه الخيوط باستخدام الزر 4
- تقوم الخيوط بقراءة قيمة الحقل 2، وزيادتها، وعرض القيمة الجديدة. في البداية، يحتوي هذا الحقل على القيمة 0.
تتشارك الخيوط التي تم إنشاؤها موردًا واحدًا: قيمة الحقل 2. نسعى هنا إلى إظهار المشكلات التي نواجهها في مثل هذه الحالة. فيما يلي مثال على التنفيذ:

نرى أنه تم طلب إنشاء 1000 مؤشر ترابط، ولكن تم حساب 7 مؤشرات فقط. فيما يلي الكود الفعلي للتطبيق:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class interfaceSynchro extends JFrame {
JPanel contentPane;
JLabel jLabel1 = new JLabel();
JTextField txtAGénérer = new JTextField();
JButton btnGénérer = new JButton();
JTextField txtStatus = new JTextField();
JTextField txtGénérés = new JTextField();
JLabel jLabel2 = new JLabel();
// متغيرات المثيل
Thread[] tâches=null; // الخيوط
int[] compteurs=null; // العدادات
//إنشاء الإطار
public interfaceSynchro() {
..........
}
//تهيئة المكون
private void jbInit() throws Exception {
......................
}
//تم استبداله، وبالتالي يمكننا الخروج عند إغلاق النافذة
protected void processWindowEvent(WindowEvent e) {
..................
}
void btnGénérer_actionPerformed(ActionEvent e) {
//إنشاء الخيوط
// قراءة عدد الخيوط المطلوب إنشاؤها
int nbThreads=0;
try{
// قراءة الحقل الذي يحتوي على عدد الخيوط
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// إيجابي >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//خطأ
txtStatus.setText("Nombre invalide");
// إعادة المحاولة
txtAGénérer.requestFocus();
return;
}//استثناء
// في البداية لم يتم إنشاء أي خيوط
txtGénérés.setText("0"); // عداد المهام عند 0
// يتم إنشاء الخيوط وتشغيلها
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// يتم إنشاء الخيط i
tâches[i]=new Thread() {
public void run() {
incrémente();
}
};//الخيط i
// يتم تحديد اسمه
tâches[i].setName(""+i);
// يتم تشغيله
tâches[i].start();
}//for
}//إنشاء
// زيادة
private void incrémente(){
// استرداد رقم الخيط
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// قراءة قيمة عداد المهام
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// يتم زيادته
compteurs[iThread]++;
// الانتظار لمدة 100 مللي ثانية - سيخسر الخيط عندئذ المعالج
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// نقوم بعرض العداد الجديد
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
// متابعة
System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
}// يتم الزيادة
}// فئة
دعونا نوضح الكود بالتفصيل:
- تعلن النافذة عن متغيرين للمثيل:
سيكون المصفوف tâches هو مصفوفة الخيوط التي تم إنشاؤها. وسيتم ربط المصفوف compteurs بالمصفوف tâches. سيكون لكل مهمة عداد خاص بها لاسترداد قيمة الحقل txtGénérés من واجهة المستخدم الرسومية.
- عند النقر على الزر Générer، يتم تنفيذ الطريقة btnGénérer_actionPerformed.
- تبدأ هذه الطريقة باسترداد عدد الخيوط المطلوب إنشاؤها. عند الضرورة، يتم الإبلاغ عن خطأ إذا كان هذا العدد غير قابل للاستخدام. ثم تقوم بإنشاء الخيوط المطلوبة مع الحرص على تدوين مراجعها في مصفوفة وإعطاء رقم لكل منها. تقوم الطريقة run للخيوط التي تم إنشاؤها بإرجاع الطريقة incrémente للفئة. يتم تشغيل جميع الخيوط (start). كما يتم إنشاء مصفوفة العدادات المرتبطة بالخيوط.
- الطريقة incrémente:
- تقرأ القيمة الحالية للحقل txtGénérés وتخزنها في العداد الخاص بالخيط قيد التنفيذ
- تتوقف لمدة 100 مللي ثانية، وذلك من أجل إخلاء المعالج عمدًا
- تعرض القيمة الجديدة في الحقل txtGénérés
لنوضح الآن سبب عدم صحة عد الخيوط. لنفترض أن هناك خيطين يجب إنشاؤهما. يتم تنفيذهما بترتيب غير متوقع. يمر أحدهما أولاً ويقرأ القيمة 0 من العداد. ثم يغيرها إلى 1 لكنه لا يكتبها في النافذة: فهو يتوقف عمدًا لمدة 100 مللي ثانية. وبذلك يفقد وحدة المعالجة المركزية التي تُمنح بعد ذلك لخيط آخر. يعمل هذا الخيط بنفس طريقة الخيط السابق: يقرأ عداد النافذة ويسترد الرقم 0 الذي لا يزال موجودًا فيه. يغير العداد إلى 1، وكما فعل السابق، يتوقف لمدة 100 مللي ثانية. ثم يُمنح المعالج مرة أخرى للخيط الأول: سيقوم هذا بكتابة القيمة 1 في عداد النافذة وينتهي. يُمنح المعالج الآن للخيط الثاني الذي سيكتب هو الآخر 1. نحصل على نتيجة غير صحيحة.
من أين تأتي المشكلة؟ قرأ الخيط الثاني قيمة خاطئة لأن الخيط الأول قد توقف قبل أن ينهي مهمته التي كانت تتمثل في تحديث عداد النافذة. وهذا يقودنا إلى مفهوم المورد الحرج والقسم الحرج في البرنامج:
- المورد الحرج هو مورد لا يمكن أن يمتلكه سوى خيط واحد في كل مرة. هنا، المورد الحرج هو العداد 2 في النافذة.
- القسم الحرج في البرنامج هو تسلسل من التعليمات في تدفق تنفيذ الخيط الذي يصل خلاله إلى مورد حرج. يجب التأكد من أنه خلال هذا القسم الحرج، يكون هو الوحيد الذي يمكنه الوصول إلى المورد.
7.6.2. العد المتزامن حسب الطريقة
في المثال السابق، كان كل مؤشر ترابط ينفذ الطريقة incrémente الخاصة بالنافذة. تم تعريف الطريقة incrémente على النحو التالي:
الآن نعلنها بشكل مختلف:
الكلمة الرئيسية synchronized تعني أنه لا يمكن إلا لخيط واحد في كل مرة تنفيذ الطريقة incrémente. لننظر إلى الترميزات التالية:
- كائن النافذة F الذي ينشئ الخيوط في btnGénérer_actionPerformed
- خيطان T1 و T2 تم إنشاؤهما بواسطة F
يتم إنشاء الخيطين بواسطة F ثم يتم تشغيلهما. وبالتالي، سيقوم كلاهما بتنفيذ الطريقة F.run. لنفترض أن T1 وصل أولاً. يقوم بتنفيذ F.run ثم F.incremente وهي طريقة متزامنة. يقرأ القيمة 0 للعداد، ويزيدها ثم يتوقف لمدة 100 مللي ثانية. ثم يُعطى المعالج إلى T2 الذي يقوم بدوره بتنفيذ F.run ثم F.incremente. وهنا يتم حظره لأن الخيط T1 يقوم بتنفيذ F.incremente والكلمة الرئيسية synchronized تضمن أن خيطًا واحدًا فقط في كل مرة يمكنه تنفيذ F.incremente. ثم يفقد T2 بدوره وحدة المعالجة المركزية دون أن يتمكن من قراءة قيمة العداد. بعد مرور 100 مللي ثانية، يستعيد T1 وحدة المعالجة المركزية، ويعرض القيمة 1 للعداد، ويخرج من F.incremente ثم F.run وينتهي. يستعيد T2 عندئذٍ المعالج ويمكنه هذه المرة تنفيذ F.incremente لأن T1 لم يعد يقوم بتنفيذ هذه الطريقة. ثم يقرأ T2 القيمة 1 للعداد، ويزيدها، ويتوقف لمدة 100 مللي ثانية. بعد 100 مللي ثانية، يستعيد وحدة المعالجة المركزية، ويعرض القيمة 2 للعداد، وينتهي هو الآخر. هذه المرة، تكون القيمة التي تم الحصول عليها صحيحة. فيما يلي مثال تم اختباره:

7.6.3. العد المتزامن بواسطة كائن
في المثال السابق، تمت مزامنة الوصول إلى العداد txtGénérés بواسطة طريقة. إذا كانت النافذة التي تنشئ الخيوط تسمى F، فيمكننا أيضًا القول إن الطريقة F.incremente تمثل موردًا لا يجب استخدامه إلا بواسطة خيط واحد في كل مرة. لذا فهو مورد حرج. تم ضمان الوصول المتزامن إلى هذا المورد بواسطة الكلمة الرئيسية synchronized:
ويمكن القول أيضًا إن المورد الحرج هو الكائن F نفسه. وهذا أكثر صرامة من الحالة التي يكون فيها المورد الحرج هو F.incremente. ففي الحالة الأخيرة، إذا قام مؤشر الترابط T1 بتنفيذ F.incremente، فلن يتمكن مؤشر الترابط T2 من تنفيذ F.incremente ولكنه سيتمكن من تنفيذ طريقة أخرى للكائن F سواء كانت متزامنة أم لا. في حالة ما إذا كان الكائن F هو المورد الحرج نفسه، فعندما يقوم مؤشر الترابط T1 بتنفيذ قسم متزامن من هذا الكائن، تصبح أي أقسام متزامنة أخرى من الكائن غير قابلة للوصول بالنسبة لمؤشرات الترابط الأخرى. وبالتالي، إذا نفذ مؤشر الترابط T1 الطريقة المتزامنة F.incremente، فلن يتمكن مؤشر الترابط T2 من تنفيذ F.incremente فحسب، بل وأي قسم متزامن آخر من F، وذلك حتى لو لم يستخدمها أي مؤشر ترابط. لذا فهي طريقة أكثر تقييدًا.
لنفترض إذن أن النافذة أصبحت المورد الحرج. سنكتب عندئذٍ:
// زيادة
private void incrémente(){
// القسم الحرج
synchronized(this){
// استرداد رقم الخيط
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// قراءة قيمة عداد المهام
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// يتم زيادته
compteurs[iThread]++;
// ننتظر 100 مللي ثانية - سيخسر الخيط عندئذ المعالج
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// نقوم بعرض العداد الجديد
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}//متزامن
}// يتم الزيادة
تستخدم جميع الخيوط النافذة this للتزامن. عند التنفيذ، نحصل على نفس النتائج الصحيحة كما في السابق. يمكننا في الواقع التزامن على أي كائن معروف لجميع الخيوط. إليك على سبيل المثال طريقة أخرى تعطي نفس النتائج:
// متغيرات المثيل
Thread[] tâches=null; // الخيوط
int[] compteurs=null; // العدادات
Object synchro=new Object(); // كائن تزامن الخيوط
// زيادة
private void incrémente(){
// القسم الحرج
synchronized(synchro){
..............
}//متزامن
}// زيادة
تقوم النافذة بإنشاء كائن من النوع Object الذي سيُستخدم لمزامنة الخيوط. هذه الطريقة أفضل من تلك التي تتم المزامنة فيها على الكائن this لأنها أقل تقييدًا. هنا، إذا كان مؤشر ترابط T1 موجودًا في القسم المتزامن لـ incrémente وأراد مؤشر ترابط T2 تنفيذ قسم متزامن آخر من نفس الكائن this ولكن متزامن بواسطة كائن آخر غير synchro، فسيكون بإمكانه ذلك.
7.6.4. التزامن عن طريق الأحداث
هذه المرة، نستخدم قيمة منطقية peutPasser لإعلام مؤشر الترابط ما إذا كان بإمكانه الدخول إلى قسم حرج أم لا. قد يكون الكتابة بدون تزامن كما يلي:
while(! peutPasser); // في انتظار أن يصبح peutPasser صحيحًا
peutPasser=false; // لا يجب أن يمر أي مؤشر ترابط آخر
section critique; // هنا الخيط وحده
peutPasser=true; // يمكن لخيط آخر المرور في القسم الحرج
التعليمات الأولى التي يتكرر فيها الخيط في انتظار أن يصبح peutPasser صحيحًا هي تعليمات غير ملائمة: فالخيط يشغل المعالج دون داعٍ. وهذا ما يُسمى بالانتظار النشط. يمكن تحسين الكتابة على النحو التالي:
while(! peutPasser){ // ننتظر حتى يتحول peutPasser إلى صحيح
Thread.sleep(100); // توقف لمدة 100 مللي ثانية
}
peutPasser=false; // لا يجب أن يمر أي مؤشر ترابط آخر
section critique; // هنا الخيط بمفرده
peutPasser=true; // يمكن لخيط آخر المرور في القسم الحرج
حلقة الانتظار هنا أفضل: إذا لم يتمكن الخيط من المرور، فإنه يدخل في حالة سكون لمدة 100 مللي ثانية قبل أن يتحقق مرة أخرى مما إذا كان بإمكانه المرور أم لا. وفي هذه الأثناء، سيتم تخصيص المعالج لخيوط أخرى في النظام.
هاتان الطريقتان غير صحيحتين في الواقع: فهما لا تمنعان خيطين من الدخول في نفس الوقت إلى القسم الحرج. لنفترض أن خيطًا T1 يكتشف أن peutPasser صحيح. عندها سينتقل إلى التعليمات التالية حيث يعيد تعيين peutPasser إلى "خطأ" لحجب الخيوط الأخرى. إلا أنه من الممكن تمامًا أن يتم مقاطعته في هذه اللحظة، إما لأن حصته من وقت المعالج قد نفدت، أو لأن مهمة ذات أولوية أعلى قد طلبت المعالج، أو لأي سبب آخر. والنتيجة هي أنه يفقد المعالج. وسيستعيده بعد ذلك بقليل. وفي غضون ذلك، ستحصل مهام أخرى على المعالج، ومن بينها ربما مؤشر ترابط T2 الذي يدور في حلقة انتظارًا حتى يصبح peutPasser صحيحًا. وسيكتشف هو أيضًا أن peutPasser أصبح صحيحًا (لم يتسنى للخيط الأول الوقت لتعيينه على أنه خطأ) وسينتقل هو أيضًا إلى القسم الحرج. وهذا ما كان يجب تجنبه.
التسلسل
while(! peutPasser){ // ننتظر حتى يتحول peutPasser إلى صحيح
try{
Thread.sleep(100); // توقف لمدة 100 مللي ثانية
} catch (Exception e) {}
}// بينما
peutPasser=false; // يجب ألا يمر أي مؤشر ترابط آخر
هي تسلسل مهم يجب حمايته بالتزامن. استلهامًا من المثال السابق، يمكننا كتابة:
synchronized(synchro){
while(! peutPasser){ // في انتظار أن يصبح peutPasser صحيحًا
try{
Thread.sleep(100); // توقف لمدة 100 مللي ثانية
} catch (Exception e) {}
}//while
peutPasser=false; // يجب ألا يمر أي مؤشر ترابط آخر
}// متزامن
section critique; // هنا الخيط بمفرده
peutPasser=true; // يمكن لخيط آخر المرور في القسم الحرج
يعمل هذا المثال بشكل صحيح. يمكن تحسينه بتجنب الانتظار شبه النشط للخيط عندما يراقب بانتظام قيمة القيمة المنطقية peutPasser. بدلاً من الاستيقاظ بانتظام كل 100 مللي ثانية للتحقق من حالة peutPasser، يمكنه أن ينام ويطلب إيقاظه عندما تكون قيمة peutPasser صحيحة. نكتب ذلك بالطريقة التالية:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // إذا لم يكن بالإمكان المرور، فإننا ننتظر
} catch (Exception e){
…
}
}
peutPasser=false; // لا يجب أن يمر أي مؤشر ترابط آخر
}// متزامن
لا يمكن تنفيذ العملية synchro.wait() إلا بواسطة مؤشر ترابط "مالك" مؤقت للكائن synchro. وهنا، التسلسل هو:
synchronized(synchro){
…
}// متزامن
التي تضمن أن الخيط هو المالك للكائن synchro. من خلال العملية synchro.wait()، يتنازل الخيط عن ملكية قفل التزامن. لماذا؟ بشكل عام لأن الموارد اللازمة لمواصلة العمل غير متوفرة. لذا، بدلاً من حجب الخيوط الأخرى التي تنتظر المورد synchro، فإنه يتنازل عنه وينتظر المورد الذي ينقصه. في مثالنا، ينتظر أن يتحول القيمة المنطقية peutPasser إلى صحيح. كيف سيتم إخطاره بهذا الحدث؟ بالطريقة التالية:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // إذا تعذر المرور، ننتظر
} catch (Exception e){
…
}
}
peutPasser=false; // لا يجب أن يمر أي مؤشر ترابط آخر
}// متزامن
section critique...
synchronized(synchro){
synchro.notify();
}
لنفترض أن الخيط الأول هو الذي يمر عبر قفل التزامن. لنسمه T1. لنتخيل أنه يجد القيمة المنطقية peutPasser صحيحة لأنه الأول. لذا، فإنه يمررها إلى قيمة خاطئة. ثم يخرج من القسم الحرج المقفل بواسطة الكائن synchro. يمكن عندئذٍ لخيط آخر الدخول إلى القسم الحرج لاختبار قيمة peutPasser. سيجده كاذبًا، ثم ينتظر حدثًا (wait). وبذلك، يتنازل عن ملكية الكائن synchro. يمكن عندئذٍ لخيط آخر الدخول إلى القسم الحرج: سيقوم هو أيضًا بالانتظار لأن peutPasser غير صحيح. يمكننا إذن أن نحصل على عدة خيوط في انتظار حدث على الكائن synchro.
لنعد إلى الخيط T1 الذي تم تمريره إليه. يقوم بتنفيذ القسم الحرج ثم يشير إلى أنه يمكن لخيط آخر المرور الآن. ويقوم بذلك باستخدام التسلسل:
synchronized(synchro){
synchro.notify();
}
يجب عليه أولاً استعادة الكائن synchro باستخدام التعليمات synchronized. لا ينبغي أن يشكل ذلك مشكلة لأنه يتنافس مع خيوط أخرى، والتي إذا حصلت مؤقتًا على الكائن synchro، فيجب عليها التخلي عنه باستخدام wait لأنها تجد أن peutPasser غير صحيح. لذلك، سيحصل مؤخراً مؤشر الترابط الخاص بنا T1 على ملكية الكائن synchro. وبعد ذلك، يشير من خلال العملية synchro.notify إلى أن أحد الخيوط المعطلة بواسطة synchro.wait يجب إيقاظه. ثم يتخلى مرة أخرى عن ملكية الكائن synchro الذي سيُمنح بعد ذلك لأحد الخيوط المنتظرة. ويواصل هذا الخيط تنفيذه مع التعليمات التي تلي wait التي كانت قد وضعته في حالة انتظار. وبدوره، سيقوم بتنفيذ القسم الحرج وتنفيذ synchro.notify لتحرير خيط آخر. وهكذا دواليك.
لنرى طريقة العمل هذه على مثال العد الذي درسناه سابقًا.
void btnGénérer_actionPerformed(ActionEvent e) {
//إنشاء الخيوط
// قراءة عدد الخيوط المطلوب إنشاؤها
int nbThreads=0;
try{
// قراءة الحقل الذي يحتوي على عدد الخيوط
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// موجب >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//خطأ
txtStatus.setText("Nombre invalide");
// إعادة المحاولة
txtAGénérer.requestFocus();
return;
}//استثناء
// RAZ عداد المهام
txtGénérés.setText("0"); // عداد المهام عند 0
// يمكن للخيط الأول المرور
peutPasser=true;
// يتم إنشاء الخيوط وتشغيلها
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// يتم إنشاء الخيط i
tâches[i]=new Thread() {
public void run() {
synchronise();
}
};//الخيط i
// يتم تحديد اسمه
tâches[i].setName(""+i);
// يتم تشغيله
tâches[i].start();
}//for
}//إنشاء
الآن، لم تعد الخيوط تنفذ الطريقة incrémente بل الطريقة التالية synchronise:
// مرحلة مزامنة الخيوط
public void synchronise(){
// طلب الوصول إلى القسم الحرج
synchronized(synchro){
try{
// هل يمكن المرور؟
if(! peutPasser){
System.out.println(Thread.currentThread().getName()+ " en attente");
synchro.wait();
}
// لقد تم المرور - يُمنع الخيوط الأخرى من المرور
peutPasser=false;
} catch(Exception e){
txtStatus.setText(""+e);
return;
}//محاولة
}// متزامن
// القسم الحرج
System.out.println(Thread.currentThread().getName()+ " passé");
incrémente();
// انتهينا - نحرر أي مؤشر ترابط قد يكون محجوبًا عند مدخل القسم الحرج
peutPasser=true;
System.out.println(Thread.currentThread().getName()+ " terminé");
synchronized(synchro){
synchro.notify();
}// متزامن
} // مزامنة
تهدف الطريقة synchronise إلى تمرير الخيوط واحدًا تلو الآخر. وهي تستخدم لهذا الغرض متغير التزامن synchro. لم تعد الطريقة incrémente محمية الآن بواسطة الكلمة الرئيسية synchronized:
// زيادة
private void incrémente(){
// نسترد رقم الخيط
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// قراءة قيمة عداد المهام
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// يتم زيادته
compteurs[iThread]++;
// ننتظر 100 مللي ثانية - سيخسر الخيط عندئذ المعالج
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// نقوم بعرض العداد الجديد
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}// زيادة
بالنسبة لـ 5 خيوط، النتائج التي تم الحصول عليها هي التالية:
0 passé
1 en attente
2 en attente
3 en attente
4 en attente
0 terminé
1 passé
1 terminé
2 passé
2 terminé
3 passé
3 terminé
4 passé
4 terminé
لنفترض أن T0 إلى T4 هي الخيوط الخمسة التي تم إنشاؤها بواسطة التطبيق. يستحوذ T0 أولاً على قفل synchro ويجد أن peutPasser صحيح. ويضع peutPasser على "خطأ" ويمر: هذا هو معنى الرسالة الأولى passé. على الأرجح، يستمر وينفذ القسم الحرج، ولا سيما الطريقة incrémente. وفي هذه الطريقة، سيدخل في حالة سكون لمدة 100 مللي ثانية (sleep). وبالتالي، فإنه يترك المعالج. ويتم تخصيص المعالج لخيط آخر، وهو الخيط T1 الذي يحصل عندئذٍ على ملكية الكائن synchro. يكتشف أنه لا يمكنه المرور ويدخل في حالة انتظار (wait). ثم يتخلى عن ملكية الكائن synchro وكذلك عن المعالج. يتم تخصيص المعالج للخيط T2 الذي يواجه نفس المصير. خلال 100 مللي ثانية من توقف T0، يتم وضع الخيوط من T1 إلى T4 في حالة انتظار. هذا هو معنى الرسائل الأربع "في حالة انتظار". بعد 100 مللي ثانية، يستعيد T0 المعالج وينهي عمله: هذا هو معنى الرسالة "0 terminé". ثم يحرر أحد الخيوط المحجوبة وينتهي. يتم بعد ذلك تخصيص المعالج الذي تم تحريره لخيط متاح: وهو الخيط الذي تم تحريره للتو. هنا هو T1. يدخل الخيط T1 بعد ذلك إلى القسم الحرج: هذا هو معنى الرسالة "1 passé". يقوم بما عليه القيام به ثم يتوقف بدوره لمدة 100 مللي ثانية. يصبح المعالج عندئذ متاحًا لخيط آخر، لكن جميع الخيوط في انتظار حدث ما: لا يمكن لأي منها الاستحواذ على المعالج. بعد 100 مللي ثانية، يستعيد الخيط T1 المعالج وينتهي: هذا هو معنى الرسالة "1 terminé". ستتصرف الخيوط من T1 إلى T4 بنفس طريقة T1: هذا هو معنى سلسلة الرسائل الثلاث: "تم"، "انتهى".



