الحوسبة المتوازية
مقالة رائجة

البرمجة المتوازية بين المعالجة المتعددة والخيوط

شرح بسيط ومدخل قوي للبرمجة المتوازية في بايثون

ظهر أول تطبيق لفكرة الحوسبة المتوازية في سبعينيات القرن الماضي على يد مختبرات لورانس ليفرمور ، وبتمويل من سلاح الجو الأمريكي سنة 1964 ، و كان إسم المشروع آنذاك ” حاسوب 4 ILLIAC ” حيث كان يحتوي على 256 معالجا ، وسُمي “أكثر الحواسيب العملاقة غموضاً” ، وقد استغرق تطويره 11 سنة مع تكلفة بلغت 31 مليون دولار ، وحين جاء موعد تشغيله سنة 1976 كانت الشركات الخاصة قد أنتجت وطورت حواسيب جديدة أقوى من 4 ILLIAC ، فتم التخلي عن المشروع ، غير أن فكرة تطوير الحوسبة المعددة أو البرمجة المتوازية ستبقى مسيطرة على الأوساط التقنية إلى يومنا هذا .

مفهوم التوازي

من البديهي في عصر السرعة –العصر الحالي- أن تكون معظم التطبيقات العملية الحديثة تتطلب سرعة تنفيذ عالية ، مما دَفع شركات تصنيع العتاد الحوسبي إلى إنتاج حاسبات مجهزة بمُعالِجات متعددة النواة ، فظهرت المعالجات ثنائية النواة (dual-core) من ” إنتل” وتلتها مجموعة من المعالِجات التي جعلت من تعدد الأنوية قاعدة مهمة في عملية التطوير .
وهذا الإتجاه نحو المعالجات متعددة النواة جعل مفهوم البرمجة المتوازية هو الأساس . خاصة بعد ظهور تقنيات حديثة تتطلب الأداء الأمثل، مثل تقنية الذكاء الصنعي و تقنية بوابات الدماغ-كومبيوتر .

يستخدم مفهوم البرمجة المتوازية عادة كمرادف لمصطلح المعالجة المتوازية عند المبرمجين .

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

المعالجة المتسلسلة ( Serial Computing )

أو البرمجة ذات التنفيذ التسلسلي للتعليمة ، وهي أعرق برادايمات اليرمجة حيث ظهرت مع ظهور أول لغة برمجة في التاريخ  ( لغة Plankalkül )  ومازال هذا النمط التسلسلي يستخدم إلى يومك هذا ، وهو النمط التقليدي الذي تعلمناه واعتدناه ، ومفاده أن الكود ينُفذ سطراً بسطرِ ، من الأعلى إلى الأسفل بشكل متسلسل . مثال :

A = 20 * 20
B = 40 * 40
C = A * B
Print ( C )

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

المعالجة المتوازية ( Parallel Computing )

تخيل أنك تقف أمام محل للوجبات السريعة وتنتظر في طابور طويل للحصول على سندويش شاورما -أو أي شيء تفضله أنت- ، وترتيبك في الطابور هو 12 ، العامل بالمحل يستغرق 4 دقائق لتحضير كل طلبية ، فكم يلزمك من الوقت للحصول على سندويشك : مع العلم أن عدد عمال الطلبيات في المحل هو عامل واحد ؟
الجواب ببساطة هو المعادلة التالية :
ترتيبك في الطابور * وقت تجهيز الطلبية / عدد عمال الطلبيات
في حالتنا هذه :
12 * 4 / 1 = 48 دقيقة .
لا أحد يفضل أن ينتظر 48 دقيقة للحصول على غدائه ، خاصة لو كان جائعا ، إذن الحل في هذه الحالة هو زيادة عدد عمال الطلبيات إلى 4 عمال ولنسميها الحالة (2) :
12 * 4 / 4 = 12 دقيقة
إذن انخفضت مدة الإنتظار من 48 الى 12 دقيقة ، وهي مدة انتظار مقبولة خاصة إذا كان السندويش يستحق ، هذا هو مبدأ المعالجة المتوازية باختصار : كلما قسمنا العمل على عدد أكبر من أنوية المعالجة كلما صارت مدة الإنتظار أقل .
إذن الشرط الأساسي هنا هو تقسيم المشكلة إلى أجزاء ثم تنفيذها بالتوازي ؛ هنا يأتي دور البرمجة المتوازية التنفيذ .

ما هي البرمجة المتوازية ( Parallel programming ) ؟

البرمجة المتوازية في أبسط تعريفاتها هي الإستخدام المتزامن لموارد العتاد البرمجي من CPU و GPU وغيرهما لتنفيذ كود أو شفرة برمجية معينة عبر تقسيمها إلى أجزاء/تعليمات منفصلة يمكن تنفيذها في وقت واحد .

استخدام البرمجة المتوازية في لغة بايثون

في المثال التالي سنقوم بعمل دالة لتحميل الصور ، وستأخذ متغيرين : الأول URL وهو عنوان الصورة ، الثاني Name وهو الإسم الذي ستحفظ به الصورة في الجهاز :

أولاً : المقاربة التسلسلية (النمط التقليدي)

 

import urllib.request
def Download (URL , FileName):
urllib.request.urlretrieve(URL , FileName)

لنفترض أننا سنستخدم الدالة لتحميل 3 صور :

urls = [
"https://i.pinimg.com/564x/1c/0b/75/1c0b75c9a8e511bf7ea15988b5f6df32.jpg",
"https://i.pinimg.com/564x/f0/47/97/f04797385c24c98db149b752073f7d63.jpg",
"https://i.pinimg.com/564x/9d/eb/aa/9debaac8c3c33953d4ed752842b30c4a.jpg"
]

img_name = 0
for image in urls:
start = time.time()
print ("start" , start)
img_name = img_name + 1
Download (image , str(img_name))
end = time.time() - start
print(end)

قمنا بإنشاء قائمة بعناوين الصور ثم أنشأنا حلقة لاستدعاء الدالة Download لتحميلها ، وهذا هو الوقت المستغرق لكل صورة :

البرمجة المتسلسلة في بايثون
الوقت المستغرق في البرمجة المتسلسلة

ثانيا : المقاربة المتوازية ( المعالجة المتعددة multiprocessing )

سنطبق نفس المثال الأول باستخدام مكتبة multiprocessing لتحميل الصور بشكل متوازي ، أول شيء نقوم باستيراد المكتبات اللّازمة :

 

import urllib.request
from multiprocessing.pool import ThreadPool as Pool
import time
from random import randint

 

إستخدمنا مكتبة time لحساب الوقت المستغرق لتحميل كل صورة ، واستخدمنا مكتبة random لتوليد أرقام عشوائية لتسمية الصور التي سيتم تحميلها (تفاديا للأخطاء) .

بعد استيراد المكتبات سنقوم بتعريف دالة التحميل :

def Download (URL):
start = time.time()
urllib.request.urlretrieve(URL , str(randint(0,444)) + ".jpg")
end = time.time() - start
return end
urls = [
"https://i.pinimg.com/564x/1c/0b/75/1c0b75c9a8e511bf7ea15988b5f6df32.jpg",
"https://i.pinimg.com/564x/f0/47/97/f04797385c24c98db149b752073f7d63.jpg",
"https://i.pinimg.com/564x/9d/eb/aa/9debaac8c3c33953d4ed752842b30c4a.jpg"
]
  • في السطر (6) المغير start لتسجيل وقت بدء التحميل .
  • السطر (7) لتحميل الملف من الأنترنت باستخدام العنوان URL ، وتسميته برقم عشوائي بين 0 و 444 ثم أضفنا نوع الملف ” .jpg ” إليه .
  • السطر (8 ) قمنا بتسجيل الوقت الحالي وطرحنا منه وقد بدء التحميل ، وجعلنا الدالة ترجع وقت تحميل الملف .
    السطر (10) : أضفنا متغير جديد وهو عبارة عن قائمة بعناوين الصور المراد تحميلها .

الآن وصلنا إلى مرحلة تنفيذ الدالة download على محتويات القائمة urls بالتوازي :

pool = Pool(3)
start = time.time()
for result in pool.imap_unordered(Download, urls):
print(result)
end = time.time() - start
print( end)
  • في السطر (15) قمنا بتعريف عدد عمليات التوازي في الـ Pool .
  • السطر (16) : أضفنا المتغير start لتسجيل وقت بداية جميع العمليات .
  • السطر (17) : قمنا باستدعاء نسخة من الدالة download مع كل عنصر من عناصر القائمة urls بالتوازي ، وأعدنا القيم المرجعة من الدالة .
  • السطرين الأخيرين لحساب وقت التنفيذ .
إذن ما هو الوقت المستغرق باستخدام مكتبة multiprocessing :
البرمجة المتوازية باستخدام multiprocessing في بايثون
الوقت المستغرق في البرمجة المتوازية باستخدام multiprocessing

البرمجة المتعددة الخيوط ( Multithreading )

هناك شكل آخر من أشكال التوازي ويُسمى البرمجة الخيطية Multi-Threaded programing ، و أول تطبيق له كان سنة 1967 في نظام التشغيل OS/360 من IBM ، غير أن الحاجة إلى threading لم تظهر بقوة إلا مع ظهور البرمجيات ذات الواجهة الرسومية ، حيث كان – ومايزال – المبرمجون يواجهون مشكلة في تزامن الكود مع الواجهة ، فأثناء التنفيذ وحين يكون اليرنامج يؤدي مهمة معينة تعلق الواجهة الرسومية في انتظار انتهاء هذه المهمة ، لأن النمط التقليدي يفرض تنفيذ المهام بالتسلسل . إذن فكرة الخيوط Threads تتلخص في إطلاق مسارات متعددة للشفرة ومستقلة عن بعض ، حتى يتمكن المبرمج مثلا من جعل مسار الواجهة مستقلا عن مسار العمليات والمهام الأخرى .

 

نقاط قوة الخيوط ( Threads) :

البرمجة الخيطية شاع استخدامها وانتشر لثلاثة أسباب رئيسية :

  1.  تفادي انهيار البرمجية أثناء التنفيذ ، بحيث لا يؤدي انهيار خيط برمجي واحد ومستقل إلى انهيار البرمجية بالكامل .
  2.  إضافة طبقة أمان إضافية للعمليات الحساسة عن طريق إضافة خيوط برمجية لحماية البيانات ، أو توزيع الصلاحيات .
  3.  سهولة الاستخدام .

استخدام Multithreading في لغة بتيثون

سنستخدم نفس المثال السابق ولكن بأسلوب الـ threading ..

أولا استيراد المكتبات وتعريف الدالة وقائمة الـ urls :

import urllib.request
import time
from random import randint
import threading

def Download (URL):
start = time.time()
urllib.request.urlretrieve(URL , str(randint(0,444)) + ".jpg")
end = time.time() - start
print( end)
#timing = timing + end

urls = [
"https://i.pinimg.com/564x/1c/0b/75/1c0b75c9a8e511bf7ea15988b5f6df32.jpg",
"https://i.pinimg.com/564x/f0/47/97/f04797385c24c98db149b752073f7d63.jpg",
"https://i.pinimg.com/564x/9d/eb/aa/9debaac8c3c33953d4ed752842b30c4a.jpg"
]

ثم نقوم بتقسيم المهام على الخيوط :

start = time.time()
for url in urls:
thread = threading.Thread(target=Download, args=(url,))
thread.start()
end = time.time() - start
print( end)

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

شرح استخدام threading في بايثون
الوقت المستغرق في البرمجة الخيطية باستخدام threading بايثون

 

هل الـ threading برمجة متوازية حقا ؟

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

البرمجة المتوازية و حقيقة threading
المصدر : contentsquare-engineering-blog

ما هي محدودية GIL ) global interpreter lock ) :

في بايثون الـ Global-Interpreter-Lock اختصارا GIL ، صراحة لا أدري هل يمكن ترجمتها بالعربي إلى ” قفل المترجم العام” ، هذا القفل موجود في المفسر القياسي لبايثون (cpython) وليس في تصميم اللغة حيث يمنع تنفيذ الخيوط مع بعض بالتوازي ، و يجعلها متسلسلة مع التبديل بينها .

من الأسرع إذن ؟

بعد اختبارات الأداء السابقة يمكن تلخيص التجربة في التالي :

threading vs multiprocessing مقارنة برمجة
threading vs multiprocessing

 

نظرة سريعة على إيجابيات وعيوب المكتبتين multiprocessing و threads

 

مكتبة multiprocessing

نقاط قوة مكتبة الـ multiprocessing

  1. ذاكرة مستقلة لكل عملية .
  2.  الكود بسيط ومفهوم عادة .
  3.  تستفيد من تعدد المعالِجات والأنوية .
  4.  تتجاوز محدودية GIL .
  5.  العمليات الفرعية processes قابلة للتوقف والإستمرار بسلاسة .

نقاط ضعف مكتبة الـ multiprocessing

  1.  التواصل بين العمليات المتوازية أكثر تعفيدا بسبب الذاكرة المستقلة .
  2.  استهلاك أكبر للذاكرة .

مكتبة Threading

نقاط قوة مكتبة الـ Threading

  1. خفيفة وتستهلك ذاكرة أقل.
  2.  الذاكرة المشتركة تضمن تواصلا أفضل بين الخيوط threads .
  3. تسهل إنشاء الواجهات الرسومية المتجاوبة responsive UIs .
  4.  ممتازة في عمليات الـ I/O .

نقاط ضعف مكتبة الـ Threading

  1.  محدودية GIL .
  2.  قابلية القراءة في الكود تزداد تعقيدا مع تفرع الخيوط .
  3.  الخيوط حساسة وقابلة للإنهيار إذا حدث خلل في الذاكرة .

كلمات أخيرة عن التوازي ..

إنّ دمجك لتقنيات التوازي في البرمجة لا يعني بالضرورة أداءً وسرعة أفضل لتطبيقاتك ، لأن الأمر يتوقف على طريقة تطبيقك للتقنية وأسلوب كتابتك للكود ، فهناك دائما طرق أبسط وأقل كلفة للوصول إلى الهدف المتوقع من التطبيق ، وأفضلها على الإطلاق : تقنية البرمجة اللّامتزامنة .

شكر خاص للأستاذ فهد السعيدي مؤسس موقع وادي التقنية الذي اعتمدنا مقالته مدخل إلى الحوسبة المتوازية كمصدر لأجزاء كثيرة من هذا المقال .

 

الوسوم

‫7 تعليقات

    1. وجزاك كل خير ، أنا أيضا لم يكن لدي الكثير من المعلومات حتى بدأت البحث لكتابة هذه التدوينة مما دفعني لتجربة الأساليب المختلفة ومقارنتها . إذا كان هناك أية مواضيع أخرى تريدون أن نتكلم عنها أخبرونا عنها

      ممتن جدا لتعليقك

  1. الله يعطيك العافية مبدع استمر استفدت كثيرا على الرغم من خبرتي لكن مواضيع كهذه صعب فهمها بالانجليزي والالمام بها من أول مرة

  2. Excellent goods from you, man. I’ve understand your stuff previous
    to and you are just too fantastic. I really like what you’ve acquired here, certainly like
    what you’re stating and the way in which you say it. You make it enjoyable and you still care for to keep it wise.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

زر الذهاب إلى الأعلى
إغلاق