• 0
romansy

Multithreaded Programming

سؤال

Multithreaded Programming

=======================

أغلب البرامج التي نتعامل معها هذه الأيام تستطيع عمل العديد من الأشياء في نفس الوقت Concurrenlty ، مثلا متصفح الأنترنت يقوم بعرض الصور والبيانات وفي نفس اللحظه يستطيع المستخدم التعامل مع قوائم البرنامج ، مثال أخر لبرنامج المحادثه Messanger فقد تستطيع ارسال رساله الى الطرف الأخر وفي نفس اللحظه تقوم باستقبال الرساله القادمه من الجهه الأخرى . أيضا في مجال الألعاب دائما نريد ان نشغل أكثر من وظيفه Method في نفس الوقت لأجرء عمليات مختلفه .

وفي لغات البرمجه عموما ، نستطيع التعامل مع بعض الأحداث في نفس اللحظه Concurrently باستخدام ما يعرف بـ Thread . وهو مسار في تنفيذ البرنامج ينتقل التنفيذ فيه من بدايه هذه الـ Thread الى نهايته . ويمكن أن يحتوي البرنامج ( العمليه Process ، لأن أي برنامج يعمل في Process خاصه به) على أكثر من Thread وهنا يسمى البرنامج بـ Mutlithread .

حتى نربط بعض الأمور ، فإن الـ Mutlithread تعتبر حاله خاصه من Mutlitasking ، حيث أن الـ Mutlitasking يمكن أن تصنف الى Process-based و Thread-based . أغلب المستخدمين يعرفوا ماذا يعني Process Mutlitasking وهو ببساطه تشغيل أكثر من برنامج في نفس اللحظه ، كأن تقرأ الأن هذه الصفحه في متصفح الأنترنت وأنت تسمع للصوت في Realplayer وتترجم برنامج باستخدام gcc . وهنا يقوم الـ CPU باعطاء كل برنامج|عمليه فتره معينه Quantim Time وبعدها عند إنتهاء هذا الزمن يقوم بالتحويل الى برنامج أخر ، هذه العمليه تتم بشكل سريع جدا لذلك سوف تحس أن جميع البرامج تعمل في أن واحد . النوع الأخر من الـ Mutlitasking وهو الـ Thread-based وهنا سيكون لدينا أكثر من مسار Thread في البرنامج الواحد يعملوا مع بعض ، مثلا تقوم بالكتابه في برنامج محرر النصوص word وفي نفس اللحظه تقوم بطباعه صفحه ، باللإضافه الى عمل محرر الأخطاء في وورد (والذي يعمل في Thread منفصل) . وبنفس الأمر هذه الـ Thread لا تعمل مع بعض في نفس اللحظه ، بل لكل منها زمن معين أو شرط معين حتى يعمل لكن الفرق هنا أنك أنت المبرمج من سيتحكم بهذه العمليه .

والتعامل مع الـ Thread أفضل وأخف وأسرع من التعامل مع الـ Process حيث هذه الأخيره heavyweight أي تتطلب مساحه عنواين خاصه بها Address Space ، إضافه الى أن عمليه الـ InterProcess Communication و Context Switching مكلفه وليست سهله كما في الـ Thread الذي يتشارك في مساحه العمليه process .

اذا العمليه Process قد تحتوي على أكثر من Thread بداخلها ، تتشارك هذه المسارات Threads في المتغيرات العامه في الـ Porcess ،بالإضافه الى أن كل Thread منها قد يحتوي على متغيرات داخليه خاصه به أي أن لكل Thread مكدس Stack خاصه فيه .

في لغه البرمجه جافا ، لن تستطيع تجاهل هذا المفهوم أبدا حتى لو أردت أن تطبع جمله Hello World لأن عند تنفيذ أي برنامج يقوم الـ JVM بتشغيل Thread يسمى Main Thread ومنه يبدأ تنفيذ البرنامج . لهذا السبب أي برنامج جافا يعمل دائما على VM خاصه به .

لذلك عاجلا أم أجلا سوف تضطر الى أستخدامه ، وسوف نأخذ أمثله بعد قليل لحالات لا يمكن حلها الا باستخدام الـ Thread . فتوكل على الله ، وأترك التردد ، وأبدا معنا :) .

محتويات الموضوع ::

---------------------

Main Thread

Using Thread IN Java

MultiThread Example

Using Join Method

Asynchronization Problem

DeadLock

InterThread Communication

Controlling Thread

Playing With Mutlithreading

Resource

Main Thread

===========

قبل أن نبدأ بعرض كيفيه عمل الـ Thread يجب أن نتحدث عن الـ Thread الرئيسي الذي يعمل مع بدء تشغيل برنامجك وهو يسمى main thread . هذا الـ MainThread ضروري لأمرين أولهما أنه يكون أب لجميع الـ Thread التي سوف تنشئها أيضا يكون هو أخر Thread ينتهي من العمل Shutdown Thread .

مثال لطباعه معلومات الـ Main Thread بالاضافه الى استخدام بعض الدوال :

/* Work With Main Thread */

public class ThreadDemo
{
public static void main (String args[])
{
Thread t = Thread.currentThread(); // here get this Thread
System.out.println("Current Thread Info : " + t ); // toString Method
t.setName("My Thread"); // change Thread Name
System.out.println("Current Thread Info : " + t ); // to String Method
System.out.println("Current Thread Name : " + t.getName() ); // print Thread Name

try
{
for (int i=0; i<5; i++)
{
System.out.println("Welecom To Thread Programming :)");
Thread.sleep(1000); // sleep this thread 1 second
}
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Interrupted");
}
}
}

في المثال أعلاه قمنا أولا بأخذ Reference للـ Thread الحالي وذلك بواسطه الداله currentThread والتي هي داله static موجوده في الكلاس Thread . بعدها قمنا بطباعه الـ Thread الحالي وهو t في حالتنا ، وكان مخرج هذا السطر هو :

Current Thread Info : Thread[main,5,main]

القيمه الأولى داخل الأقواس هي أسم الـ Thread . والقيمه الثانيه هو قيمه تحدد الأولويه لهذا الـ Thread تعرف بـ Priority وكل ما كانت قيمه الأولويه أعلى معناه هذا الثريد له الأولويه في العمل على CPU ، مثلا لو كانت الأولويه 2 وبدأ Thread أخر بالعمل سوف يسقط هذا الـ Thread لأن له أولويه أعلى منه . القيمه الثالثه وهي تحدد الـ Group الخاصه بThread ، فيمكن أن نتعامل مع عده Threads مع بعضهما عن طريق التعامل مع group معين . ولكن نادرا ما نحتاج الى التعامل مع هذه الـ group ، فقط يستخدمها JVM بنفسه.

نكمل في المثال . قمنا بعد طباعه معلومات الـ Thread الحالي ، بتغيير اسمه عن طريق الداله setName ، وبعدها قمنا بطباعه المعلومات مره أخرى ، وستلاحظ أن القيمه الأولى أختلف الأن وأصبحت My Thread . بعدها طبعنا أسم الThread فقط باستخدام الداله getName .

أخيرا في الكتله الأخيره مابين try و catch قمنا بطباعه جمله 5 مرات ، وفي كل مره نقوم بعمل sleep لهذا الThread أي نوقف عمله لمده معينه نقوم بارسالها كمعامل للداله . القيمه تكون Millisecond . الداله sleep قد تولد Exception أسمه InterruptedException وقد يحدث عندما يكون الThread في حاله sleep ويقوم Thread أخر بعمل Interrupt لهذا الThread حتى يكمل عمله ( أي يوقظ هذا الـ Thread ) .

Using Thread IN Java

================

كما نعرف أن أحد ميزات لغه الجافا هي سهوله التعامل مع الـ Thread ، حيث أننا نستطيع عمل الكائن كـ Thread بمجرد تطبيق الأنترفيس Implements Interface Runnable وإعاده تعريف الداله run بالوظيفه التي تريد أن يقوم بها الـ Thread . أو بطريقه ثانيه وهي عمل كائن يرث الكلاس Thread ونقوم باعاده تعريف Override الداله run (حيث أن الكلاس Thread يطبق Imeplements Runnable ) .

نستعرض الأن الطريقه التي نرث من الكلاس Thread . أولا الكلاس Thread به مجموعه من الداول مثل sleep و join والعديد من الدوال كلها معرفه مسبقا لكن هناك داله run توجد في Thread لأن Thread implements Runnable ، هذه الداله run هي التي يجب أن نضع فيها الكود الذي نريده في Thread ، أي أن بدايه الـ Thread تكون من بدايه الداله run ونهايته تكون بانتهاء عمل الداله run . بشكل أوضح الداله run يمكن أن تعتبرها بمثابه الداله main بالنسبه للـ Thread .

بعد عمل كلاس يقوم بوراثه الكلاس Thread ، نقوم باعاده تعريف الداله run بما نريد . الان لكي نشغل الThread يجب أن نستدعي الداله strart (وهي تقوم باستدعاء run ) .

في المثال التالي ، سوف نقوم بعمل Thread يقوم بطباعه أسمه wajdy خمس مرات كل نصف ثانيه ، وفي نفس اللحظه في الMain Thread نقوم بطباعه main كل ثانيه . أي أن المخرج يجب أن يكون main ثم wajdy ثم wajdy ثم main وهكذا ...

/* Thread By Extending From Thread Class */

public class ThreadDemo
{
public static void main (String args[])
{
MyThread thread = new MyThread("Wajdy");

try
{
for (int i=0; i<5; i++)
{
System.out.println("Main : " + i);
Thread.sleep(1000);
}
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Inptrrupted ");
}
}
}

class MyThread extends Thread
{
public MyThread (String s)
{
super(s);
System.out.println(this);
start();
}

public void run ()
{
try
{
for (int i=0; i<5; i++)
{
System.out.println(this.getName() + " : " + i);
Thread.sleep(500);
}
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Inptrrupted ");
}
}
}

في المثال أعلاه وفي الداله main قمنا بعمل كائن من كلاس يرث Thread ، اذاً الكائن thread في المثال يعتبر Thread . وكما ذكرنا ان الكلاس Thread به 7 دوال ، وسوف نستخدم الداله التي يمكن أن نرسل لها أسم ليكون هو أسم الـ Thread . وقمنا بارسال أسم wajdy لداله البناء في الكلاس MyThread والذي بدورها تمرره لداله البناء في الكلاس Thread . وقمنا بطباعه معلومات الThread الحالي ، وبعدها قمنا بتشغيل الـ Thread عن طريق الداله start والتي سوف تقوم باستدعاء الداله run ومنها يبدأ عمل الـ Thread .

لاحظ أننا عندما تكلمنا عن الـ Main Thread قبل قليل ، ذكرنا أنه مهمه لأنها يجب أن تكون أخر Thread ينتهي العمل منه ، وفي اصدارات جافا القديمه كان الـ VM يعلق Hang في حال اذا انتهي الـ Main Thread قبل أي Thread أخر .

لذلك لكي نتجنب المشاكل قمنا بعمل sleep للMain Thread بمده ثانيه ، أما الـ Thread الأخر wajdy فقمنا بتأخيره مده نصف ثانيه وهكذا نضمن أن الMain Thread ينتهي في الأخير . بعد قليل سنرى طريقه أفضل من هذه باستخدام الداله join .

المخرج سوف يكون بهذا الشكل :

Thread[Wajdy,5,main]

Main : 0

Wajdy : 0

Wajdy : 1

Main : 1

Wajdy : 2

Wajdy : 3

Main : 2

Wajdy : 4

Main : 3

Main : 4

الأن نقوم بكتابه نفس المثال ولكن باستخدام الطريقه الثانيه وهي عن طريق عمل implements للأنترفيس Runnable ، وهنا بعد أن نعمل كائن من كلاس implements Runnbale يجب أن نمرر هذا الكائن للداله البناء في الكلاس Thread ومن ثم نقوم بتشغيله بالداله start .

داله البناء في Thread :

Thread(Runnable threadOb, String threadName)

المثال :

/* Thread By Implements Runnable interface */

public class ThreadDemo
{
public static void main (String args[])
{
MyThread thread = new MyThread("Wajdy");

try
{
for (int i=0; i<5; i++)
{
System.out.println("Main : " + i);
Thread.sleep(1000);
}
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Interrupted !");
}
}
}

class MyThread implements Runnable
{
private Thread t;

public MyThread (String str )
{
t = new Thread(this,str);
System.out.println(t);
t.start();
}

public void run ()
{
try
{
for (int i=0; i<5; i++)
{
System.out.println(t.getName() + " : " + i);
Thread.sleep(500);
}
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Interrupted !");
}
}
}

MultiThread Example

================

الى الأن كنا نتعامل مع Main Thread بالأضافه الى Thread أخر ، بالطبع نستطيع عمل أكثر من Thread مع بعض ، المثال التالي ينشئ 3 مسارات في البرنامج ، وفي الداله main سوف نتنظر حوالي 10 ثواني وهكذا سنضمن أن أخر Thread ينتهي هو main .

/* Thread By Implements Runnable interface */

public class ThreadDemo
{
public static void main (String args[])
{
MyThread thread = new MyThread("Wajdy");
MyThread thread1 = new MyThread("Ali");
MyThread thread2 = new MyThread("Ahmed");

try
{
Thread.sleep(10000);
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Interrupted !");
}
}
}

class MyThread implements Runnable
{
private Thread t;

public MyThread (String str )
{
t = new Thread(this,str);
System.out.println(t);
t.start();
}

public void run ()
{
try
{
for (int i=0; i<5; i++)
{
System.out.println(t.getName() + " : " + i);
Thread.sleep(1000);
}
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Interrupted !");
}
}
}

المخرج :

Thread[Wajdy,5,main]

Thread[Ali,5,main]

Thread[Ahmed,5,main]

Ali : 0

Wajdy : 0

Ahmed : 0

Ali : 1

Wajdy : 1

Ahmed : 1

Ali : 2

Wajdy : 2

Ahmed : 2

Ali : 3

Wajdy : 3

Ahmed : 3

Ali : 4

Wajdy : 4

Ahmed : 4

Using Join Method

==============

الى هنا وفي كل مره أردنا أن نعمل Thread جديد ، يجب أن نزيد المده التي ينتظرها الMain Thread حتى يكون هو أخر Thread ينتهي من العمل . طبعا مثل هذه الطريقه غير صحيحه أبدا حيث أننا في بعض الأحيان لن نعرف متى ينتهي عمل الـThread لارتباطه مثلا بشرط معين . وجافا قدمت الداله join والتي تحل هذه المشكله .

الداله join عندما نطبقها على Thread ما ، فأنها تعني أنه يتم الأنتظار الى أن ينتهي هذا الـ Thread من العمل . المثال القادم يقوم بنفس عمل المثال السابق ولكن مع استخدام الداله join بدلا من sleep من الداله main . أيضا سوف نستخدم الداله isAlive وهي ترجع true في حال كان الـ Thread يعمل ، والا فترجع false .

/* Thread By Implements Runnable interface */

public class ThreadDemo
{
public static void main (String args[])
{
MyThread thread = new MyThread("Wajdy");
MyThread thread1 = new MyThread("Ali");
MyThread thread2 = new MyThread("Ahmed");

System.out.println( thread.t.getName() + " Is Alive : " + thread.t.isAlive() );
System.out.println( thread1.t.getName() + " Is Alive : " + thread.t.isAlive() );
System.out.println( thread2.t.getName() + " Is Alive : " + thread.t.isAlive() );

try
{
System.out.println("Main Wating Other Thread To Finish ");
thread.t.join();
thread1.t.join();
thread2.t.join();
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Interrupted !");
}

System.out.println( thread.t.getName() + " Is Alive : " + thread.t.isAlive() );
System.out.println( thread1.t.getName() + " Is Alive : " + thread.t.isAlive() );
System.out.println( thread2.t.getName() + " Is Alive : " + thread.t.isAlive() );

System.out.println("Main Thread End");
}
}

class MyThread implements Runnable
{
Thread t;

public MyThread (String str )
{
t = new Thread(this,str);
System.out.println(t);
t.start();
}

public void run ()
{
try
{
for (int i=0; i<5; i++)
{
System.out.println(t.getName() + " : " + i);
Thread.sleep(1000);
}
}
catch ( InterruptedException e)
{
System.out.println("Main Thread Interrupted !");
}

}
}

مثال أخر لبرنامج فيه 2-Threads الأول يطبع جمله Hello World ، والثاني يطبع أرقام . لمده خمس مرات ، وسوف نمرر للداله Sleep قيمه عشوائيه هذه المره ، حتى تتضح فكره عمل الـ Thread بشكل أكبر ، وهو أن الـ Thread يبدأ في عمله من بدايه run وبعدها في حال تم أستدعاء sleep سوف يتوقف لفتره معينه ، في هذه الفتره يبدأ الThread الأخر بالعمل الى أن يصل الى الداله sleep هو أيضا ، ويبدأ الأول بالعمل ، وهكذا ..

public class ThreadDemo
{
public static void main (String args[])
{
HelloThread ht = new HelloThread();
CountThread ct = new CountThread();

ht.start();
ct.start();

try
{
ht.join();
ct.join();
}
catch (InterruptedException e)
{
e.printStackTrace();
}

System.out.println("Main End");
}
}

class HelloThread extends Thread
{
public void run ()
{
try
{
for (int i=0; i<5; i++)
{
System.out.println("Hello Java Programmer !");
int pause = (int) ( Math.random()*3000); // from 0-2999
sleep(pause);
}
}
catch (InterruptedException e)
{ System.out.println(e); }
}
}

class CountThread extends Thread
{
public void run ()
{
try
{
for (int i=0; i<5; i++)
{
System.out.println(i);
int pause = (int) (Math.random()*3000);
sleep(pause);
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}

Asynchronization Problem :

======================

بالرغم من سهوله أستخدام الـ Thread ، فإنه في بعض الأحيان يكون مصدرا للمشاكل ، وخاصه عندما يكون لدينا أكثر من Thread يتعامل مع بيانات أو Data Structure في لحظه واحده ، وهنا سوف تكون النتيجه غير التي تريدها تماما ! .

أنظر للصوره التاليه ، وهنا لدينا 2-Thread كل واحد منهم يحاول أن يقوم بعمليه Update للمتغير sum ، طبعا عمليه الأضافه هذه تتم بعده خطوات صغيره ، فلو أراد Thread 1 تغيير المتغير sum يقوم أولا بقرائه قيمه المتغير الحاليه ، ويحفظها لديه في متغير مؤقت ، ثم بعد ذلك يقوم باضافه القيمه الجديده في المتغير المؤقت ، بعدها أخير يقوم بعمليه write في المتغير sum . نفس الكلام أيضا بالنسبه للـ Thread الثاني .

th.JPG

الأن في الصوره السابقه قام المسارين بعمليه القرائه في نفس اللحظه ( طبعا في "نفس اللحظه" كلمه تقريبيه لأنه يستحيل التعامل مع أكثر من Thread في نفس اللحظه بالضبط ، ولكن المقصد هنا أن المسار الأول قام بقرائه المتغير وقبل أن يجري باقي العمليات قام المسار الثاني بقرائه المتغير أيضا) . الأن يقوم Thread 1 بتغيير قيمه المتغير sum لتصبح 23 + 5 = 28 ، بعدها يقوم الـ Thread 2 بتغيير القيمه (تذكر أنه قرأ المتغير sum عندما كان 23 ) لذلك القيمه سوف تصبح 23 + 19 = 42 . وهذه نتيجه خاطئه ، لأن الصحيح هو 5 + 23 + 19 = 47 .

نأخذ مثال عملي يوضح هذه المشكله :

/* Example About Race Condition */

public class ThreadDemo
{
public static void main (String args[])
{
Data data = new Data();

MyThread t1 = new MyThread(data,"First");
MyThread t2 = new MyThread(data,"Second");
MyThread t3 = new MyThread(data,"Thrid");

t1.start();
t2.start();
t3.start();

try
{
t1.join();
t2.join();
t3.join();
}
catch ( InterruptedException e)
{ System.out.println("Main Interrupted"); }
}
}

class Data
{
public void print ( String d )
{
System.out.print("[" + d );
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{ e.printStackTrace(); }

System.out.println("]");
}
}

class MyThread extends Thread
{
private Data data;
private String str;

public MyThread ( Data d , String s)
{
data = d;
str = s;
}

public void run ()
{
data.print(str);
}
}

الأن في المثال السابق ، قمنا بعمل أكثر من Thread وShared Resource هو Data . الأن عندما قمنا بتشغيل جميع الـ Threads الثلاثه ، قام الأول بتنفيذ الداله print وطبع علامه ] بالإضافه الى First وبعدها قمنا بعمل sleep لهذا الThread (لكي تتضح لك العمليه بشكل أكبر) . وفي نفس اللحظه تقريبا قام الـ Thread الثاني والثالث بالعمل وكل منهم طبع علامه ] بالأضافه الى أسمه .

هنا في هذا المثال لا يوجد ما يمنع هؤلاء المسارات من أستدعاء الداله print من نفس الكائن في نفس اللحظه . ولحل هذه المشكله علينا أن نضع مقبض Lock على الداله التي تتعامل معها هذه الـ Thread في نفس اللحظه ، وحتى يتعامل الـ Thread مع الداله أو يجب أن يحصل على هذا المقبض ، فاذا لم يستطع الحصول فسوف ينتظر في صف الأنتظار الى أن يقوم الThread الذي يتعامل مع هذه الداله بعمل Release لهذا المقبض وبهذا يستطيع باقي الـ Thread بالحصول على المقبض.

في جافا تستطيع الحصول على المقبض بكلمه synchronized تضعها في الداله التي نريد أن يتعامل معها Thread واحد في نفس اللحظه ، أو باستخدم synchronized block كما سنوضح الأن .

نقوم الأن باصلاح المثال السابق ، وذلك بوضع كلمه synchronized في الداله print ، وهكذا نضمن أنه بعد حصول الـ Thread الأول على المقبض واستخدام الداله أن أي Thread أخر أراد أستدعاء نفس الداله أو أي داله synchronized من نفس الكائن سوف يكون في صف الأنتظار .

public synchronized void print ( String d )
{
System.out.print("[" + d );

try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{ e.printStackTrace(); }

System.out.println("]" );
}

قم بتشغيل المثال ،وستجد أن المخرج الأن صحيح ، وكل Thread لا يستطيع بدء عمليه الطباعه الأ أن ينتهي الThread الموجود في الداله print .

الحل السابق كان باستخدام الـ synchronized Method ، ولكنه في بعض الأحيان لن نستطيع استخدام هذا الحل ، ربما يكون أن الكلاس Data قد صمم وكتب وليس به هذه الكلمه synchronized ونحن لا نستطيع الوصول الى الكود لكي نضع synchronized ، لهذا يمكن أستخدام الحل الأخر وهو synchronized Block .

أستخدام الـ synchronized Block بهذا الشكل :

synchronized(object) {
// statements to be synchronized
}

ونضع هذه الجمله في المكان الذي نريد ان نصل فيه للداله الموجوده في الكائن المرسل ، وهذا الـ Block يضمن لنا أنه لن يستدعى الداله الموجوده في البلوك الا اذا كان المقبض خالي أي لا يوجد Thread يعمل في الداله .

نقوم بتغيير المثال السابق باستخدم الـ synchronized Block :

/* Example About Synchronized Statement*/

public class ThreadDemo
{
public static void main (String args[])
{
Data data = new Data();

MyThread t1 = new MyThread(data,"First");
MyThread t2 = new MyThread(data,"Second");
MyThread t3 = new MyThread(data,"Thrid");

t1.start();
t2.start();
t3.start();

try
{
t1.join();
t2.join();
t3.join();
}
catch ( InterruptedException e)
{ System.out.println("Main Interrupted"); }
}
}

class Data
{
public void print ( String d )
{
System.out.print("[" + d );

try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{ e.printStackTrace(); }

System.out.println("]" );
}
}

class MyThread extends Thread
{
private Data data;
private String str;

public MyThread ( Data d , String s)
{
data = d;
str = s;
}

public void run ()
{
synchronized ( data )
{
data.print(str);
}
}
}

DeadLock :

==========

حاليا أنهينا مشكله عدم التزامن باستخدام الـ Synchronized Method or Block ، ولكن في بعض الأحيان علينا أن نتوخي الحذر حتى لا نقع في مشكله جديده وهي الـ Deadlock . وهي تحصل في حال كان لدينا Thread A قد حصل على مقبض لداله ما . ويكون لدينا Thread B أيضا قد حصل على مقبض لداله ما ويريد أستدعاء الداله التي يستحوذ عليها A أو أي داله Synchronized في A (وهنا سوف ينتظر الى أن ينتهي A ) وفى نفس اللحظه يريد A الداله التي يعمل عليها B أو أي داله Synchronized في B (وهنا سوف ينتظر هو أيضا الى أن ينتهي B) . وهكذا سيظل الاثنان منتظران الى ما لا نهايه . الصوره التاليه لوضح العمليه :

th2.JPG

نأخذ مثال ليوضح عمليه الـ DeadLock ، وهنا سوف يكون لدينا كلاس A و B كل منهم يحتوي على دالتين Synchronized ، المهم في الكلاس Deadlock سوف نقوم بعمل كائن من A و B ، وسوف نقوم استدعاء الداله funcA في A ونمرر لها b ، ونفس الشيء بالنسبه لـ B . وسوف يحصل الDeadlock هنا لأننا في A نعمل بـ MainThread و نريد استدعاء b.print (ولن نستطيع بسبب أن RacingThread يعمل في B) و في B نعمل بـ RacingThread و نريد استدعاء a.print (ولن نستطيع بسبب أن A يعمل فيه MainThread .

/* Example Of DeadLock */

public class ThreadDemo
{
public static void main (String args[])
{
Thread.currentThread().setName("MainThread");
Deadlock d = new Deadlock("RacingThread");
}
}

class Deadlock extends Thread
{
private A a = new A();
private B b = new B();

public Deadlock(String s)
{
super(s);
start();
a.funcA(b);
}

public void run ()
{
b.funcB(a);
}
}

class A
{
public synchronized void funcA (B b)
{
String name = Thread.currentThread().getName();
System.out.println( name + " Inside A.funcA() ");

try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{ e.printStackTrace(); }

System.out.println( name + " try To Call B.printB() ");
b.printB();

}

public synchronized void printA ()
{
System.out.println("Inside A.printA()");
}
}

class B
{
public synchronized void funcB (A a)
{
String name = Thread.currentThread().getName();
System.out.println( name + " Inside B.funcB() ");

try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{ e.printStackTrace(); }

System.out.println( name + " try To Call A.printA() ");
a.printA();

}

public synchronized void printB ()
{
System.out.println("Inside B.printB()");
}
}

InterThread Communication :

=======================

بعض الأحيان نحتاج الى رابط أو وسيله نتمكن من خلالها التنقل من بين Thread إلى أخر ، أي نقوم بتشغيل Thread لكي نحقق غرض ما وبعدما ينهيه نقوم بتشغيل الأخر وهكذا .. نأخذ المثال الشهير Producer-Consumer Problem ، وهنا يقوم الـ Producer بتقديم شيء ما لنفرض أنه رقم ، ومن ثم يقوم الـ Consumer بأخذ هذا الرقم . أي هذا يقدم شيء ويقوم الأخر باستهلاكه ...

الحلل التقليدي بجعل Thread يمثل الـ Producer و Thread أخر يمثل الـ Consumer ، وبعدها نقوم بتشغيل الـ Threads وفي كل مره قبل أن أقدم Produce الرقم يجب أن أختبر هل تم أخذ ما وضعته المره السابقه ، فاذا لم يكن الـ Consumer قد أخذ هذا الرقم فيجب أن أنتظر الى أن يأخذه ، نفس الأمر عندما يعمل الـ Consumer يجب أن يكون هناك رقم لكي يأخذه والا عليه الأنتظار .

لكن لهذا مساوئ كبيره حيث أنه يضيع وقت الـ CPU في Idle Loop مستمربلا فائده ... لذلك يجب أن نتطرق لحل أخر وهو عن طريق الـ Communication بين هذه الـ Threads . حيث عندما يبدأ الـ Producer بالعمل نختبر هل أخذ الـ Consumer ما وضعه الـ Producer المره السابقه ، فاذا تحقق هذا الشرط فيضع الـ Producer الرقم الجديد ، أما اذا لم يتحقق فنقوم بعمل wait للـ Producer . هذه الداله wait تعني أن نقوم بعمليه sleep لهذا الـ Thread الى أن يقوم Thread أخر بايقاظه فيما بعد عن طريق الداله notify .

الأن عندما يبدأ الـ Consumer بالعمل يختبر أيضا هل هناك رقم ، فاذا كان هناك فيقوم بأخذه وإيقاظ الThread النائم عن طريق notify . أما اذا لم يكن هناك رقم فعليه الأنتظار الى أن يأتي هذا الرقم wait .

هكذا سنكون قد قمنا بعمليه التخاطب بين الـ Threads باستخدام هذه الدوال الثلاثه ، وللعلم هذه الدوال تعمل فقط داخل الدوال التي تكون synchronized فقط . والداله wait تولد IntrruptedException .

الداله wait :

تقوم بتوقف sleep الـ Thread الحالي ، الى أن يقوم الـ Thread الذي بدأ بالعمل حاليا بعمليه notify حتى يوقظ الـ Thread الأول .

الداله notify :

تقوم بإيقاظ الـ Thread الذي قمنا بعمل wait له من نفس الكائن

الداله notifyAll :

تقوم بإيقاظ جميع الـ Threads الذي قمنا بعمل wait لهم من نفس الكائن ، ويعمل من له أعلى أولويه High Prioiry .

مثال لمشكله الـ Producer-Consmer ، وفي المثال سوف نقوم بعمليه set و get للرقم 100 مره فقط .

//Example For Producer-Consumer Problem 

public class ThreadDemo
{
public static void main (String args[])
{
Data data = new Data();

Producer p = new Producer(data);
Consumer c = new Consumer(data);

p.start();
c.start();

try
{
p.join();
c.join();
}
catch ( InterruptedException e)
{ e.printStackTrace(); }

System.out.println("End Of Program");
}
}

class Data
{
private int number;
private boolean state = false;

public synchronized void setData (int d)
{
if ( state )
{
try
{
wait();
}
catch (InterruptedException e)
{ e.printStackTrace(); }
}

number = d;
System.out.println("Set : " + number );
state = true;
notify();
}

public synchronized void getData ()
{
if ( ! state )
{
try
{
wait();
}
catch (InterruptedException e)
{ e.printStackTrace(); }

}

System.out.println("Get : " + number );
state = false;
notify();
}
}

class Producer extends Thread
{
private Data d;
private int count = 100;

public Producer (Data d)
{
this.d = d;
}

public void run ()
{
int i = 0;

while ( count-- > 0)
d.setData(++i);
}
}

class Consumer extends Thread
{
private Data d;
private int count = 100;

public Consumer (Data d )
{
this.d = d;
}

public void run ()
{
while ( count-- > 0 )
d.getData();
}
}

والمخرج سوف يكون بهذا الشكل :

Set : 1

Get : 1

Set : 2

Get : 2

Set : 3

Get : 3

Set : 4

Get : 4......... الخ

Controlling Thread :

================

أي مبرمج مخضرم بالجافا وأستخدم java 1.1 سوف يكون متأثر جدا بالأسلوب القديم الذي كان يستخدم لكي يتحكم في عمل الـ Thread ، حيث كان يقوم بتوقيف الـ Thread الى أن يقوم بتشغيله هو مره أخرى . وكانت داله التوقيف المؤقت هذه هي suspend ، أما داله التشغيل فهي resume ، وكانت هناك داله ثالثه هي stop تستخدم لتوقف الـ Thread نهائيا من العمل ولن يمكن أن نقوم بتشغيله resume بعد أستخدام هذه الداله stop .

المشكله أنه هذه الدوال خطره وقد تسبب مشاكل ، ولهذا قامت sun بالغاء هذه الدوال Deprecated it . نأخذ مثال يوضح خطورتها ، مثلا قمت باستخدام الداله suspend في أثناء العمل في داله synchronized ، وكانت هناك Thread أخرى تريد الوصول الى هذه الداله ، اذاً مباشره سوف نكون وقعنا في Deadlock . الداله resume لا تشكل خطرا ، لكنها لا تستخدم الا مع suspend لذلك تم الغائها هي أيضا . أما الداله stop فهي أيضا تسبب مشاكل ، تخيل كنت تكتب في LinkedList أو مصفوفه ، وفي منتصف عمليه الكتابه أو الترتيب تقوم بتوقف هذا الـ Thread ، وهو مايسبب مشكله في هذه البيانات وربما تفقدها أيضا .

Java 2 قدمت الحلول ، باستخدام دوال الـ Commuincation التي ذكرناها في الفقره السابقه ، وسوف نأخذ مثال نقوم بكتابته بالأسلوب القديم ، بعدها نقوم بتحويله بعد دوال الـ Commuincation .

في المثال ، سوف نشغل مسارين مع بعض ، وفي كل مره نقوم بايقاف أحدهم suspend بعدها تشغيله resume .

// Java 1.1 Method for Controlling Thread

public class ThreadDemo
{
public static void main (String args[])
{
MyThread t1 = new MyThread("First");
MyThread t2 = new MyThread("Second");

t1.start();
t2.start();

try
{
Thread.sleep(1000);
t1.suspend();
System.out.println("Suspend : " + t1.getName() );
Thread.sleep(1000);
t1.resume();
System.out.println("Resume : " + t1.getName() );

t2.suspend();
System.out.println("Suspend : " + t2.getName() );
Thread.sleep(1000);
t2.resume();
System.out.println("Resume : " + t2.getName() );
}
catch (InterruptedException e)
{ e.printStackTrace(); }

try
{
System.out.println("\n\nMain Wait Threads To Finish");
t1.join();
t2.join();
}
catch (InterruptedException e)
{ e.printStackTrace(); }

System.out.println("Main End");
}
}

class MyThread extends Thread
{
public MyThread ( String s)
{
super(s);
}

public void run ()
{
try
{
for (int i=15; i>0; i--)
{
System.out.println( getName() + " : " + i );
sleep(200);
}
}
catch (InterruptedException e)
{ e.printStackTrace(); }
}
}

عند الترجمه سنلاحظ بأن المترجم يحذرنا ، حيث أننا نستخدم داول deprecated

Note: ThreadDemo.java uses or overrides a deprecated API.

Note: Recompile with -Xlint:deprecation for details.

الأن نقوم بتحويل هذا الكود "المغبر" والذي عفا عليه الدهر ، والنقطه التي يجب أن نوضحها هي أنه دائما نستخدم متغير نطلق عليه flag من نوع boolean ، وفي كل مره نقوم في run باختبار هذا المتغير ، فاذا كان true فأقوم بعمل wait للـ Thread . والداله mySuspend تضع الـ flag بـ true . أما الداله myResume تضع الـ flag ب false وتقوم بتشغيل الThread النائم .

// Java 2  Method for Controlling Thread

public class ThreadDemo
{
public static void main (String args[])
{
MyThread t1 = new MyThread("First");
MyThread t2 = new MyThread("Second");

t1.start();
t2.start();

try
{
Thread.sleep(1000);
t1.mySuspend();
System.out.println("Suspend : " + t1.getName() );
Thread.sleep(1000);
t1.myResume();
System.out.println("Resume : " + t1.getName() );

t2.mySuspend();
System.out.println("Suspend : " + t2.getName() );
Thread.sleep(1000);
t2.myResume();
System.out.println("Resume : " + t2.getName() );
}
catch (InterruptedException e)
{ e.printStackTrace(); }

try
{
System.out.println("\n\nMain Wait Threads To Finish");
t1.join();
t2.join();
}
catch (InterruptedException e)
{ e.printStackTrace(); }

System.out.println("Main End");
}
}

class MyThread extends Thread
{
boolean flag = false;

public MyThread ( String s)
{
super(s);
}

public void run ()
{
try
{
for (int i=15; i>0; i--)
{
System.out.println( getName() + " : " + i );
sleep(200);

synchronized(this)
{
while (flag)
wait();
}
}
}
catch (InterruptedException e)
{ e.printStackTrace(); }
}

public void mySuspend ()
{
flag = true;
}

public synchronized void myResume ()
{
flag = false;
notify();
}

}

الناتج بالضبط هو نفسه المخرج السابق ... وباستخدام الطريقه الأفضل . وإن كانت الطريقه القديمه هي "أوضح" قليلا من هذه الطريقه . ربما sun تورطت ولم تجد الا هذا المخرج :) .

Playing With Multithreading :

======================

لنأخذ مثال عملي ، على برنامج لكشف باسورد مكون من 4 خانات ، كل خانه تتكون من الأرقام من 0 - 9 . الخوارزميه هنا بسيطه للغايه ، حيث سنطبق مبدأ الـ Brute-Force ونقوم باختبار باسورد باسورد بدأ من 0000 و 0001 وانتهائا بـ 9999 . وفي كل مره نأخذ العدد نقوم بالتحقق هل هذا العدد يساوي الباسورد فاذا كان كذلك فنقوم بالتوقف ونطبع العدد (الباسورد) والا سوف نستمر بالعدد التالي ، وهكذا .

طبعا ستبدأ المشاكل في حال كبرت خانات العدد ، وأصبحت مثلا 10 خانات ، وسوف يأخذ وقتا طويلا للغايه ، حتى يقوم بمعرفه الباسورد ، أضافه في حال أردنا أن تكون الخانات عباره عن أرقام وحروف وليس أرقام فقط . سنحاول الأن أن نتطرق الى مشكله البطئ في حال كبرت خانات العدد ، ونحاول حلها باستخدام ما يعرف بـ Thread ، أي سنقوم بتشغيل مسار يبحث ضمن مجموعه معينه ، ومسار أخر ضمن مجموعه معينه ، وفي حال توصل أي مسار الى الحل ، نقوم بطباعه الباسورد .

الميزه هنا باستخدام هذه الطريقه ، أنه يمكن أن نصل للحل في ثانيه واحده فقط ، بينما بدون استخدام الThread قد نتتظر ساعات وأيام أيضا (في حال كبرت الخانات) . نقوم بتوضيح الصوره بشكل أكبر ، مثلا نريد البحث عن باسورد مكون من 5 خانات ، أي موجود في المدى 00000 الى 99999 ، الأن باستخدام الThread سوف نقوم بعمل Thread يبحث من البدايه الى منتصف العدد 99999 ، ونقوم بعمل Thread أخر يقوم بالبحث من 99999 الى منتصف 99999 ، وهكذا عندما نبدأ البرنامج سوف يعمل واحد من البدايه والثاني من النهايه .

في حال كان الباسورد هو مثلا 5 سوف نجده في أقل من ثانيه ، وفي حال كان 99998 سوف نجده أيضا في أقل من ثانيه ، لأنه يوجد لدى مسار يعمل من النهايه (في حال لم يكن هناك Thread سوف ننتظر الى أن يصل العدد الى هذه المرحله ، تخيل لو كانت الخانات عشره 9999999999999 سوف ننتظر طويلا جدا جدا .

لكن هذه الطريقه ليست الطريقه الأكمل ، فلو أخذنا العدد 55555 (منتصف العدد 99999 ، بشكل تقريبي) ، المهم ، سوف نأخذ وقتا أطول من الوقت الذي سوف نستغرقه في حال لم نستخدم Thread .

مثلا : الباسورد مكون من 5 خانات ،اذا هو محصور بين : 99999 ، الان في حال أستخدمت الطريقه الأولى (من غير مسار) وكان الباسورد هو : 44444 فسوف نصل للحل في 10 ثواني . أما لو استخدمنا الـ Thread فسوف نصل للحل في 49 ثانيه .

نستنتج من ذلك ، أننا لو استخدمنا الـ Thread واحد يبدأ من النهايه وأخر من البدايه سوف نصل للحل بسرعه في حال كان الباسورد موجود في الأطراف ، أما اذا كان في الوسط ، فسوف يأخذ الكثير من الوقت .

ولنحسن من الحل ، ما رأيك من البحث من البدايه والنهايه ومن المنتصف أيضا ؟ ولكن في المنتصف يجب أن نقرر هل نذهب الى اليمين أم الى اليسار ؟ فلو ذهبنا الى اليسار نحو 99999 فربما يكون الباسورد هو أقل من المنتصف ونجلس زمنا طويلا الى أن يتحرك المسار في البدايه الى ذلك الباسورد ، ونفس الأمر في حال ذهبنا نحو اليمين باتجاه الصفر .

اذا أفضل حل (ولكنه سيبطئ كثيرا خاصه في عمليه الـ Context-Switch ) هو البحث من المنتصف في الأتجاهين اليمين واليسار .

اذا لدينا 4 مسارا تThread واحد من البدايه 0 الى المنتصف ، وأخر من المنتصف الى النهايه ، والثالث من المنتصف الى البدايه ، والأخير من النهايه الى المنتصف .

دعنا الأن نقارن الزمن باستخدام هذه الطريقه ، مع الطريقه التي لم نستخدم في الـ Thread ، ومع الطريقه التي استخدمنا في مسارين .

عدد الخانات هي 5 . اذا المجال من 00000 الى 99999 ، الان سوف نعرض عده باسوردات ونرى مدى سرعه كل خوارزميه في الوصول للحل مع ذلك الباسورد .

الباسورد : 40

Without Thread الزمن 0.031

Using 2-Thread الزمن 0.62

Using 4-Thread الزمن 0.172

الباسورد : 7985

Without Thread الزمن 3.171

Using 2-Thread الزمن 9.766

Using 4-Thread الزمن 22.063

الباسورد : 99988

Without Thread الزمن 18.765

Using 2-Thread الزمن 0.15

Using 4-Thread الزمن 0.31

الباسورد : 79865

Without Thread الزمن 10.219

Using 2-Thread الزمن 8.344

Using 4-Thread الزمن 32.359

الباسورد : 55555

Without Thread الزمن 7.016

Using 2-Thread الزمن 44.734

Using 4-Thread الزمن 15.703

ليس هناك شيء متكامل اليس كذلك :) ، المهم باستخدام 4-Thread لن تكون النتيجه جيده الا اذا كانت النتيجه قريبه جدا من البدايه أو النهايه أو المنتصف ، والا فالخوارزميه سوف تكون من أسوأ مايكون حيث أن الـ Context-Swtching بين هذه الـ Thread سوف يأخذ زمنا أطول من زمن المعالجه وهو أمر سيء للغايه ، أما اذا أستخدمنا Using 2-Thread فإن النتيجه تكون سريعه جدا في حال كانت النتيحه في الأطراف ، أما اذا كانت في المنتصف فسوف تكون سيئه هي الأخرى ، Without Thread اذا كانت النتيجه في الشق الأول لأنها تتحرك للأمام فقط فالنتيجه جيده ، والا فسوف تكون سيئه ويفضل استخدام 4-Thread اذا كانت النتيجه قريبه من المنتصف أو النهايه أو 2-Thread اذا كانت قريبه من النهايه .

Java Code - Without Thread (one Thread Only)

// Crack Password - First Version
// using Brute-Force Method

// we move from 0 to 10000 and check every digit with password

public class PasswordCrack1
{
public static void main (String args[])
{
double start = System.currentTimeMillis();

Cracking crk = new Cracking(99999);
crk.start();

try
{
crk.join();
}
catch ( Exception e)
{ e.printStackTrace(); }


double end = System.currentTimeMillis();
double time = ( end - start) / 1000;
System.out.println("Cracking Taking : " + time + " Second ");
}
}


class Cracking extends Thread
{
private int password = 79865;

private int start;
private int end;

public Cracking (int pass )
{
start = 0;
end = pass;
}

public void run()
{
for (int i=start; i<= end; i++)
{

System.out.println( this.getName() + " " );

if ( checkPassword(i) )
{
System.out.println("Cracking Compelete , Password is : " + i );
return;
}
}
}

public boolean checkPassword (int pass )
{
System.out.print("Check : " + pass + " : " );

if ( pass == password )
{
System.out.println(" True");
return true;
}
else
{
System.out.println(" False");
return false;
}
}
}

Using 4-Thread And 2-Thread

Remove the comment if You want 4-Thread

// Crack Password - using Brute-Force Method

public class PasswordCracking
{

public static void main (String args[])
{
double start = System.currentTimeMillis();

Cracking crk = new Cracking(99999,Cracking.START);
crk.start();

Cracking crk2 = new Cracking(99999,Cracking.END);
crk2.start();

// Cracking crk3 = new Cracking(99999,Cracking.M_LEFT);
// crk3.start();

// Cracking crk4 = new Cracking(99999,Cracking.M_RIGHT);
// crk4.start();

try
{
crk.join();
crk2.join();
// crk3.join();
// crk4.join();
}
catch ( Exception e)
{ e.printStackTrace(); }


double end = System.currentTimeMillis();
double time = ( end - start) / 1000;
System.out.println("Cracking Taking : " + time + " Second ");

}
}


class Cracking extends Thread
{
private int number;
private int path ;
private int password = 79865 ;
private static boolean state = false;

private int start;
private int end;

public static final int START = 1;
public static final int END = 2;
public static final int M_LEFT = 3;
public static final int M_RIGHT = 4;

public Cracking (int pass , int a )
{
path = a;
number = pass;

if ( path == START )
{
start = 0;
end = pass / 2;
}

else if (path == M_LEFT )
{
start = pass / 2;
end = pass;
}

else if (path == M_RIGHT )
{
start = pass / 2;
end = 0;
}

else if (path == END )
{
start = pass;
end = pass / 2;
}
}

public void run()
{
if ( path == START )
{
for (int i=start; i<= end; i++)
{
if ( state )
return;

System.out.println( this.getName() + " " );

if ( checkPassword(i) )
{
state = true;
System.out.println("Cracking Compelete , Password is : " + i );
return;
}

yield();
}
}

else if ( path == M_LEFT )
{
for (int i=start; i<= end; i++)
{
if ( state )
return;

System.out.println( this.getName() + " " );

if ( checkPassword(i) )
{
state = true;
System.out.println("Cracking Compelete , Password is : " + i );
return;
}

yield();
}
}

else if ( path == M_RIGHT )
{
for (int i=start; i>= end; i--)
{
if ( state )
return;

System.out.println( this.getName() + " " );

if ( checkPassword(i) )
{
state = true;
System.out.println("Cracking Compelete , Password is : " + i );
return;
}

yield();
}
}

else if ( path == END )
{
for (int i=start; i>= end; i--)
{
if ( state )
return;

System.out.println( this.getName() + " " );

if ( checkPassword(i) )
{
state = true;
System.out.println("Cracking Compelete , Password is : " + i );
return;
}

yield();
}
}

}

public boolean checkPassword (int pass )
{
System.out.print("Check : " + pass + " : " );

if ( pass == password )
{
System.out.println(" True");
return true;
}
else
{
System.out.println(" False");
return false;
}
}
}

Resource :

========

java 2 Complete Refernce

Introduction to network programming in java

java 1.5 Documenation

java tutorial

شكرا لوصولك نقطه النهايه بأمان :) :) .

Wajdy Essam

0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه

7 إجابة على هذا السؤال .

  • 0

بارك الله فيك أيها الغالي , فعلا مواضيعك تستحق القراءة و المتابعة ....

الف الف الف تحية إليك أخي رومانسي .

0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

صدقني انو انا زعلت منك اخ وجدي :D

تعرفنا ما نقدر نقاوم دروسك :wub:

وضعته بالوقت الحساس هذا وقت الامتحانات ..

الف شكر الك ...صدقني لن ابتعد عن الدرس كثيرا ..

جزاك الله كل خير ..

الله معك.

0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

درس رائع وجاء في وقته كما قال الاخ shado .

. جزاك الله خير .

جاءني سؤال في الاختبار عن الدالة Join وفائدتها .. طبعا لم اجيب عنه ... قهر <_< .

تم تعديل بواسطه الشمري
0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

جزاك الله خيراً شرح ممتع وبسيط

0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

http://modonat-alaa.freehostia.com/?p=54

http://modonat-alaa.freehostia.com/?p=64

http://modonat-alaa.freehostia.com/?p=83

هذه مقالات كتبتها عن موضوع الخيوط thread أو التزامن synchronized

بالطبع لا تأتي شيء مقابل ما وضعه الأخ رومانسي

لكني أردت أن أشارككم لا أكثر ولا أقل

تحياتي

0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

السلام عليكم ورحمة الله وبركاتة

دعني اولا اشكر على الموضوع الله يعطيك العافية بالنسبة لي yield(); ما الفائده منة او ماهي وظيفته.

وشكرا.

0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

موضوع رائع بكل مت تحمله الكلمة من معنى

بارك الله فيك اخ وجدي

دمت ذحرا للمنتدى وأطال الله بعمرك

0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه

من فضلك سجل دخول لتتمكن من التعليق

ستتمكن من اضافه تعليقات بعد التسجيل



سجل دخولك الان

  • يستعرض القسم حالياً   0 members

    لا يوجد أعضاء مسجلين يشاهدون هذه الصفحة .