الفرق بين المراجعتين لصفحة: «Rails/threading and code execution»

من موسوعة حسوب
أنشأ الصفحة ب'الخيوط وتنفيذ الشيفرة في ريلز بعد قراءة هذا الدليل، ستتلعم: * ما هي شيفرة ريلز التي ستُنفَّذ...'
 
طلا ملخص تعديل
 
(1 مراجعات متوسطة بواسطة نفس المستخدم غير معروضة)
سطر 1: سطر 1:
الخيوط وتنفيذ الشيفرة في ريلز
<noinclude>{{DISPLAYTITLE:الخيوط وتنفيذ الشيفرة في ريلز}}</noinclude>
 
بعد قراءة هذا الدليل، ستتلعم:
بعد قراءة هذا الدليل، ستتلعم:
* ما هي شيفرة ريلز التي ستُنفَّذ تلقائيا في وقت واحد.
* ما هي شيفرة ريلز التي ستُنفَّذ تلقائيا في وقت واحد.
سطر 12: سطر 11:
عند استخدام خادم ويب ذي [[Ruby/Thread|خيوط]]، مثل الخادم Puma الافتراضي، ستُخدَّم طلبات HTTP متعددة في وقت واحد؛  تقدم مع كل طلب نسخة لوحدة التحكم الخاص به.
عند استخدام خادم ويب ذي [[Ruby/Thread|خيوط]]، مثل الخادم Puma الافتراضي، ستُخدَّم طلبات HTTP متعددة في وقت واحد؛  تقدم مع كل طلب نسخة لوحدة التحكم الخاص به.


محولات الوظيفة الفعالة التي تدعم تعدد الخيوط، تتضمن المحول Async المدمج، ستنفذ أيضًا العديد من المهام في نفس الوقت. قنوات [[Rails/action cable overview|إجراء الربط]] تدار بهذه الطريقة أيضًا.
محولات [[Rails/active job|Active Job]] التي تدعم تعدد الخيوط، تتضمن المحول Async المدمج، ستنفذ أيضًا العديد من المهام في نفس الوقت. قنوات [[Rails/action cable overview|Action Cable]] تدار بهذه الطريقة أيضًا.


كل هذه الآليات تنطوي على تعدد الخيوط، إذ كل منها يعمل لإدارة نسخة فريدة من بعض الكائنات (وحدة التحكم، الوظيفة، القناة)، أثناء مشاركة مساحة العملية العامة (مثل الأصناف والضبط والمتغيرات العامة الخاصة بها). طالما أن شيفرتك لا تعدّل أيًا من تلك الأشياء المشتركة، يمكن أن تتجاهل في الغالب وجود خيوط أخرى.
كل هذه الآليات تنطوي على تعدد الخيوط، إذ كل منها يعمل لإدارة نسخة فريدة من بعض الكائنات (وحدة التحكم، الوظيفة، القناة)، أثناء مشاركة مساحة العملية العامة (مثل الأصناف والضبط والمتغيرات العامة الخاصة بها). طالما أن شيفرتك لا تعدّل أيًا من تلك الأشياء المشتركة، يمكن أن تتجاهل في الغالب وجود خيوط أخرى.
سطر 26: سطر 25:
في تطبيق ريلز الافتراضي، تُستخدَم ردود نداء المُنفِّذ لإجراء ما يلي:
في تطبيق ريلز الافتراضي، تُستخدَم ردود نداء المُنفِّذ لإجراء ما يلي:
* تتبع أية خيوط تقع في المواقع الآمنة للتحميل التلقائي وإعادة التحميل.
* تتبع أية خيوط تقع في المواقع الآمنة للتحميل التلقائي وإعادة التحميل.
* تمكين وتعطيل ذاكرة التخزين المؤقت لاستعلام السجل الفعال.
* تمكين وتعطيل ذاكرة التخزين المؤقت لاستعلام [[Rails/active record|Active Record]].
* إعادة اتصالات السجل الفعال المكتسبة إلى المجمَّع (pool).
* إعادة اتصالات [[Rails/active record|Active Record]] المكتسبة إلى المجمَّع (pool).
* تقييد عمر ذاكرة التخزين المؤقت الداخلية.
* تقييد عمر ذاكرة التخزين المؤقت الداخلية.
قبل الإصدار 5.0 من ريلز، بعض هذه الأمور جرى معالجتها عبر أصناف وسيطة منفصلة لـ Rack (مثل <code>ActiveRecord::ConnectionAdapters::ConnectionManagement</code>)، أو تغليف الشيفرة مباشرةً بتوابع مثل <code>ActiveRecord::Base.connection_pool.with_connection</code>. يستبدل المُنفِّذ هذه بواجهة واحدة أكثر تجريدية.
قبل الإصدار 5.0 من ريلز، بعض هذه الأمور جرى معالجتها عبر أصناف وسيطة منفصلة لـ Rack (مثل <code>ActiveRecord::ConnectionAdapters::ConnectionManagement</code>)، أو تغليف الشيفرة مباشرةً بتوابع مثل <code>ActiveRecord::Base.connection_pool.with_connection</code>. يستبدل المُنفِّذ هذه بواجهة واحدة أكثر تجريدية.
سطر 58: سطر 57:


=== التزامن ===
=== التزامن ===
سيضع المُنفِّذ الخيط الحالي في الوضع <code>running</code> في Load Interlock. ستُحجَز هذه العملية مؤقتًا إذا كان هناك خيط آخر إمَّا يحمِّل تلقائيًا لثابت أو يغلي / يعيد تحميل التطبيق حاليًا.
سيضع المُنفِّذ الخيط الحالي في الوضع <code>running</code> عند تداخل التحميل (Load Interlock). ستُحجَز هذه العملية مؤقتًا إذا كان هناك خيط آخر إمَّا يحمِّل تلقائيًا لثابت أو يغلي / يعيد تحميل التطبيق حاليًا.


== إعادة تحميل ==
== إعادة تحميل ==
سطر 65: سطر 64:
   # استدعي شيفرة التطبيق هنا
   # استدعي شيفرة التطبيق هنا
end
end
</syntaxhighlight>لا يكون معيد التحميل Reloader مناسبًا إلا عندما تستدعي عملية طويلة الأجل على مستوى الإطار بشكل متكرر في شيفرة التطبيق، مثل خادم الويب أو قائمة انتظار المهام. يغلف ريلز تلقائيًا طلبات الويب وعمليات الوظيفة الفعالة، لذا نادرًا ما تحتاج إلى استدعاء معيد التحميل بنفسك. ضع في اعتبارك دائمًا ما إذا كان المُنفِّذ مناسبًا بشكل أفضل لحالة الاستخدام الخاصة بك.
</syntaxhighlight>لا يكون معيد التحميل Reloader مناسبًا إلا عندما تستدعي عملية طويلة الأجل على مستوى الإطار بشكل متكرر في شيفرة التطبيق، مثل خادم الويب أو قائمة انتظار المهام. يغلف ريلز تلقائيًا طلبات الويب وعمليات [[Rails/active job|Active Job]]، لذا نادرًا ما تحتاج إلى استدعاء معيد التحميل بنفسك. ضع في اعتبارك دائمًا ما إذا كان المُنفِّذ مناسبًا بشكل أفضل لحالة الاستخدام الخاصة بك.


=== ردود النداء ===
=== ردود النداء ===
سطر 83: سطر 82:


== سلوك الإطار ==
== سلوك الإطار ==
تستخدم مكونات إطار ريلز هذه الأدوات لإدارة احتياجات التزامن الخاصة بهم أيضاً.
تستخدم مكونات إطار ريلز هذه الأدوات لإدارة احتياجات التزامن الخاصة بهم أيضًا.


ActionDispatch :: Executor و ActionDispatch :: Reloader هي middlewares Rack الذي يلف الطلب مع Executeor أو Reloader ، على التوالي. وتضمن تلقائيًا في مكدس التطبيقات الافتراضي. سيضمن برنامج Reloader أي طلب HTTP قادم ليقدم مع نسخة محملة حديثًا من التطبيق في حالة حدوث أي تغييرات في الشيفرة.
<code>ActionDispatch::Executor</code> و <code>ActionDispatch::Reloader</code> هي برمجيات Rack وسيطة تغلف الطلب مع مُنفِّذ (Executor) أو معيد تحميل (Reloader) معطى، على التوالي. وتُضمَّن تلقائيًا في مكدس التطبيقات الافتراضي. سيأكد معيد التحميل من تخديم أي طلب HTTP قادم ليقدم مع نسخة محملة حديثًا من التطبيق في حالة حدوث أي تغييرات في الشيفرة.


يلف Active Job أيضًا عمليات التنفيذ مع أداة Reloader ، وتحميل أحدث شيفرة لتنفيذ كل مهمة لأنها تأتي من قائمة الانتظار.
تغلف [[Rails/active job|Active Job]] أيضًا عمليات التنفيذ مع معيد التحميل، وتحميل أحدث شيفرة لتنفيذ كل وظيفة يحين دورها في طابور الانتظار.


يستخدم كبل Action المنفذ بدلاً من ذلك: نظرًا لأن اتصال الكبل مرتبط بنسخة محددة من الفئة، لا يمكن إعادة التحميل لكل رسالة websocket قادمة.
يستخدم [[Rails/action cable|Action Cable]] المُنفِّذ بدلًا من ذلك: نظرًا لأن اتصال الربط (Cable connection) مرتبط بنسخة محددة من صنفٍ، لا يمكن إعادة التحميل لكل رسالة قادمة من مقبس ويب websocket. على أية حال، يغلف معالج الرسالة فقط؛ لا يمنع اتصال الربط طويل الأمد عملية إعادة تحميل شُغلَت بواسطة طلب جديد أو مهمة واردة. بدلًا من ذلك، يستخدم "[[Rails/action cable overview|Action Cable]]" الاستدعاء <code>before_class_unload</code> الخاص بمعيد التحميل Reloader لفصل جميع اتصالاته. عندما يعيد العميل الاتصال تلقائيًا، سوف يتحدث إلى الإصدار الجديد من الشيفرة.


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


ما سبق هو نقاط الدخول إلى الإطار، لذا فهم مسؤولون عن ضمان حماية الترابطات المعنية لهم، وتحديد ما إذا كانت إعادة التحميل ضرورية أم لا. المكونات الأخرى تحتاج فقط إلى استخدام المنفذ عند إنتاج ترابط عمليات إضافية.
=== الضبط ===
يقوم معيد التحميل Reloader بفحص تغييرات الملف فقط عندما تكون قيمة <code>cache_classes</code> هي <code>false</code> وتكون قيمة <code>reload_classes_only_on_change</code> هي <code>true</code> (وهو الإعداد الافتراضي في بيئة التطوير).


=== إعدادات التكوين ===
عندما تكون قيمة <code>cache_classes</code> هي <code>true</code> (في مرحلة الإنتاج، بشكل افتراضي)، فإن معيد التحميل يمر مباشرةً إلى المُنفِّذ.
يقوم برنامج Reloader بفحص تغييرات الملف فقط عندما يكون cache_classes خاطئًا ويكون reload_classes_only_on_change صحيحًا (وهو الإعداد الافتراضي في بيئة التطوير).


عندما يكون cache_classes صحيحًا (في مرحلة الإنتاج، بشكل افتراضيفإن أداة إعادة التحميل هي فقط تمرره إلى المنفذ.
المُنفِّذ لديه دائمًا عمل مهم للقيام به، مثل إدارة اتصال قاعدة البيانات. عندما تكون قيمة كل من <code>cache_classes</code> و <code>eager_load</code> هي <code>true</code> (في مرحلة الإنتاجلن يحدث أي تحميل ذاتي أو إعادة تحميل للصنف، لذلك لا تحتاج إلى تداخل التحميل (Load Interlock). إذا كانت قيمة أيًا منهما هي <code>false</code> (في مرحلة التطوير)، فسيستخدم المُنفِّذ تداخل التحميل (Load Interlock) لضمان تحميل الثوابت فقط عندما تكون آمنة.


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


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


عند إجراء التحميل التلقائي خيط واحد عن طريق تقييم تعريف الفئة من الملف المناسب، من المهم ان لا يواجه خيط آخر مرجع إلى ثابت معرف جزئياً.
وبالمثل، من الآمن فقط إجراء إلغاء تحميل / إعادة تحميل عندما لا يوجد شيفرة تطبيق في منتصف التنفيذ: بعد إعادة التحميل، قد يشير ثابت المستخدم، على سبيل المثال، إلى صنف مختلف. بدون هذه القاعدة، فإن إعادة التحميل في توقيت سيئ سيعني <code>User.new.class == User</code> أو حتى <code>User == User</code> قد يكون خطأ.


وبالمثل، من الآمن فقط إجراء إلغاء تحميل / إعادة تحميل عندما لا يوجد شيفرة تطبيق في منتصف التنفيذ: بعد إعادة التحميل، قد يشير ثابت المستخدم، على سبيل المثال، إلى فئة مختلفة. بدون هذه القاعدة، فإن إعادة التحميل في توقيت سيئ سيعني
كل من هذين القيدين يعالجان عبر تداخل التحميل (Load Interlock). إنه يحتفظ بتتبع أي خيط يُشغل شيفرة التطبيق أو يحمِّل صنفًا أو يلغي تحميل الثوابت المحملة تلقائيًّا في الوقت الحالي.


User.new.class == User او User == User قد يكون خطأ.
قد يُحمِّل أو يُلغي تحميل خيط واحد فقط في كل مرة، وللقيام بذلك، يجب الانتظار حتى لا تُشغل أي خيوط عمليات أخرى لشيفرة التطبيق. إذا كان هناك خيط ينتظر إجراء تحميل، فإنَّه لا يمنع خيطًا آخر من التحميل (في الواقع، سوف يتعاونون، وينفِّذ كل منهم الحمل المخصص له في طابور الانتظار، قبل أن يستأنفوا العمل معًا).


كل من هذه القيود نتناولها بواسطة Load Interlock. فإنه يحتفظ بتتبع مؤشرات الترابط التي تُشغل التعليمات البرمجية للتطبيق حالياً أو تحميل فئة أو إلغاء تحميل التحميل التلقائي للثوابت.
=== <code>permit_concurrent_loads</code> ===
يكتسب المُنفِّذ Executor تلقائيًا قفل تشغيل (القفل <code>running</code>) طوال فترة الحظر، ويعرف التحميل التلقائي متى تجري الترقيةإلى قفل التحميل (القفل <code>load</code>)، ثم الرجوع إلى القفل <code>running</code> مرة أخرى بعد ذلك.


قد يُحمل أو يُلغى تحميل خيط واحد فقط في كل مرة، وللقيام بذلك، يجب الانتظار حتى لا تُشغل أي ترابط عمليات أخرى لشيفرة التطبيق. إذا كان هناك خيط ينتظر إجراء تحميل، فإنه لا يمنع تحميل ترابط الرسائل الأخرى (في الواقع، سوف يتعاونون، ويقوم كل منهم بحمله في طابور الانتظار، قبل أن يستأنفوا العمل معاً).
ومع ذلك، فإن عمليات المنع الأخرى التي تكون داخل كتلة المُنفِّذ (والتي تتضمن كل شيفرة التطبيق)، يمكنها الاحتفاظ بقفل <code>running</code> دون داع. إذا واجه خيط آخر ثابتًا يجب تحميله تلقائيًّا، يمكن أن يؤدي هذا إلى حالة قفل ميت.
 
=== 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


على سبيل المثال، بافتراض أن المستخدم لم يُحمَّل بعد، سيؤدي المأزق التالي إلى:ra<syntaxhighlight lang="rails">
Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # الخيط الداخلي ينتظهر هنا. إن لا يستطيع
          # أثناء عمل خيط آخر User تحميل
    end
  end
  th.join # 'running' الخيط الخارجي ينتظر هنا ويحمل القفل
end
</syntaxhighlight>لمنع حالة القفل الميت هذه، يمكن للخيط الخارجي أن يسمح بالتحميل المتزامن عبر التابع <code>allow_concurrent_loads</code>. عن طريق استدعاء هذا التابع، يضمن الخيط أنه لن يؤدي إلى إبطال أي ثابت مُحمل تلقائيًا داخل الكتلة التي أعطيت. والطريقة الأكثر أمانًا لتحقيق هذا الوعد هي جعله أقرب ما يمكن إلى استدعاء الحجز:<syntaxhighlight lang="rails">
Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # 'load' الخيط الداخلي يمكن أن يكسب القفل
          # ويكمل User ويحمل
    end
  end
  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    th.join # ينتظر القفل الخارجي هنا ولكن لا يملك أي قفل
  end
end
</syntaxhighlight>مثال آخر، باستخدام تزامن روبي:<syntaxhighlight lang="rails">
Rails.application.executor.wrap do
  futures = 3.times.collect do |i|
    Concurrent::Future.execute do
      Rails.application.executor.wrap do
        # نفّذ العمل هنا
      end
    end
  end
  values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    futures.collect(&:value)
  end
end
end
</syntaxhighlight>


=== ActionDispatch::DebugLocks ===
=== <code>ActionDispatch::DebugLocks</code> ===
إذا كان التطبيق الخاص بك توقف تمامًا وكنت تعتقد أن التحميل Interlock قد يكون متضمناً، يمكنك إضافة الوسيطة ActionDispatch :: DebugLocks إلى config / application.rb:
إذا كان التطبيق الخاص بك يعلق في أحد الأقفال الميتة وكنت تعتقد أن تداخل التحميل قد يكون متضمنًا، يمكنك إضافة الوسيط <code>ActionDispatch::DebugLocks</code> مؤقتًا إلى config/application.rb:<syntaxhighlight lang="rails">
 
config.middleware.insert_before Rack::Sendfile,
config.middleware.insert_before Rack::Sendfile,
                                  ActionDispatch::DebugLocks
</syntaxhighlight>إذا قمت بإعادة تشغيل التطبيق ثم أعدت تشغيل حالة شرط التوقف التام، سيعرض ‎/rails/locks ملخصًا لجميع الخيوط المعروفة حاليًا بالتداخل، والتي تقفل مستوى محتواها أو تنتظره، بالإضافة إلى التعقب العكسي الحالي.


                                 ActionDispatch::DebugLocks
بشكل عام، قد يكون هناك حالة توقف تام (قفل ميت) بسبب التداخل المتعارض مع بعض عمليات القفل الخارجية الأخرى أو حظر استدعاء الإدخال / الإخراج. بمجرد العثور عليه، يمكنك تغليفه مع <code>permit_concurrent_loads</code>.
 
إذا قمت بإعادة تشغيل التطبيق ثم إعادة تشغيل شرط حالة التوقف التام، /ريلز/locks سوف تعرض ملخصًا لجميع الترابطات المعروفة حاليًا بـ interlock، والتي تقفل مستوى محتواها أو تنتظرها، وتتبعهم الخلفي الحالي.
 
بشكل عام، قد يكون هناك حالة توقف تام بسبب التعطل المتعارض مع بعض عمليات القفل الخارجية الأخرى أو حظر استدعاء الإدخال / الإخراج. بمجرد العثور عليه، يمكنك لفه مع permit_concurrent_loads.


== المراجع ==
== مصادر ==
* مرور وتنفيذ التعليمات البرمجية في ريلز.
* [https://guides.rubyonrails.org/threading_and_code_execution.html صفحة Threading and Code Execution in Rails في توثيق Ruby On Rails الرسمي.]
[[تصنيف:Rails]]
[[تصنيف:Rails Extending]]

المراجعة الحالية بتاريخ 09:04، 25 مارس 2019

بعد قراءة هذا الدليل، ستتلعم:

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

التزامن التلقائي

تسمح ريلز تلقائيًا بإجراء عمليات مختلفة في نفس الوقت.

عند استخدام خادم ويب ذي خيوط، مثل الخادم Puma الافتراضي، ستُخدَّم طلبات HTTP متعددة في وقت واحد؛ تقدم مع كل طلب نسخة لوحدة التحكم الخاص به.

محولات Active Job التي تدعم تعدد الخيوط، تتضمن المحول Async المدمج، ستنفذ أيضًا العديد من المهام في نفس الوقت. قنوات Action Cable تدار بهذه الطريقة أيضًا.

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

يصف الجزء المتبقي من هذا الدليل الآليات التي تستخدمها ريلز لجعلها "قابلة للتجاهل غالبًا" ، وكيف يمكن للملحقات والتطبيقات ذات الاحتياجات الخاصة استخدامها.

المُنفِّذ

يقوم مُنفِّذ ريلز (Rails Executor) بفصل شيفرة التطبيق عن شيفرة الإطار: في أي وقت يستدعي الإطار الشيفرة التي كتبتها للتطبيق الخاص بك، ستُغلَّف من قبل المُنفِّذ.

يتكون المُنفِّذ من ردي نداء: to_run و to_complete. يُستدعَى رد النداء الأول قبل شيفرة التطبيق، ويُستدعى رد النداء الثاني بعدها.

رد النداء الافتراضي

في تطبيق ريلز الافتراضي، تُستخدَم ردود نداء المُنفِّذ لإجراء ما يلي:

  • تتبع أية خيوط تقع في المواقع الآمنة للتحميل التلقائي وإعادة التحميل.
  • تمكين وتعطيل ذاكرة التخزين المؤقت لاستعلام Active Record.
  • إعادة اتصالات Active Record المكتسبة إلى المجمَّع (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 مناسبًا إلا عندما تستدعي عملية طويلة الأجل على مستوى الإطار بشكل متكرر في شيفرة التطبيق، مثل خادم الويب أو قائمة انتظار المهام. يغلف ريلز تلقائيًا طلبات الويب وعمليات Active Job، لذا نادرًا ما تحتاج إلى استدعاء معيد التحميل بنفسك. ضع في اعتبارك دائمًا ما إذا كان المُنفِّذ مناسبًا بشكل أفضل لحالة الاستخدام الخاصة بك.

ردود النداء

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

كما يوفر معيد التحميل أيضًا ردي النداء to_run و to_complete؛ يستدعيان في المكان نفسه حيث يستدعى ردي نداء المُنفِّذ المقابلين لهما، ولكن فقط عندما يبدأ التنفيذ الحالي بإعادة تحميل التطبيق. عندما لا تكون هناك حاجة إلى إعادة التحميل، سيستدعي الأمر إعادة تحميل الكتلة المغلفة دون أي استدعاءات أخرى.

إلغاء تحميل صنف

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

في كثير من الأحيان، يجب تنفيذ إجراءات إعادة التحميل الإضافية إما قبل أو بعد انتهاء تحميل الصنف تمامًا، لذلك يوفر معيد التحميل أيضًا ردي النداء before_class_unload و after_class_unload.

التزامن

يجب أن تستدعي عمليات "المستوى الأعلى" طويلة الأمد معيد التحميل Reloader، لأنه إذا دعت الحاجة إلى إعادة التحميل، فستُحظَر العملية إلى أن تنتهي جميع الخيوط من أي استدعاءات للمُنفِّذ.

إن كان هذا سيحدث في خيط فرعي (ابن)، مع انتظار الأب داخل المُنفِّذ، فسيتسبَّب في إحداث قفل ميت لا مفر منه: يجب أن تحدث إعادة التحميل قبل تنفيذ الخيط الفرعي (الابن)، ولكن لا يمكن تنفيذه بأمان في حين أن الخيط الرئيسي (الأب) هو منتصف التنفيذ. يجب أن تستخدم الخيوط الفرعية (الأبناء) المٌنفِّذ (Executor ) بدلًا من ذلك.

سلوك الإطار

تستخدم مكونات إطار ريلز هذه الأدوات لإدارة احتياجات التزامن الخاصة بهم أيضًا.

ActionDispatch::Executor و ActionDispatch::Reloader هي برمجيات Rack وسيطة تغلف الطلب مع مُنفِّذ (Executor) أو معيد تحميل (Reloader) معطى، على التوالي. وتُضمَّن تلقائيًا في مكدس التطبيقات الافتراضي. سيأكد معيد التحميل من تخديم أي طلب HTTP قادم ليقدم مع نسخة محملة حديثًا من التطبيق في حالة حدوث أي تغييرات في الشيفرة.

تغلف Active Job أيضًا عمليات التنفيذ مع معيد التحميل، وتحميل أحدث شيفرة لتنفيذ كل وظيفة يحين دورها في طابور الانتظار.

يستخدم Action Cable المُنفِّذ بدلًا من ذلك: نظرًا لأن اتصال الربط (Cable connection) مرتبط بنسخة محددة من صنفٍ، لا يمكن إعادة التحميل لكل رسالة قادمة من مقبس ويب websocket. على أية حال، يغلف معالج الرسالة فقط؛ لا يمنع اتصال الربط طويل الأمد عملية إعادة تحميل شُغلَت بواسطة طلب جديد أو مهمة واردة. بدلًا من ذلك، يستخدم "Action Cable" الاستدعاء before_class_unload الخاص بمعيد التحميل Reloader لفصل جميع اتصالاته. عندما يعيد العميل الاتصال تلقائيًا، سوف يتحدث إلى الإصدار الجديد من الشيفرة.

ما سبق هو نقاط الدخول إلى الإطار، لذا فهم مسؤولون عن ضمان حماية الخيوط المعنية لهم، وتحديد ما إذا كانت إعادة التحميل ضرورية أم لا. المكونات الأخرى تحتاج فقط إلى استخدام المُنفِّذ عند توليد خيوط إضافية.

الضبط

يقوم معيد التحميل Reloader بفحص تغييرات الملف فقط عندما تكون قيمة cache_classes هي false وتكون قيمة reload_classes_only_on_change هي true (وهو الإعداد الافتراضي في بيئة التطوير).

عندما تكون قيمة cache_classes هي true (في مرحلة الإنتاج، بشكل افتراضي)، فإن معيد التحميل يمر مباشرةً إلى المُنفِّذ.

المُنفِّذ لديه دائمًا عمل مهم للقيام به، مثل إدارة اتصال قاعدة البيانات. عندما تكون قيمة كل من cache_classes و eager_load هي true (في مرحلة الإنتاج)، لن يحدث أي تحميل ذاتي أو إعادة تحميل للصنف، لذلك لا تحتاج إلى تداخل التحميل (Load Interlock). إذا كانت قيمة أيًا منهما هي false (في مرحلة التطوير)، فسيستخدم المُنفِّذ تداخل التحميل (Load Interlock) لضمان تحميل الثوابت فقط عندما تكون آمنة.

تداخل التحميل

يسمح تداخل التحميل (Load Interlock) بتمكين التحميل التلقائي وإعادة التحميل في بيئة تشغيل متعددة الخيوط.

عند إجراء التحميل التلقائي لخيط واحد عن طريق تقييم تعريف الصنف من الملف المناسب، من المهم أن لا يواجه خيط آخر مرجعًا إلى ثابت معرف جزئيًّا.

وبالمثل، من الآمن فقط إجراء إلغاء تحميل / إعادة تحميل عندما لا يوجد شيفرة تطبيق في منتصف التنفيذ: بعد إعادة التحميل، قد يشير ثابت المستخدم، على سبيل المثال، إلى صنف مختلف. بدون هذه القاعدة، فإن إعادة التحميل في توقيت سيئ سيعني User.new.class == User أو حتى User == User قد يكون خطأ.

كل من هذين القيدين يعالجان عبر تداخل التحميل (Load Interlock). إنه يحتفظ بتتبع أي خيط يُشغل شيفرة التطبيق أو يحمِّل صنفًا أو يلغي تحميل الثوابت المحملة تلقائيًّا في الوقت الحالي.

قد يُحمِّل أو يُلغي تحميل خيط واحد فقط في كل مرة، وللقيام بذلك، يجب الانتظار حتى لا تُشغل أي خيوط عمليات أخرى لشيفرة التطبيق. إذا كان هناك خيط ينتظر إجراء تحميل، فإنَّه لا يمنع خيطًا آخر من التحميل (في الواقع، سوف يتعاونون، وينفِّذ كل منهم الحمل المخصص له في طابور الانتظار، قبل أن يستأنفوا العمل معًا).

permit_concurrent_loads

يكتسب المُنفِّذ Executor تلقائيًا قفل تشغيل (القفل running) طوال فترة الحظر، ويعرف التحميل التلقائي متى تجري الترقيةإلى قفل التحميل (القفل load)، ثم الرجوع إلى القفل running مرة أخرى بعد ذلك.

ومع ذلك، فإن عمليات المنع الأخرى التي تكون داخل كتلة المُنفِّذ (والتي تتضمن كل شيفرة التطبيق)، يمكنها الاحتفاظ بقفل running دون داع. إذا واجه خيط آخر ثابتًا يجب تحميله تلقائيًّا، يمكن أن يؤدي هذا إلى حالة قفل ميت.

على سبيل المثال، بافتراض أن المستخدم لم يُحمَّل بعد، سيؤدي المأزق التالي إلى:ra

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # الخيط الداخلي ينتظهر هنا. إن لا يستطيع
           # أثناء عمل خيط آخر User تحميل
    end
  end
 
  th.join # 'running' الخيط الخارجي ينتظر هنا ويحمل القفل
end

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

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # 'load' الخيط الداخلي يمكن أن يكسب القفل
           # ويكمل User ويحمل
    end
  end
 
  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    th.join # ينتظر القفل الخارجي هنا ولكن لا يملك أي قفل
  end
end

مثال آخر، باستخدام تزامن روبي:

Rails.application.executor.wrap do
  futures = 3.times.collect do |i|
    Concurrent::Future.execute do
      Rails.application.executor.wrap do
        # نفّذ العمل هنا
      end
    end
  end
 
  values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    futures.collect(&:value)
  end
end

ActionDispatch::DebugLocks

إذا كان التطبيق الخاص بك يعلق في أحد الأقفال الميتة وكنت تعتقد أن تداخل التحميل قد يكون متضمنًا، يمكنك إضافة الوسيط ActionDispatch::DebugLocks مؤقتًا إلى config/application.rb:

config.middleware.insert_before Rack::Sendfile,
                                  ActionDispatch::DebugLocks

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

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

مصادر