مثال لتعدد المهام في فري باسكال/لازاراس multi-threading

تكلمنا في تدوينة سابقة عن تعدد المهام والآن نُريد عمل مثال بلغة فري باسكال بإستخدام أداة التطوير لازاراس.

نقوم بإنشاء مشروع جديد من نوع Application ثم نضع فيه زر ومحرر نصوص TMemo كما في الشكل التالي:

thread1

ثم نكتب الكود التالي في حدث ضغط الزر:

Memo1.Lines.Add('Started at: ' + DateTimeToStr(Now));
Sleep(30000);
Memo1.Lines.Add('Finshed at: ' + DateTimeToStr(Now));

عند تشغيل البرنامج والضغط على الزر نُلاحظ أن البرنامج قد تجمد لمدة ٣٠ ثانية، خلالها لا يمكننا الضغط مرة أخرى على الزر أو الكتابة في المحرر مثلاً. وذلك لأن نظام التشغيل قد أعطاه مهمة رئيسية واحدة فقط، فهو إذا single thread application.

thread2

يتحرر البرنامج ويُصبح قابلاً للتفاعل مرة أخرى بعد إنتهاء الإجراء المرتبط بضغط الزر. وقد قمنا بإستخدام الدالة Sleep والتي تعطل التنفيذ لفترة، فيها محاكاة لتنفيذ أي إجراء يأخذ وقت.

thread3

لإضافة مهمة ثانوية thread في البرنامج لتعمل في الخلفية، نقوم بإنشاء فئة جديدة من النوع TThread في الوحدة الرئيسية فوق تعريف الفورم كالتالي:

TSampleThread = class(TThread)
public
  procedure execute; override;
end;

ثم نقوم بكتابة كود اﻹجراء execute كالتالي:


Sleep(30000);

ثم نقوم بإنشاء كائن من هذه الفئة في كود الزر كالتالي:


procedure TForm1.Button1Click(Sender: TObject);
var
  MySample: TSampleThread;
begin
  Memo1.Lines.Add('Started at: ' + DateTimeToStr(Now));
  MySample:= TSampleThread.Create(False);
end;

عند تشغيل البرنامج هذه المرة نُلاحظ أن لا يتجمد، بل يُمكننا الضغط على الزر أكثر من مرة قبل نهاية اﻹجراء السابق، لكن لا نعرف إذا ما إنتهى اﻷجراء اﻷول أم لا.

لمعرفة نهاية اﻹجراء نقوم بكتابة حدث وربطه بنهاية المهمة OnTerminate.

لكن قبل ذلك نريد عمل بعض اﻹضافات، وهي تعريف متغير ID لتسجيل رقم المهمة، لمعرفة أي مهمة إنتهت، كذلك نقوم بأجيل التشغيل قليلاً حتى نقوم بتهيأة كل متطلبات المهمة ثم نقوم بتشغيلها بواسطة اﻹجراء Resume.

يصبح كود الوحدة المعدل كالتالي:


unit main;

{$mode objfpc}{$H+}

interface

uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

{ TSampleThread }

TSampleThread = class(TThread)
public
  ID: Integer;
  procedure execute; override;
end;

{ TForm1 }

TForm1 = class(TForm)
  Button1: TButton;
  Memo1: TMemo;

  procedure Button1Click(Sender: TObject);

  private
  { private declarations }
  public
    procedure Finished(Sender: TObject);
  { public declarations }
  end;

var
  Form1: TForm1;

implementation

{ TSampleThread }

procedure TSampleThread.execute;
begin
  Sleep(30000);
end;

procedure TForm1.Finished(Sender: TObject);
begin
  Form1.Memo1.Lines.Add('Thread # ' + IntToStr((Sender as TSampleThread).ID) +
    ' is finished at: ' + DateTimeToStr(Now));
end;

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
var
  MySample: TSampleThread;
begin
  MySample:= TSampleThread.Create(True);
  MySample.ID:= Random(100);
  MySample.OnTerminate:= @finished;
  MySample.Resume;
  Memo1.Lines.Add('Thread # ' + IntToStr(MySample.ID) + ' has started at: '
     + DateTimeToStr(Now));
end;

end.

عند تشغيله يمكننا الضغط أكثر من مرة على الزر لتوليد أكثر من مهمة ثم مراقبة إنتهائها الواحدة بعد اﻷخرى:

thread5

نلاحظ بعد تشغيل هذه المهام الفرعية أن المهمة الرئيسية main thread أيضاً متوفرة حيث يمكن الكتابة في المحرر أثناء اﻹنتظار.

يوجد شيء أخير متعلق بالموارد، وهي أن مكونات لازاراس المعروفة بالـ LCL غير أمنة مع تعدد المهام not thread-safe لذلك يُمكن أن تحدث مشكلة عند محاولة أكثر من مهمة الكتابة في نفس اللحظة، فنقوم بحل ذلك برمجياً بحماية هذا المورد بطريقة تُعرف بالـ synchronization وذلك لأن الكتابة تُعتبر نقطة حرجة critical section.

قُمنا بإضافة إجراء جديد في فئة المهمة أسميناه writeFinish وهو المسؤول عن الكتابة في المورد المشترك (المحرر) وهو يُمثل القسم الحرج critical section، حتى يسنى لنا ندائه بالدالة Synchornize والتي تضمن إصطفاف المهام عند محاولتها الكتابة في وقت واحد في نفس المورد.

يُصبح الكود المعدل كالتالي:


unit main;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

{ TSampleThread }

TSampleThread = class(TThread)
public
  ID: Integer;
  procedure execute; override;
  procedure writeFinish;
end;

{ TForm1 }

TForm1 = class(TForm)
  Button1: TButton;
  Memo1: TMemo;

  procedure Button1Click(Sender: TObject);

  private
  { private declarations }
  public
    procedure Finished(Sender: TObject);
  { public declarations }
  end;

var
  Form1: TForm1;

implementation

{ TSampleThread }

procedure TSampleThread.execute;
begin
  Sleep(30000);
end;

procedure TSampleThread.writeFinish;
begin
  Form1.Memo1.Lines.Add('Thread # ' + IntToStr(ID) +
    ' is finished at: ' + DateTimeToStr(Now));

end;

procedure TForm1.Finished(Sender: TObject);
var
  aThread: TSampleThread;
begin
  aThread := (Sender as TSampleThread);
  aThread.Synchronize(@aThread.writeFinish);
end;

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
var
  MySample: TSampleThread;
begin
  MySample:= TSampleThread.Create(True);
  MySample.ID:= Random(100);
  MySample.OnTerminate:= @finished;
  MySample.Resume;
  Memo1.Lines.Add('Thread # ' + IntToStr(MySample.ID) + ' has started at: '
     + DateTimeToStr(Now));
end;

end.

15 thoughts on “مثال لتعدد المهام في فري باسكال/لازاراس multi-threading

  1. أنا لا استعمل Random في اي من برامجي على الاطلاق، إلا اذا كانت لعبة تحتاج على عدد عشواي، اظن هذا سيشوش القاريء
    استعمل رقم متزايد بكل بساطة

  2. أنا استخدمها بكثرة، خصوصاً في مجال اﻹتصالات وفي التشفير، حيث أن فيها نظام وليست عشوائة مائة بالمائة، فقط ضع رقم في المتغير RandSeed فسيعطيك نفس اﻷرقام بنفس الترتيب كل مرة.
    الحل اﻵخر هو بإستخدام متغير عام global في اﻵونة اﻷخيرة اصبحت أحاول الإقلاع عنه، فهو عادة سيئة في البرمجة.
    هل يُمكنك مساعدتنا في معرفة مفهوم الـ Event delegation في هذا التعليق
    https://abueyas.wordpress.com/about/#comment-2019

  3. >أنا استخدمها بكثرة، خصوصاً في مجال اﻹتصالات وفي التشفير، حيث أن فيها نظام وليست عشوائة مائة بالمائة، فقط ضع رقم في المتغير RandSeed فسيعطيك نفس اﻷرقام بنفس الترتيب كل مرة.

    منذ 25 سنة حدثني احدهم عن هذه الطريقة باستخدام هذا التابع الذي ينتج ارقام عشوائية متسلسلة في التشفير و كان يفتخر بهذا، و ناقشته كثيرا بانه استخدام غير منطقي، بعدها بفتر عندما نقل الخوارزمية إلى حاسب اخر و مترجم اخر اختلفت القيم بالنسبة له و تعرض لمشكلة كبيرة، و اذكر اني قرأت مقالا مؤخرا يتحدث عن الاستخدام الخاطيء لهذه التعليمة لا يحضرني الرابط الآن.
    عندما كان ذلك الشخص يحدثني عن خورازمية التشفير التي استعملها انا كنت افكر كيف ان اخطاء من هذا النوع يتم استغلالها بشكل خاطء و خطر ببالي عمل دارة الكترونيةة تحدث ضجيج عشوائي و تحوله لرقم ديجيتال ترسله للجهاز، او ربما يجب على المعالج كما يوفر ساعة يوفر مزود ارقام عشوائية (، كان هذا قبل 25 سنة.) و يمكن استخدم اشارات الضحيح المحيط من اصوات و حركة فأرة مثلا، هنالك برامج تعمل هذا مثل truecrupt لعمل مفتاح للتشفير.

  4. ايضا استخدام متحول عام ليس خطأ، الاستخدام الخاطيء له هو الخطأ، في حالتك و في مجال تعدد المسارب (الـ threads) تكون صحيحة تماما.
    مع ذلك يمكن وضعه في كحقل في الفورم نفسه.

  5. أنا استخدمه بدون أي مشكلة طوال الستة عشر سنة الماضية، وهي ليست عشوائية بالكامل، بل فيها نظام، يختلف حسب قيمة الـ RandSeed و المدى المستخدم. وقد استخدمتها في أكثر من لغة برمجة. لكن أكثرها كان بإستخدام لغة أوبجكت باسكال بكافة مترجماتها. هل تستطيع إثبات أن مترمج فري باسكال تنتج أرقام عشوائية مختلفة إذا قمنا بتثبيت الـ RandSeed والـ Range في ظروف ترجمة مختلفة، مثلاً قم بترجمته في ٣٢ بت أو ٦٤ بت، وندوز ولينكس، عندها يُمكن أن نُثبت أن هُناك خلل في إستخدام هذه الطريقة في التشفير. أو على اﻷقل هناك مشكلة في مترجم فري باسكال.
    وقد حضرت مرة مناقشة رسالة دكتوراة في توليد أرقام عشوائية لغرض التشفير. وكانت مشكلة توليد اﻷرقام العشوائية (على اﻷقل في أوبجكت باسكال أو دلفي) أنها تميل لتوليد أرقام عشوائية في الجزء اﻷصغر من النطاق.

    >> مع ذلك يمكن وضعه في كحقل في الفورم نفسه.
    أنا أقصد هي متغيرات عامة بالنسبة لمكون الـ thread وليس عام في البرنامج، الهدف أن تكون المتغييرات في اقل مجال لها في اﻹستخدام scope هكذا يكون أفضل، فإذا كان متغير نحتاج له في دالة معينة نقوم بتعريفه داخل الدالة فقط وليس في المكون.
    من اﻷشياء التي أعجبتي في لغة جافا هي إمكانية تضييق نطاق المتغيرات، مثلاً يمكن تعريف متغير أو كائن داخل حلقة بحيث لايمكن الوصول إليه خارجها. فكلما كان المتغيير أو الكائن له حيز أكبر في التعريف كلما كان هُناك إحتمال أكبر لتعرضه لوضع قيمة غير مناسبة فيه. اليوم مثلاً كانت لدي مشكلة في إجراء حاولت حلها لأكثر من نصف ساعة، في النهاية اكتشفت أني قمت بإغلاق الكائن في بداية اﻹجراء وحاولت إستخدامه في نهاية اﻹجراء، فإذا كنت اعدت تعريفه في الجزء اﻷخير فقط لما حدثت هذه المشكلة.

  6. يمكن توليد أرقام عشوائية حقيقة بإستخدام الدالة Random وذلك بتنفيذ اﻹجراء Randomize والذي يقوم بإختيار رقم عشوائي للمتغير RandSeed (بذرة العشوائية) لكن هذه الطريقة لا يُمكن إستخدامها في التشفير، لكن في مجالات أخرى مثل اﻷلعاب كما ذكرت. هذه الطريقة كُنا نسميها باللغة العامية – أقصد عامية المبرمج- بعشوشة العشوائية 🙂

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

  8. بالنسبة للمتحول العام انت تقوم

    MySample.ID:= Random(100);
    

    داخل الفورم لذلك عليك تعريف متحول داخل الفورم نفسه LastID: Integer و استخدامه
    وهو في مجاله (Scope) المناسب هنا.

    TForm1 = class(TForm)
      Button1: TButton;
      Memo1: TMemo;
     
      procedure Button1Click(Sender: TObject);
     
      private
        LastID: Integer;
      public
    
  9. <<طيب جربت تشفر ملف بالباسكال باستخدام rand و قمت بفك تشفيره بلغة C ؟
    لم أجربها، لكن فكرت أنها ربما تكون فيها مشكلة، في هذه الحال يجب الإعتماد على دالة عشوائية محايدة مثل مكتبات التشفير أو الهاش والتي تعطي نفس النتائج في كل اللغات. موضوع العشوائية هو موضوع رياضي وهو توليد أرقام متسلسلة بطريقة منتظمة، ربما سميت عشوائية بالخطأ. وإذا كان هُناك مشكلة في إستخدامها في التشفير، فإن دوال ضغط الملفات ربما تكون مشكلة أيضاً لأن ضغط الملفات تُستخدم فيه طريقة رياضية أيضاً ويجب أن تتوافق فيه لغات البرمجة، كذلك دوال الهاش مثل MD5, SHA1 لابد لكل اللغات أن تعطي كل النتائج، كذلك طُرق التشفير اﻷخرى مثل DES والـ ACE. فكلها يُمكن تطبيق نفس كلامك عليها وإفتراض أنها طرق غير صحيحة من ناحية منطقية، فهي تعتمد على متغيرات مثل طول العدد الصحيح في الجهاز أو لغة البرمجة، وأقل تغيير يؤثر على نتائجها. لكن كل هذه اﻷشياء يُعتمد عليها يومياً في عدد من البرامج والتطبيقات المختلفة.

    <<لا يمكن توليد قيم عشوائية فعلية اذا بالضرورة ان تكون تأتي بنفس التسلسل..
    يُمكن توليد أرقام عشوائية وذلك بإستخدام الموقت الداخلي أو الساعة.

  10. توليد أرقام متسلسلة بطريقة منتظمة له اسم لو قمت بتسمية الدالة بهذا الاسم لما اختلفنا، فعليا تعليمة Random صممت لتقوم بتوليد ارقام عشوائية و ليس “توليد أرقام متسلسلة بطريقة منتظمة”
    كل ما عليك هو نسخ خوارزمية توليد الارقام العشوائية و اعادة تسميتها حتى نتفق لا اكثر

    لا لايمكن توليد ارقام عشوائية فعليا، و لكن يمكن استعمال الساعة او الماوس كمصدر بدء التسلسل، و بعض الاجهزة فعليا لا تملك ساعة (القديمة جدا).

  11. هذه نُسخة البرنامج بدون عشوائية:

    unit main;
    
    {$mode objfpc}{$H+}
    
    interface
    
    uses
      Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;
    
    type
    
      { TSampleThread }
    
      TSampleThread = class(TThread)
        public
          ID: Integer;
          procedure execute; override;
          procedure writeFinish;
      end;
    
      { TForm1 }
    
      TForm1 = class(TForm)
        Button1: TButton;
        Memo1: TMemo;
    
        procedure Button1Click(Sender: TObject);
      private
        LastID: Integer;
      public
        procedure Finished(Sender: TObject);
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    { TSampleThread }
    
    procedure TSampleThread.execute;
    begin
      Sleep(30000);
    end;
    
    procedure TSampleThread.writeFinish;
    begin
      Form1.Memo1.Lines.Add('Thread # ' + IntToStr(ID) +
          ' is finished at: ' + DateTimeToStr(Now));
    
    end;
    
    procedure TForm1.Finished(Sender: TObject);
    var
      aThread: TSampleThread;
    begin
      aThread := (Sender as TSampleThread);
      aThread.Synchronize(@aThread.writeFinish);
    end;
    
    {$R *.lfm}
    
    { TForm1 }
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      MySample: TSampleThread;
    begin
      Inc(LastID);
      MySample:= TSampleThread.Create(True);
      MySample.ID:= LastID;
      MySample.OnTerminate:= @finished;
      MySample.Resume;
      Memo1.Lines.Add('Thread # '
         + IntToStr(MySample.ID) + ' has started at: ' + DateTimeToStr(Now));
    end;
    
    end.
    

أضف تعليقاً

إملأ الحقول أدناه بالمعلومات المناسبة أو إضغط على إحدى الأيقونات لتسجيل الدخول:

WordPress.com Logo

أنت تعلق بإستخدام حساب WordPress.com. تسجيل خروج   / تغيير )

صورة تويتر

أنت تعلق بإستخدام حساب Twitter. تسجيل خروج   / تغيير )

Facebook photo

أنت تعلق بإستخدام حساب Facebook. تسجيل خروج   / تغيير )

Google+ photo

أنت تعلق بإستخدام حساب Google+. تسجيل خروج   / تغيير )

Connecting to %s