Rails/threading and code execution
الخيوط وتنفيذ الشيفرة في ريلز
بعد قراءة هذا الدليل، ستتلعم:
- ما هي شيفرة ريلز التي ستُنفَّذ تلقائيا في وقت واحد.
- كيفية دمج التزامن اليدوي مع أجزاء ريلز الداخلية.
- كيفية تغليف كل شيفرة التطبيق.
- كيف التأثير على عملية إعادة تحميل التطبيق.
التزامن التلقائي
تسمح ريلز تلقائيًا بإجراء عمليات مختلفة في نفس الوقت.
عند استخدام خادم ويب ذي خيوط، مثل الخادم Puma الافتراضي، ستُخدَّم طلبات HTTP متعددة في وقت واحد؛ تقدم مع كل طلب نسخة لوحدة التحكم الخاص به.
محولات الوظيفة الفعالة التي تدعم تعدد الخيوط، تتضمن المحول Async المدمج، ستنفذ أيضًا العديد من المهام في نفس الوقت. قنوات إجراء الربط تدار بهذه الطريقة أيضًا.
كل هذه الآليات تنطوي على تعدد الخيوط، إذ كل منها يعمل لإدارة نسخة فريدة من بعض الكائنات (وحدة التحكم، الوظيفة، القناة)، أثناء مشاركة مساحة العملية العامة (مثل الأصناف والضبط والمتغيرات العامة الخاصة بها). طالما أن شيفرتك لا تعدّل أيًا من تلك الأشياء المشتركة، يمكن أن تتجاهل في الغالب وجود خيوط أخرى.
يصف الجزء المتبقي من هذا الدليل الآليات التي تستخدمها ريلز لجعلها "قابلة للتجاهل غالبًا" ، وكيف يمكن للملحقات والتطبيقات ذات الاحتياجات الخاصة استخدامها.
المُنفِّذ
يقوم مُنفِّذ ريلز (Rails Executor) بفصل شيفرة التطبيق عن شيفرة الإطار: في أي وقت يستدعي الإطار الشيفرة التي كتبتها للتطبيق الخاص بك، ستُغلَّف من قبل المُنفِّذ.
يتكون المُنفِّذ من ردي نداء: to_run
و to_complete
. يُستدعَى رد النداء الأول قبل شيفرة التطبيق، ويُستدعى رد النداء الثاني بعدها.
رد النداء الافتراضي
في تطبيق ريلز الافتراضي، تُستخدَم ردود نداء المُنفِّذ لإجراء ما يلي:
- تتبع أية خيوط تقع في المواقع الآمنة للتحميل التلقائي وإعادة التحميل.
- تمكين وتعطيل ذاكرة التخزين المؤقت لاستعلام السجل الفعال.
- إعادة اتصالات السجل الفعال المكتسبة إلى المجمَّع (pool).
- تقييد عمر ذاكرة التخزين المؤقت الداخلية.
قبل الإصدار 5.0 من ريلز، بعض هذه الأمور جرى معالجتها عبر أصناف وسيطة منفصلة لـ Rack (مثل ActiveRecord::ConnectionAdapters::ConnectionManagement
)، أو تغليف الشيفرة مباشرةً بتوابع مثل ActiveRecord::Base.connection_pool.with_connection
. يستبدل المُنفِّذ هذه بواجهة واحدة أكثر تجريدية.
تغليف شيفرة التطبيق
إذا كنت تكتب مكتبة أو مكوّنًا سيستدعي شيفرة التطبيق، فيجب عليك تغليفها باستدعاءٍ إلى المُنفِّذ:
Rails.application.executor.wrap do
# استدعي شيفرة التطبيق هنا
end
ملاحظة: إذا استدعيت شيفرة التطبيق بشكل متكرر من عملية مستمرة منذ فترة طويلة، قد ترغب في إجراء التغليف باستخدام Reloader بدلًا من ذلك.
يجب أن يُغلَّف كل خيط قبل أن يُشغِّل شيفرة التطبيق، لذلك إذا قام التطبيق الخاص بك يدويًا بتفويض العمل إلى خيوط أخرى، مثل عبر Thread.new
أو ميزات روبي المتزامنة التي تستخدم مجمعات خيوط (thread pools)، فيجب عليك تغليف الكتلة مباشرةً:
Thread.new do
Rails.application.executor.wrap do
# your code here
end
end
ملاحظة: يستخدم التزامن في روبي ThreadPoolExecutor
، الذي تضبطه في بعض الأحيان مع الخيار executor
على الرغم من أنَّ الاسم لا علاقة له به.
يعاد مشاركة المُنفِّذ بأمان؛ إذا كان نشطًا بالفعل على خيط الحالي، فإنَّ wrap
هو عملية فارغة (no-op).
إذا كان من غير العملي تغليف شيفرة التطبيق في الكتلة (على سبيل المثال، تسبب واجهة Rack البرمجية هذه الإشكالية)، يمكنك أيضًا استخدام الزوج run!
/ complete!
:
Thread.new do
execution_context = Rails.application.executor.run!
# ضع شيفرتك هنا
ensure
execution_context.complete! if execution_context
end
التزامن
سيضع المُنفِّذ الخيط الحالي في الوضع running
في Load Interlock. ستُحجَز هذه العملية مؤقتًا إذا كان هناك خيط آخر إمَّا يحمِّل تلقائيًا لثابت أو يغلي / يعيد تحميل التطبيق حاليًا.
إعادة تحميل
بشكل مشابه للمُنفِّذ، يغلف معيد التحميل Reloader أيضًا شيفرة التطبيق. إذا لم يكن المُنفِّذ نشطًا بالفعل في الخيط الحالي، سيستدعيه Reloader لك، لذلك تحتاج فقط إلى الاتصال به. هذا يضمن أيضا أن كل شيء يقوم به Reloader، بما في ذلك جميع استدعاءات ردود النداء الخاصة به، يحدث وهو مغلَّف داخل المُنفِّذ.
Rails.application.reloader.wrap do
# استدعي شيفرة التطبيق هنا
end
لا يكون معيد التحميل Reloader مناسبًا إلا عندما تستدعي عملية طويلة الأجل على مستوى الإطار بشكل متكرر في شيفرة التطبيق، مثل خادم الويب أو قائمة انتظار المهام. يغلف ريلز تلقائيًا طلبات الويب وعمليات الوظيفة الفعالة، لذا نادرًا ما تحتاج إلى استدعاء معيد التحميل بنفسك. ضع في اعتبارك دائمًا ما إذا كان المُنفِّذ مناسبًا بشكل أفضل لحالة الاستخدام الخاصة بك.
ردود النداء
قبل الدخول إلى الكتلة المغلَّفة، سيتحقق معيد التحميل من ما إذا كان التطبيق قيد التشغيل يحتاج إلى إعادة تحميل - على سبيل المثال، لأنه عُدل على الملف المصدر الخاص بالنموذج. إذا حُدد أن إعادة التحميل مطلوبة، فسينتظر حتى يتأكد من أمان تنفيذ العملية، ثم يعيد التحميل قبل المتابعة. عند ضبط التطبيق لإعادة تحميله دائمًا بغض النظر عما إذا اكتشفت أي تغييرات، تُنفَّذ إعادة التحميل بدلًا من ذلك في نهاية الكتلة.
كما يوفر معيد التحميل أيضًا ردي النداء to_run
و to_complete
؛ يستدعيان في المكان نفسه حيث يستدعى ردي نداء المُنفِّذ المقابلين لهما، ولكن فقط عندما يبدأ التنفيذ الحالي بإعادة تحميل التطبيق. عندما لا تكون هناك حاجة إلى إعادة التحميل، سيستدعي الأمر إعادة تحميل الكتلة المغلفة دون أي استدعاءات أخرى.
إلغاء تحميل صنف
الجزء الأكثر أهمية من عملية إعادة التحميل هو إلغاء تحميل صفن، حيث تزال جميع الأصناف التي حُمّلَت تلقائيًا، وتكون جاهزة للتحميل مرة أخرى. سيحدث هذا مباشرةً قبل معاودة الاتصال أو التشغيل الكامل، استنادًا إلى الضبط reload_classes_only_on_change
.
في كثير من الأحيان، يجب تنفيذ إجراءات إعادة التحميل الإضافية إما قبل أو بعد انتهاء تحميل الصنف تمامًا، لذلك يوفر معيد التحميل أيضًا ردي النداء before_class_unload
و after_class_unload
.
التزامن
يجب أن تستدعي عمليات "المستوى الأعلى" طويلة الأمد معيد التحميل Reloader، لأنه إذا دعت الحاجة إلى إعادة التحميل، فستُحظَر العملية إلى أن تنتهي جميع الخيوط من أي استدعاءات للمُنفِّذ.
إن كان هذا سيحدث في خيط فرعي (ابن)، مع انتظار الأب داخل المُنفِّذ، فسيتسبَّب في إحداث قفل ميت لا مفر منه: يجب أن تحدث إعادة التحميل قبل تنفيذ الخيط الفرعي (الابن)، ولكن لا يمكن تنفيذه بأمان في حين أن الخيط الرئيسي (الأب) هو منتصف التنفيذ. يجب أن تستخدم الخيوط الفرعية (الأبناء) المٌنفِّذ (Executor ) بدلًا من ذلك.
سلوك الإطار
تستخدم مكونات إطار ريلز هذه الأدوات لإدارة احتياجات التزامن الخاصة بهم أيضاً.
ActionDispatch :: Executor و ActionDispatch :: Reloader هي middlewares Rack الذي يلف الطلب مع Executeor أو Reloader ، على التوالي. وتضمن تلقائيًا في مكدس التطبيقات الافتراضي. سيضمن برنامج Reloader أي طلب HTTP قادم ليقدم مع نسخة محملة حديثًا من التطبيق في حالة حدوث أي تغييرات في الشيفرة.
يلف Active Job أيضًا عمليات التنفيذ مع أداة Reloader ، وتحميل أحدث شيفرة لتنفيذ كل مهمة لأنها تأتي من قائمة الانتظار.
يستخدم كبل Action المنفذ بدلاً من ذلك: نظرًا لأن اتصال الكبل مرتبط بنسخة محددة من الفئة، لا يمكن إعادة التحميل لكل رسالة websocket قادمة.
يُلف معالج الرسالة فقط، على أية حال؛ لا يمنع اتصال كابل طويل الأمد عملية إعادة تحميل شُغلت بواسطة طلب جديد أو مهمة واردة. بدلاً من ذلك، يستخدم "كابل الإجراء" الاستدعاء before_class_unload الخاص بـ Reloader لفصل جميع اتصالاته. عندما يعيد العميل الاتصال تلقائيًا، سوف يتحدث إلى الإصدار الجديد من الشيفرة.
ما سبق هو نقاط الدخول إلى الإطار، لذا فهم مسؤولون عن ضمان حماية الترابطات المعنية لهم، وتحديد ما إذا كانت إعادة التحميل ضرورية أم لا. المكونات الأخرى تحتاج فقط إلى استخدام المنفذ عند إنتاج ترابط عمليات إضافية.
إعدادات التكوين
يقوم برنامج Reloader بفحص تغييرات الملف فقط عندما يكون cache_classes خاطئًا ويكون reload_classes_only_on_change صحيحًا (وهو الإعداد الافتراضي في بيئة التطوير).
عندما يكون cache_classes صحيحًا (في مرحلة الإنتاج، بشكل افتراضي)، فإن أداة إعادة التحميل هي فقط تمرره إلى المنفذ.
المنفذ لديه دائما عمل مهم للقيام به، مثل إدارة اتصال قاعدة البيانات. عندما يكون كل من cache_classes و eager_load صحيحين (في مرحلة الإنتاج)، لن يحدث أي تحميل ذاتي أو إعادة تحميل للفئة، لذلك لا تحتاج إلى Load Interlock. إذا كان أيًا منهما غير صحيح (في مرحلة التطوير)، فسيستخدم المنفذ Load Interlock لضمان تحميل الثوابت فقط عندما تكون آمنة.
تحميل Interlock
يسمح Load Interlock بتمكين التحميل التلقائي وإعادة التحميل في بيئة تشغيل متعددة المسارات.
عند إجراء التحميل التلقائي خيط واحد عن طريق تقييم تعريف الفئة من الملف المناسب، من المهم ان لا يواجه خيط آخر مرجع إلى ثابت معرف جزئياً.
وبالمثل، من الآمن فقط إجراء إلغاء تحميل / إعادة تحميل عندما لا يوجد شيفرة تطبيق في منتصف التنفيذ: بعد إعادة التحميل، قد يشير ثابت المستخدم، على سبيل المثال، إلى فئة مختلفة. بدون هذه القاعدة، فإن إعادة التحميل في توقيت سيئ سيعني
User.new.class == User او User == User قد يكون خطأ.
كل من هذه القيود نتناولها بواسطة Load Interlock. فإنه يحتفظ بتتبع مؤشرات الترابط التي تُشغل التعليمات البرمجية للتطبيق حالياً أو تحميل فئة أو إلغاء تحميل التحميل التلقائي للثوابت.
قد يُحمل أو يُلغى تحميل خيط واحد فقط في كل مرة، وللقيام بذلك، يجب الانتظار حتى لا تُشغل أي ترابط عمليات أخرى لشيفرة التطبيق. إذا كان هناك خيط ينتظر إجراء تحميل، فإنه لا يمنع تحميل ترابط الرسائل الأخرى (في الواقع، سوف يتعاونون، ويقوم كل منهم بحمله في طابور الانتظار، قبل أن يستأنفوا العمل معاً).
Permit_concurrent_loads
يكتسب Executor تلقائيًا قفلً running طوال فترة الحظر، ويُعرف التحميل التلقائي عند الترقية إلى قفل load، ثم الرجوع إلى running مرة أخرى بعد ذلك.
ومع ذلك، فإن عمليات المنع الأخرى التي تكون داخل كتلة المنفذ (والتي تتضمن كل شيفرة التطبيق)، يمكنها الاحتفاظ بقفل running دون داع. إذا واجه خيط آخر ثابت يجب تحميله تلقائياً، ويمكن أن يؤدي هذا إلى حالة توقف تام.
على سبيل المثال، بافتراض أن المستخدم لم يُحمل بعد، سيؤدي المأزق التالي إلى:
ريلز.application.executor.wrap do
th = Thread.new do
ريلز.application.executor.wrap do
User # inner thread waits here; it cannot load
# User while another thread is running
end
end
th.join # outer thread waits here, holding 'running' lock
End
لمنع حالة التوقف التام هذه، يمكن لخيط الخارجي allow_concurrent_loads. عن طريق استدعاء هذا التابع، يضمن خيط أنه لن يؤدي إلى إبطال أي ثابت مُحمل تلقائيًا داخل الكتلة التي وفرت. والطريقة الأكثر أمانًا لتحقيق هذا الوعد هي جعله أقرب ما يمكن إلى استدعاء المنع:
ريلز.application.executor.wrap do
th = Thread.new do
ريلز.application.executor.wrap do
User # inner thread can acquire the 'load' lock,
# load User, and continue
end
end
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
th.join # outer thread waits here, but has no lock
end
End
مثال آخر، باستخدام تزامن روبي:
ريلز.application.executor.wrap do
futures = 3.times.collect do |i|
Concurrent::Future.execute do
ريلز.application.executor.wrap do
# do work here
end
end
end
values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
futures.collect(&:value)
end
end
ActionDispatch::DebugLocks
إذا كان التطبيق الخاص بك توقف تمامًا وكنت تعتقد أن التحميل Interlock قد يكون متضمناً، يمكنك إضافة الوسيطة ActionDispatch :: DebugLocks إلى config / application.rb:
config.middleware.insert_before Rack::Sendfile,
ActionDispatch::DebugLocks
إذا قمت بإعادة تشغيل التطبيق ثم إعادة تشغيل شرط حالة التوقف التام، /ريلز/locks سوف تعرض ملخصًا لجميع الترابطات المعروفة حاليًا بـ interlock، والتي تقفل مستوى محتواها أو تنتظرها، وتتبعهم الخلفي الحالي.
بشكل عام، قد يكون هناك حالة توقف تام بسبب التعطل المتعارض مع بعض عمليات القفل الخارجية الأخرى أو حظر استدعاء الإدخال / الإخراج. بمجرد العثور عليه، يمكنك لفه مع permit_concurrent_loads.
المراجع
- مرور وتنفيذ التعليمات البرمجية في ريلز.