عالم بدون كوروتينات. المولد التكراري

1 المقدمة



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



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



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



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



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



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



هناك عدة أسباب لذلك. من بينها الوصول المجاني إلى Python. هذه ليست أقوى حجة ، منذ ذلك الحين مثال مع نفس Qt يقول أن الوضع يمكن أن يتغير في أي وقت. لكن في حين أن Python ، على عكس Kotlin ، مجانية ، على الأقل في شكل بيئة PyCharm نفسها من JetBrains (التي شكرهم عليها بشكل خاص) ، فإن تعاطفي إلى جانبه. من الجذاب أيضًا وجود عدد كبير من الأدب باللغة الروسية ، أمثلة بلغة بايثون على الإنترنت ، تعليمية وحقيقية تمامًا. في Kotlin ، ليسوا بهذا العدد وتنوعهم ليس رائعًا.



ربما قبل المنحنى بقليل ، قررت تقديم نتائج إتقان لغة بايثون في سياق قضايا تعريف وتنفيذ توازي البرامج وعدم التزامن. بدأ هذا من خلال المادة [2]... اليوم سننظر في موضوع المولدات - coroutines. يغذي اهتمامي بهم الحاجة إلى أن أكون مدركًا للإمكانيات المحددة والمثيرة للاهتمام ولكن ليست مألوفة جدًا بالنسبة لي في الوقت الحالي ، وهي إمكانيات اللغات / لغات البرمجة الحديثة.



نظرًا لأنني في الواقع مبرمج C ++ خالص ، فهذا يفسر الكثير. على سبيل المثال ، إذا كانت coroutines والمولدات في Python موجودة لفترة طويلة ، فعندئذٍ في C ++ لم يفوزوا بمكانهم بعد. لكن هل تحتاج C ++ حقًا؟ في رأيي ، يجب توسيع لغة البرمجة بشكل معقول. يبدو أن C ++ قد انقطع قدر الإمكان ، والآن يحاول اللحاق بالركب على عجل. ولكن يمكن تنفيذ مشكلات التزامن مماثلة باستخدام مفاهيم ونماذج أخرى أكثر جوهرية من coroutines / coroutines. وسيتم توضيح حقيقة أن وراء هذا البيان ليست مجرد كلمات.



إذا أردنا الاعتراف بكل شيء ، فأنا أعترف أيضًا أنني متحفظ فيما يتعلق بـ C ++. بطبيعة الحال ، فإن كائناتها وقدراتها OOP هي "كل شيء لدينا" بالنسبة لي ، لكنني أقول ، انتقاد القوالب. حسنًا ، لم أنظر أبدًا إلى "لغة الطيور" الخاصة بهم ، والتي ، كما بدت ، تعقد بشكل كبير إدراك الكود وفهم الخوارزمية. على الرغم من أنني لجأت أحيانًا إلى مساعدتهم ، إلا أن أصابع يد واحدة كافية لكل هذا. أحترم مكتبة STL ولا يمكنني الاستغناء عنها :) لذلك ، حتى من هذه الحقيقة ، لدي شكوك أحيانًا حول القوالب. لذلك ما زلت أتجنبهم بقدر ما أستطيع. والآن أنتظر بقشعريرة "قوالب coroutines" في C ++ ؛)



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



لكن ... العودة إلى coroutines. اتضح أنهم الآن تحت اسم corutin. ما الجديد في تغيير الاسم؟ نعم ، في الواقع لا شيء. كما كان من قبل ، يتم النظر في المجموعة بدورهاأداء المهام. بنفس الطريقة السابقة ، قبل الخروج من الوظيفة ، ولكن قبل الانتهاء من عملها ، يتم إصلاح نقطة العودة ، والتي يتم استئناف العمل منها لاحقًا. نظرًا لأن تسلسل التبديل غير منصوص عليه ، يتحكم المبرمج نفسه في هذه العملية من خلال إنشاء برنامج الجدولة الخاص به. غالبًا ما يكون هذا مجرد حلقة من الوظائف. مثل ، على سبيل المثال ، دورة حدث Round Robin في الفيديو بواسطة Oleg Molchanov [3] .



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



2. مولدات قوائم البيانات



لذلك - مولدات. غالبًا ما ترتبط البرمجة غير المتزامنة و coroutines بها. سلسلة من مقاطع الفيديو من Oleg Molchanov تحكي عن كل هذا. لذلك ، يشير إلى السمة الرئيسية للمولدات على أنها "قدرتها على إيقاف تنفيذ الوظيفة مؤقتًا من أجل مواصلة تنفيذها من نفس المكان الذي توقفت فيه آخر مرة" (لمزيد من التفاصيل ، انظر [3] ). وفي هذا ، بالنظر إلى ما سبق ذكره عن التعريف القديم جدًا للكوروتين ، لا يوجد شيء جديد.



ولكن كما اتضح ، وجدت المولدات استخدامًا محددًا تمامًا لإنشاء قوائم البيانات. تم تخصيص مقدمة لهذا الموضوع بالفعل لفيديو من إيجوروف أرتيم [4]... ولكن ، كما يبدو ، من خلال هذا التطبيق ، فإننا نمزج المفاهيم المختلفة نوعيا - العمليات والعمليات. من خلال توسيع القدرات الوصفية للغة ، فإننا نتجاهل إلى حد كبير المشاكل التي قد تنشأ. هنا ، كما يقولون ، لا تلعب كثيرا. يبدو لي أن استخدام المولدات-coroutines لوصف البيانات يساهم في هذا بالضبط. لاحظ أن Oleg Molchanov يحذر أيضًا من ربط المولدات بهياكل البيانات ، مؤكداً أن "المولدات هي وظائف" [3] .



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



مشكلة أخرى هي سرعة إنشاء القائمة. نحن الآن نشكل عنصرًا واحدًا من القائمة على كل مفتاح تحويل كوري ، وهذا يزيد من وقت إنشاء البيانات. يمكن تسريع العملية بشكل كبير عن طريق توليد العناصر في "دفعات". ولكن ، على الأرجح ، ستكون هناك مشاكل مع هذا. كيف أوقف عملية جارية بالفعل؟ أو أي شيء آخر. يمكن أن تكون القائمة طويلة جدًا ، باستخدام عناصر محددة فقط. في مثل هذه الحالة ، غالبًا ما يتم استخدام حفظ البيانات للوصول الفعال. بالمناسبة ، وجدت على الفور تقريبًا مقالًا حول هذا الموضوع لبايثون راجع [5] (لمزيد من المعلومات حول الحفظ من حيث الأتمتة ، راجع المقالة [6] ). لكن ماذا عن هذه الحالة؟



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



بالمناسبة ، حول موضوع القوائم والمولدات حول مزاياها وعيوبها ، بالتقاطع مع الملاحظات أعلاه ، يمكنك مشاهدة فيديو آخر بواسطة Oleg Molchanov [7] .



3. مولدات - coroutines



يناقش الفيديو التالي بواسطة Oleg Molchanov [8] استخدام المولدات لتنسيق عمل الكوروتين. في الواقع ، هم مخصصون لهذا الغرض. يتم لفت الانتباه إلى اختيار اللحظات للتبديل بين coroutines. يتبع ترتيبهم قاعدة بسيطة - نضع بيان العائد أمام وظائف الحظر. يُفهم الأخير على أنه وظائف ، يكون وقت العودة منها طويلًا جدًا مقارنة بالعمليات الأخرى التي ترتبط بالحسابات بإيقافها. وبسبب هذا ، تم تسميتهم بالحاصرات.



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



في الفيديو قيد النظر ، كما هو الحال في الحالة العامة لـ coroutines ، يتم تحديد استمرار عملية coroutine بواسطة البيئة الخارجية ، وهي عبارة عن برنامج جدولة حدث. في هذه الحالة ، يتم تمثيلها بواسطة دالة تسمى event_loop. ويبدو أن كل شيء منطقي: سيقوم المجدول بإجراء التحليل ومواصلة عمل coroutine عن طريق استدعاء عامل التشغيل التالي () بالضبط عند الضرورة. تكمن المشكلة في الانتظار حيث لم يكن ذلك متوقعًا: يمكن أن يكون المجدول معقدًا للغاية. في مقطع الفيديو السابق لمولشانوف ( انظر [3] ) ، كان كل شيء بسيطًا منذ ذلك الحين تم إجراء نقل بسيط للتحكم بالتناوب ، حيث لم تكن هناك أقفال منذ ذلك الحين لم تكن هناك مكالمات مقابلة. ومع ذلك ، فإننا نؤكد أنه في أي حال على الأقل ، من الضروري وجود جدولة بسيطة.



المشكلة 1. , next() (. event_loop). , , yield. - , , next(), .



2. , select, — . .



لكن النقطة المهمة ليست حتى الحاجة إلى مخطط ، ولكن حقيقة أنه يتولى وظائف غير عادية بالنسبة له. يزداد الموقف تعقيدًا من خلال حقيقة أنه من الضروري تنفيذ خوارزمية للتشغيل المشترك للعديد من coroutines. تعكس المقارنة بين المجدولين الذين تمت مناقشتهم في مقطعي الفيديو المذكورين بواسطة Oleg Molchanov مشكلة مماثلة بوضوح: خوارزمية جدولة المقبس في [8] أكثر تعقيدًا بشكل ملحوظ من خوارزمية "دائري" في [3] .



3. إلى عالم خال من coroutines



نظرًا لأننا على يقين من إمكانية وجود عالم بدون coroutines ، مع معارضتهم بأتمتة ، فمن الضروري إظهار كيف يتم حل المهام المماثلة بالفعل من قبلهم. دعنا نوضح ذلك باستخدام نفس مثال العمل مع المقابس. لاحظ أن تطبيقه الأولي تبين أنه لم يكن تافهًا لدرجة أنه يمكن فهمه على الفور. تم التأكيد على هذا مرارًا وتكرارًا من قبل مؤلف الفيديو نفسه. يواجه البعض الآخر مشاكل مماثلة في سياق coroutines. لذلك ، فإن مساوئ coroutines المرتبطة بتعقيد إدراكهم وفهمهم وتصحيحهم ، إلخ. نوقشت في الفيديو [10] .



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



قائمة 1. مآخذ على المولدات
import socket
from select import select
tasks = []
to_read = {}
to_write = {}

def server():

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(('localhost', 5001))
    server_socket.listen()

    while True:
        yield ('read', server_socket)
        client_socket, addr = server_socket.accept()    
        print('Connection from', addr)
        tasks.append(client(client_socket, addr))       
    print('exit server')

def client(client_socket, addr):

    while True:
        yield ('read', client_socket)
        request = client_socket.recv(4096)              

        if not request:
            break
        else:
            response = 'Hello World\n'.encode()

            yield ('write', client_socket)

            client_socket.send(response)                
    client_socket.close()                               
    print('Stop client', addr)

def event_loop():
    while any([tasks, to_read, to_write]):

        while not tasks:

            ready_to_read, ready_to_write, _ = select(to_read, to_write, [])

            for sock in ready_to_read:
                tasks.append(to_read.pop(sock))

            for sock in ready_to_write:
                tasks.append(to_write.pop(sock))
        try:
            task = tasks.pop(0)

            reason, sock = next(task)   

            if reason == 'read':
                to_read[sock] = task
            if reason == 'write':
                to_write[sock] = task
        except StopIteration:
            print('Done!')
tasks.append(server())
event_loop()




خوارزميات الخادم والعميل أساسية إلى حد ما. ولكن يجب أن يكون من المثير للقلق أن الخادم يضع وظيفة العميل في قائمة المهام. علاوة على ذلك - المزيد: من الصعب فهم خوارزمية حلقة الحدث event_loop. حتى يمكن أن تكون قائمة المهام فارغة إذا كان على الأقل يجب أن تكون عملية الخادم موجودة فيها دائمًا؟ ..



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



وكيف سيبدو حل المهمة المطروحة؟ سيكون من المنطقي للأوتوماتا إنشاء نماذج مكافئة لعمليات العمل مع المقابس التي تمت مناقشتها بالفعل في الفيديو. في نموذج الخادم ، يبدو أنه لا يوجد شيء يحتاج إلى التغيير. سيكون هذا آليًا يعمل مثل وظيفة الخادم (). يظهر الرسم البياني الخاص بها في الشكل. 1 أ. يُنشئ الإجراء الآلي y1 () مقبس خادم ويوصله بالمنفذ المحدد. يحدد المسند x1 () اتصال العميل ، وإذا كان موجودًا ، فإن الإجراء y2 () ينشئ عملية خدمة مقبس العميل ، ويضع الأخير في قائمة عمليات الفئات ، والتي تتضمن فئات الكائنات النشطة.



في التين. يعرض الشكل 1 ب رسمًا بيانيًا لنموذج عميل فردي. كونه في الحالة "0" ، يحدد الجهاز الآلي استعداد العميل لنقل المعلومات (المسند x1 () - صحيح) ويتلقى استجابة داخل الإجراء y1 () عند الانتقال إلى الحالة "1". علاوة على ذلك ، عندما يكون العميل جاهزًا لتلقي المعلومات (يجب أن تكون x2 () صحيحة) ، فإن الإجراء y2 () ينفذ عملية إرسال رسالة إلى العميل عند الانتقال إلى الحالة الأولية "0". إذا قام العميل بقطع الاتصال بالخادم (في هذه الحالة ، x3 () خاطئ) ، فحينئذٍ يتحول الجهاز التلقائي إلى الحالة "4" ، ويغلق مقبس العميل في إجراء y3 (). تظل العملية في الحالة "4" حتى يتم استبعادها من قائمة الفئات النشطة (انظر الوصف أعلاه لنموذج الخادم لتشكيل القائمة).



في التين. يُظهر 1c آليًا ينفذ إطلاق عمليات مشابهة لوظيفة event_loop () في القائمة 1. فقط في هذه الحالة ، تكون خوارزمية التشغيل الخاصة بها أبسط بكثير. يعود الأمر كله إلى حقيقة أن الجهاز يمر عبر عناصر قائمة الفئات النشطة ويستدعي طريقة الحلقة () لكل منها. يتم تنفيذ هذا الإجراء بواسطة y2 (). يُستثنى إجراء y4 () من فئات القائمة الموجودة في الحالة "4". تعمل بقية الإجراءات مع فهرس قائمة الكائنات: يؤدي إجراء y3 () إلى زيادة الفهرس ، بينما يؤدي إجراء y1 () إلى إعادة تعيينه.



تختلف إمكانيات برمجة الكائنات في Python عن برمجة الكائنات في C ++. لذلك ، سيتم اعتبار أبسط تنفيذ لنموذج التشغيل الآلي كأساس (على وجه الدقة ، إنه تقليد لأتمتة). يعتمد على مبدأ الكائن لتمثيل العمليات ، حيث تتوافق كل عملية مع فئة نشطة منفصلة (غالبًا ما يطلق عليهم أيضًا وكلاء). يحتوي الصنف على الخصائص والطرق الضرورية (انظر المزيد من التفاصيل حول طرق آلية محددة - المسندات والإجراءات في [9] ) ، ويتركز منطق الآلي (وظائف الانتقال والخروج الخاصة به) داخل الطريقة المسماة الحلقة (). لتنفيذ منطق سلوك الإنسان الآلي ، سنستخدم بنية if-elif-else.



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



الشكل: 1. رسوم بيانية لعمليات التشغيل الآلي للعمل مع المقابس
image



تُظهر القائمة 2 رمز كائن آلي في Python للعمل مع المقابس. هذا هو نوعنا من "العالم بدون coroutines". إنه "عالم" بمبادئ مختلفة لتصميم عمليات البرامج. يتميز بوجود نموذج حسابي للحسابات المتوازية (لمزيد من التفاصيل ، انظر [9] ، وهو الاختلاف الرئيسي والنوعي بين تقنية برمجة الأتمتة (AP) و "تقنية coroutine".



تنفذ برمجة Automaton بسهولة المبادئ غير المتزامنة لتصميم البرنامج ، وتوازي العملية ، وفي نفس الوقت كل شيء يمكن أن يفكر فيه عقل المبرمج. تتحدث مقالاتي السابقة عن هذا بمزيد من التفصيل ، بدءًا من وصف النموذج الهيكلي للحساب الآلي وتعريفه الرسمي إلى أمثلة على تطبيقه. يوضح الكود أعلاه في Python التنفيذ التلقائي لمبادئ coroutine الخاصة بـ coroutines ، مما يؤدي إلى تداخلها تمامًا ، وتكميلها وتوسيعها بنموذج آلة الحالة.



قائمة 2. مآخذ على الآلات
import socket
from select import select

timeout = 0.0; classes = []

class Server:
    def __init__(self): self.nState = 0;

    def x1(self):
        self.ready_client, _, _ = select([self.server_socket], [self.server_socket], [], timeout)
        return self.ready_client

    def y1(self):
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_socket.bind(('localhost', 5001))
        self.server_socket.listen()
    def y2(self):
        self.client_socket, self.addr = self.server_socket.accept()
        print('Connection from', self.addr)
        classes.append(Client(self.client_socket, self.addr))

    def loop(self):
        if (self.nState == 0):      self.y1();      self.nState = 1
        elif (self.nState == 1):
            if (self.x1()):         self.y2();      self.nState = 0

class Client:
    def __init__(self, soc, adr): self.client_socket = soc; self.addr = adr; self.nState = 0

    def x1(self):
        self.ready_client, _, _ = select([self.client_socket], [], [], timeout)
        return self.ready_client
    def x2(self):
        _, self.write_client, _ = select([], [self.client_socket], [], timeout)
        return self.write_client
    def x3(self): return self.request

    def y1(self): self.request = self.client_socket.recv(4096);
    def y2(self): self.response = 'Hello World\n'.encode(); self.client_socket.send(self.response)
    def y3(self): self.client_socket.close(); print('close Client', self.addr)

    def loop(self):
        if (self.nState == 0):
            if (self.x1()):                     self.y1(); self.nState = 1
        elif (self.nState == 1):
            if (not self.x3()):                 self.y3(); self.nState = 4
            elif (self.x2() and self.x3()):     self.y2(); self.nState = 0

class EventLoop:
    def __init__(self): self.nState = 0; self.i = 0

    def x1(self): return self.i < len(classes)

    def y1(self): self.i = 0
    def y2(self): classes[self.i].loop()
    def y3(self): self.i += 1
    def y4(self):
        if (classes[self.i].nState == 4):
            classes.pop(self.i)
            self.i -= self.i

    def loop(self):
        if (self.nState == 0):
            if (not self.x1()): self.y1();
            if (self.x1()):     self.y2(); self.y4(); self.y3();

namSrv = Server(); namEv = EventLoop()
while True:
    namSrv.loop(); namEv.loop()




الكود في القائمة 2 أكثر تقدمًا من الناحية التكنولوجية من الكود الموجود في القائمة 1. وهذه هي ميزة النموذج التلقائي للحسابات. يتم تسهيل ذلك من خلال دمج سلوك التشغيل الآلي في نموذج كائن البرمجة. نتيجة لذلك ، يتركز منطق سلوك عمليات التشغيل الآلي بالضبط حيث يتم إنشاؤه ، ولا يتم تفويضه ، كما هو ممارس في coroutines ، في حلقة الحدث للتحكم في العملية. يثير الحل الجديد إنشاء "حلقة حدث" عالمية ، يمكن اعتبار نموذجها الأولي رمزًا لفئة EventLoop.



4. حول مبادئ SRP و DRY



تم التعبير عن مبادئ "المسؤولية الفردية" - SRP (مبدأ المسؤولية الفردية) و "لا تكرر نفسك" - DRY (لا تكرر نفسك) في سياق فيديو آخر بواسطة Oleg Molchanov [11] . ووفقًا لهم ، يجب أن تحتوي الوظيفة على رمز الهدف فقط حتى لا تنتهك مبدأ SRY ، ولا تشجع على تكرار "الرمز الإضافي" حتى لا تنتهك مبدأ DRY. لهذا الغرض ، يُقترح استخدام المصممين. لكن هناك حل آخر - حل تلقائي.



في المادة السابقة [2]غير مدركين لوجود مثل هذه المبادئ ، تم إعطاء مثال باستخدام الديكور. يعتبر عدادًا ، والذي ، بالمناسبة ، يمكنه إنشاء قوائم إذا رغبت في ذلك. تم ذكر كائن ساعة الإيقاف الذي يقيس وقت تشغيل العداد. إذا كانت الكائنات تتوافق مع مبادئ SRP و DRY ، فإن وظائفها ليست بنفس أهمية بروتوكول الاتصال. في التنفيذ ، لا علاقة لكود العداد برمز ساعة الإيقاف ، ولن يؤثر تغيير أي عنصر على الآخر. هم ملزمون فقط بالبروتوكول ، الذي تتفق عليه الأشياء "على الشاطئ" ثم تتبعه بدقة.



وبالتالي ، فإن نموذج الأوتوماتيكي المتوازي يتجاوز بشكل أساسي قدرات المصممين. إنه أكثر مرونة وأسهل في تنفيذ قدراتهم ، لأن لا "تحيط" (لا تزين) رمز الوظيفة. لغرض إجراء تقييم موضوعي ومقارنة بين الأوتوماتيكية والتقنية التقليدية ، تعرض القائمة 3 نظير كائن للعداد الذي تمت مناقشته في المقالة السابقة [2] ، حيث يتم تقديم إصدارات مبسطة مع أوقات تنفيذها والإصدار الأصلي للعداد بعد التعليقات.



قائمة 3. تنفيذ العداد التلقائي
import time
# 1) 110.66 sec
class PCount:
    def __init__(self, cnt ): self.n = cnt; self.nState = 0
    def x1(self): return self.n > 0
    def y1(self): self.n -=1
    def loop(self):
        if (self.nState == 0 and self.x1()):
            self.y1();
        elif (self.nState == 0 and not self.x1()):  self.nState = 4;

class PTimer:
    def __init__(self, p_count):
        self.st_time = time.time(); self.nState = 0; self.p_count = p_count
#    def x1(self): return self.p_count.nStat == 4 or self.p_count.nState == 4
    def x1(self): return self.p_count.nState == 4
    def y1(self):
        t = time.time() - self.st_time
        print ("speed CPU------%s---" % t)
    def loop(self):
       if (self.nState == 0 and self.x1()): self.y1(); self.nState = 1
       elif (self.nState == 1): pass

cnt1 = PCount(1000000)
cnt2 = PCount(10000)
tmr1 = PTimer(cnt1)
tmr2 = PTimer(cnt2)
# event loop
while True:
    cnt1.loop(); tmr1.loop()
    cnt2.loop(); tmr2.loop()

# # 2) 73.38 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         if (self.nState == 0 and self.n > 0): self.n -= 1;
#         elif (self.nState == 0 and not self.n > 0):  self.nState = 4;
# 
# class PTimer:
#     def __init__(self): self.st_time = time.time(); self.nState = 0
#     def loop(self):
#        if (self.nState == 0 and cnt.nState == 4):
#            t = time.time() - self.st_time
#            print("speed CPU------%s---" % t)
#            self.nState = 1
#        elif (self.nState == 1): exit()
# 
# cnt = PCount(100000000)
# tmr = PTimer()
# while True:
#     cnt.loop();
#     tmr.loop()

# # 3) 35.14 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         if (self.nState == 0 and self.n > 0):
#             self.n -= 1;
#             return True
#         elif (self.nState == 0 and not self.n > 0):  return False;
#
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 4) 30.53 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         while self.n > 0:
#             self.n -= 1;
#             return True
#         return False
#
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 5) 18.27 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         while self.n > 0:
#             self.n -= 1;
#         return False
# 
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 6) 6.96 sec
# def count(n):
#   st_time = time.time()
#   while n > 0:
#     n -= 1
#   t = time.time() - st_time
#   print("speed CPU------%s---" % t)
#   return t
#
# def TestTime(fn, n):
#   def wrapper(*args):
#     tsum=0
#     st = time.time()
#     i=1
#     while (i<=n):
#       t = fn(*args)
#       tsum +=t
#       i +=1
#     return tsum
#   return wrapper
#
# test1 = TestTime(count, 2)
# tt = test1(100000000)
# print("Total ---%s seconds ---" % tt)




دعونا نلخص أوقات تشغيل الخيارات المختلفة في جدول ونعلق على نتائج العمل.



  1. الإدراك الآلي الكلاسيكي - 110.66 ثانية
  2. تنفيذ Automata بدون أساليب التشغيل الآلي - 73.38 ثانية
  3. بدون ساعة توقيت أوتوماتيكية - 35.14
  4. عداد في النموذج بينما مع الإخراج في كل تكرار - 30.53
  5. عداد بدورة الحجب - 18.27
  6. العداد الأصلي مع الديكور - 6.96


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



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



5. الاستنتاجات



سواء أكنت تستخدم تقنية التشغيل الآلي أو تثق في coroutines - فإن القرار يقع بالكامل على عاتق المبرمج. من المهم بالنسبة لنا هنا أن يعرف أن هناك نهجًا / تقنية مختلفة عن coroutines لتصميم البرامج. يمكنك حتى تخيل الخيار الغريب التالي. أولاً ، في مرحلة تصميم النموذج ، يتم إنشاء نموذج حل آلي. إنه علمي بدقة ، وقائم على الأدلة ، وموثق جيدًا. ثم ، على سبيل المثال ، من أجل تحسين الأداء ، يتم "تشويهها" إلى إصدار "عادي" من الشفرة ، كما توضح القائمة 3. يمكنك حتى تخيل "إعادة هيكلة عكسية" للشفرة ، أي الانتقال من الخيار السابع إلى الخيار الأول ، ولكن هذا ، على الرغم من أنه ممكن ، ولكن المسار الأقل احتمالية للأحداث :)



في الشكل. 2 يعرض شرائح من الفيديو حول موضوع "غير متزامن" [10]... ويبدو أن "السيئ" يفوق "الجيد". وإذا كانت الأوتوماتا ، في رأيي ، جيدة دائمًا ، ففي حالة البرمجة غير المتزامنة ، اختر ، كما يقولون ، حسب ذوقك. لكن يبدو أن الخيار "السيئ" سيكون هو الأرجح. ويجب أن يعرف المبرمج عن هذا مسبقًا عند تصميم البرنامج.



الشكل: 2. خصائص البرمجة غير المتزامنة
image



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



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



قائمة 4. قراءة الأحرف من لوحة المفاتيح
/*
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int C=0;
    while (C != 'e')
    {
        C = getch();
        putchar (C);
    }
    return a.exec();
}
*/
//*
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int C=0;
    while (C != 'e')
    {
        if (kbhit()) {
            C = getch();
            putch(C);
        }
    }

    return a.exec();
}
//*/




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



إذا كانت الوظيفة "ثقيلة" في حد ذاتها ، أي يتطلب قدرًا كبيرًا من الوقت للعمل ، والخروج الدوري منه حسب نوع عمل coroutines (يمكن القيام بذلك دون استخدام آلية نفس coroutines ، حتى لا يتم الارتباط بها) يصعب القيام به أو لا يكون له معنى كبير ، ثم يبقى وضع هذه الوظائف في سلسلة منفصلة ثم التحكم في إكمال عملهم (انظر تنفيذ فئة QCount في [2]).



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



الأدب



1. بودكاست بايثون جونيور. حول عدم التزامن في بيثون. [مورد إلكتروني] ، وضع الوصول: www.youtube.com/watch؟v=Q2r76grtNeg ، مجانًا. لغة. الروسية (تاريخ العلاج 13/07/2020).

2. التزامن والكفاءة: بايثون مقابل FSM. [مورد إلكتروني] ، وضع الوصول: habr.com/ru/post/506604 ، مجانًا. ياز. الروسية (تاريخ العلاج 13/07/2020).

3. مولتشانوف أو. أساسيات عدم التزامن في بايثون # 4: المولدات وحلقة حدث Round Robin. [مورد إلكتروني] ، وضع الوصول: www.youtube.com/watch؟v=PjZUSSkGLE8 ] ، مجانًا. لغة. الروسية (تاريخ العلاج 13/07/2020).

4. 48 مولدات ومكررات. تعبيرات المولد في بايثون. [مورد إلكتروني] ، وضع الوصول: www.youtube.com/watch؟v=vn6bV6BYm7w، مجانا. لغة. الروسية (تاريخ العلاج 13/07/2020).

5. Memoization والكاري (Python). [مورد إلكتروني] ، وضع الوصول: habr.com/ru/post/335866 ، مجانًا. لغة. الروسية (تاريخ العلاج 13/07/2020).

6. Lyubchenko V.S. حول التعامل مع العودية. "عالم الكمبيوتر" ، رقم 11/02. www.osp.ru/pcworld/2002/11/164417

7. Molchanov O. Python Cast # 10 - ما هو العائد. [مورد إلكتروني] ، وضع الوصول: www.youtube.com/watch؟v=ZjaVrzOkpZk ، مجانًا. لغة. الروسية (تاريخ العلاج 18/07/2020).

8. مولشانوف أو. أساسيات عدم التزامن في بايثون # 5: عدم التزامن في المولدات. [مورد إلكتروني] ، وضع الوصول: www.youtube.com/watch؟v=hOP9bKeDOHs ، مجانًا. لغة. الروسية (تاريخ العلاج 13/07/2020).

9. نموذج الحوسبة المتوازية. [مورد إلكتروني] ، وضع الوصول: habr.com/ru/post/486622 مجانًا. لغة. الروسية (تاريخ العلاج 2020/07/20).

10. بوليشوك أ. عدم التزامن في بايثون. [مورد إلكتروني] ، وضع الوصول: www.youtube.com/watch؟v=lIkA0TDX8tE ، مجانًا. ياز. الروسية (تاريخ العلاج 13/07/2020).

11. Molchanov O. دروس Python cast # 6 - Decorators. [مورد إلكتروني] ، وضع الوصول: www.youtube.com/watch؟v=Ss1M32pp5Ew ، مجانًا. لغة. الروسية (تاريخ العلاج 13/07/2020).



All Articles