لماذا ومتى وكيفية استخدام خاصية المعالجة المتعددة والمعالجة المتعددة في بايثون

تحية ، خابروفسك. في الوقت الحالي ، قامت OTUS بفتح مجموعة لدورة التعلم الآلي ، فيما يتعلق بهذا قمنا بترجمة واحدة لك "قصة خرافية" مثيرة للاهتمام. اذهب.




ذات مرة ، في مجرة ​​بعيدة وبعيدة ...

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

الفصل الأول: أحادي الخيوط ، معالجة مفردة


إذا لم تكن قد خمنت بعد ، فقد رسمت تشابهًا مع المعالج ووظائفه. معالجنا هو معالج ، والتمرير عبارة عن قائمة بالروابط التي تؤدي إلى قوة Python ومعرفتها لإتقانها.

كانت الفكرة الأولى عن المعالج الذي قام بفك رموز القائمة دون أي صعوبة هو إرسال صديقه المخلص (Garrigorn؟ أعلم ، أعرف أن ذلك يبدو فظيعًا) إلى كل من الأماكن التي تم وصفها في التمرير للعثور على ما يمكن أن يجده هناك.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [3]:
%%time

results = []
for url in urls:
    with urllib.request.urlopen(url) as src:
        results.append(src)

CPU times: user 135 ms, sys: 283 µs, total: 135 ms
Wall time: 12.3 s
In [ ]:

كما ترى ، نحن ببساطة نكرر عناوين URL واحدًا تلو الآخر باستخدام حلقة for ونقرأ الإجابة. بفضل ٪٪ من الوقت وسحر IPython ، يمكننا أن نرى أن الأمر استغرق حوالي 12 ثانية مع الإنترنت المحزن.

الفصل الثاني: تعدد المواضيع


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

هذا صحيح ، بدلاً من عرض القائمة في حلقة متتالية ، يمكننا استخدام مؤشرات متعددة للوصول إلى عناوين URL متعددة في وقت واحد.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [4]:
%%time

with ThreadPoolExecutor(4) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 8.27 ms, total: 130 ms
Wall time: 3.83 s
In [5]:
%%time

with ThreadPoolExecutor(8) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 14.7 ms, total: 137 ms
Wall time: 1.79 s
In [6]:
%%time

with ThreadPoolExecutor(16) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 143 ms, sys: 3.88 ms, total: 147 ms
Wall time: 1.32 s
In [ ]:

أفضل بكثير! تقريبا مثل ... السحر. يمكن أن يؤدي استخدام سلاسل رسائل متعددة إلى تسريع العديد من مهام الإدخال / الإخراج. في حالتي ، يرجع معظم الوقت المستغرق في قراءة عناوين URL إلى وقت استجابة الشبكة. تقضي البرامج المرتبطة بإدخال / إخراج معظم حياتها في الانتظار ، كما توقعت ، من أجل الإدخال أو الإخراج (تمامًا مثلما ينتظر المعالج أصدقاءه للذهاب إلى أماكن من التمرير والعودة). يمكن أن يكون هذا الإدخال / الإخراج من الشبكة أو قاعدة البيانات أو الملف أو من المستخدم. عادةً ما يستغرق مثل هذا الإدخال / الإخراج الكثير من الوقت ، لأن المصدر قد يحتاج إلى إجراء معالجة مسبقة قبل نقل البيانات إلى I / O. على سبيل المثال ، سيحسب المعالج أسرع بكثير من اتصال الشبكة بالبيانات (بسرعة تقريبًامثل فلاش مقابل جدتك).

ملاحظة : multithreadingيمكن أن يكون مفيدًا جدًا في مهام مثل تنظيف صفحات الويب.

الفصل الثالث: المعالجة المتعددة


مرت السنين ، نمت شهرة الساحر الجيد ، ومعها نما حسد ساحر واحد محايد (Sarumort؟ أو ربما Volandeman؟). مسلحًا بمكر لا حصر له مدفوعًا بالحسد ، يلقي الساحر المظلم لعنة رهيبة على دمبلدالف. عندما تفوقت عليه اللعنة ، أدرك دمبلدالف أنه لم يكن لديه سوى بضع لحظات لصده. يائسة ، قام بتفتيش كتبه الإملائية ووجد بسرعة نوبة مضادة من المفترض أن تعمل. كانت المشكلة الوحيدة هي أن المعالج كان بحاجة إلى حساب مجموع الأعداد الأولية الأقل من 1.000.000. نوبة غريبة بالطبع ، ولكن ما لدينا.

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

In [1]:
from multiprocessing import Pool
In [2]:
def if_prime(x):
    if x <= 1:
        return 0
    elif x <= 3:
        return x
    elif x % 2 == 0 or x % 3 == 0:
        return 0
    i = 5
    while i**2 <= x:
        if x % i == 0 or x % (i + 2) == 0:
            return 0
        i += 6
    return x
In [17]:
%%time

answer = 0

for i in range(1000000):
    answer += if_prime(i)

CPU times: user 3.48 s, sys: 0 ns, total: 3.48 s
Wall time: 3.48 s
In [18]:
%%time

if __name__ == '__main__':
    with Pool(2) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 114 ms, sys: 4.07 ms, total: 118 ms
Wall time: 1.91 s
In [19]:
%%time

if __name__ == '__main__':
    with Pool(4) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 99.5 ms, sys: 30.5 ms, total: 130 ms
Wall time: 1.12 s
In [20]:
%%timeit

if __name__ == '__main__':
    with Pool(8) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

729 ms ± 3.02 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [21]:
%%timeit

if __name__ == '__main__':
    with Pool(16) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

512 ms ± 39.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [22]:
%%timeit

if __name__ == '__main__':
    with Pool(32) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

518 ms ± 13.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [23]:
%%timeit

if __name__ == '__main__':
    with Pool(64) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

621 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [ ]:

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

كل ما عليك القيام به هو:

  1. تحديد الوظيفة المطلوب تطبيقها.
  2. إعداد قائمة بالعناصر التي سيتم تطبيق الوظيفة عليها ؛
  3. multiprocessing.Pool. , Pool(), . with , .
  4. Pool map. map , , .

ملاحظة : يمكن تعريف دالة لأداء أي مهمة يمكن القيام بها بالتوازي. على سبيل المثال ، قد تحتوي دالة على رمز لكتابة نتيجة عملية حسابية إلى ملف.

فلماذا نحن بحاجة إلى فصلmultiprocessingوmultithreading؟ إذا كنت قد حاولت في أي وقت تحسين أداء مهمة على المعالج باستخدام مؤشرات الترابط المتعددة ، فإن النتيجة هي العكس تمامًا. هذا رهيب فقط! دعونا نرى كيف حدث ذلك.

تمامًا كما أن المعالج محدود بطبيعته البشرية ويمكنه حساب رقم واحد فقط لكل وحدة زمنية ، يأتي Python مع شيء يسمى Global Interpreter Lock (GIL) . يسعد Python السماح لك بتفرخ العديد من الخيوط كما تريد ، ولكن GILيضمن تنفيذ واحد فقط من هذه المواضيع في أي وقت.

بالنسبة للمهمة المتعلقة بـ I / O ، هذا الوضع طبيعي تمامًا. يرسل أحد سلاسل الرسائل طلبًا إلى عنوان URL واحد وينتظر الرد ، وعندئذ فقط يمكن استبدال هذا الموضوع بآخر ، والذي سيرسل طلبًا آخر إلى عنوان URL مختلف. نظرًا لأن مؤشر الترابط لا يحتاج إلى القيام بأي شيء حتى يتلقى استجابة ، فلا فرق في أنه في الوقت المحدد يتم تشغيل مؤشر ترابط واحد فقط.

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

للتغلب على هذا "التقييد" ، نستخدم الوحدةmultiprocessing. بدلاً من استخدام مؤشرات الترابط ، تستخدم المعالجة المتعددة ، كما تقول ... عدة عمليات. كل عملية تحصل على مترجمها الخاص ومساحة الذاكرة الخاصة بها ، لذلك لن يقيدك GIL. في الواقع ، ستستخدم كل عملية نواة المعالج الخاصة بها وستعمل مع رقمها الفريد الخاص بها ، وسيتم تنفيذ ذلك في وقت واحد مع عمل العمليات الأخرى. كم هو لطيف منهم!

قد تلاحظ أن حمل وحدة المعالجة المركزية سيكون أعلى عند استخدامه multiprocessingمقارنةً بالحلقة العادية للتكرار أو حتى multithreading. يحدث هذا لأن برنامجك لا يستخدم نواة واحدة ، بل عدة نواة. وهذا أمر جيد!

تذكر ذلكmultiprocessingلديها النفقات العامة الخاصة بها لإدارة عمليات متعددة ، والتي عادة ما تكون أكثر خطورة من تكلفة تعدد مؤشرات الترابط. (تفرخ المعالجة المتعددة مترجمين منفصلين وتخصص منطقة ذاكرة خاصة بكل عملية ، لذلك نعم!) أي أنه من الأفضل ، كقاعدة عامة ، استخدام نسخة خفيفة من مؤشرات الترابط عندما تريد الخروج بهذه الطريقة (تذكر المهام المتعلقة بإدخال / إخراج). ولكن عندما يصبح الحساب على المعالج اختناق ، يأتي وقت الوحدة multiprocessing. لكن تذكر أنه مع القوة العظيمة تأتي مسؤولية كبيرة.

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

الفصل الرابع: الخاتمة


  • في المهام المتعلقة I / O ، multithreadingيمكن أن تحسن الأداء.
  • في المهام المتعلقة بالإدخال / الإخراج ، multiprocessingيمكن أن يؤدي أيضًا إلى زيادة الإنتاجية ، ولكن التكاليف ، كقاعدة عامة ، أعلى مما كانت عليه عند الاستخدام multithreading.
  • إن وجود Python GIL يوضح لنا أنه في أي وقت معين في البرنامج ، يمكن تنفيذ مؤشر ترابط واحد فقط.
  • في المهام المتعلقة بالمعالج ، multithreadingيمكن أن يقلل الاستخدام من الأداء.
  • في المهام المتعلقة بالمعالج ، multiprocessingيمكن للاستخدام تحسين الأداء.
  • معالجات رائعة!

هذا هو المكان الذي سوف تنتهي مقدمتنا ل multithreadingو multiprocessingفي بيثون اليوم . اذهب الآن واربح!



"نمذجة COVID-19 باستخدام تحليل الرسم البياني وتحليل البيانات المفتوحة." درس مجاني.

All Articles