تم التحقق من القفل مرتين
في هندسة البرمجيات ، يُعدّ التأمين المُدقّق مرتين (المعروف أيضًا باسم "تحسين التأمين المُدقّق مرتين" [ 1 ] ) نمطًا لتصميم البرمجيات يُستخدم لتقليل تكلفة الحصول على القفل عن طريق اختبار معيار التأمين ("تلميح التأمين") قبل الحصول عليه. ولا يتم التأمين إلا إذا أشار فحص معيار التأمين إلى ضرورة التأمين.
يُعاني الشكل الأصلي لهذا النمط، الذي ظهر في كتاب "لغات أنماط تصميم البرامج 3" [ 2 ] ، من تضارب البيانات ، وذلك تبعًا لنموذج الذاكرة المُستخدم، ويصعب تطبيقه بشكل صحيح. يعتبره البعض نمطًا سيئًا [ 3 ] . توجد أشكال صحيحة لهذا النمط، بما في ذلك استخدام الكلمة المفتاحية في لغة جافا وحواجز الذاكرة الصريحة في لغة سي++ [ 4 ] .volatile
يُستخدم هذا النمط عادةً لتقليل تكلفة التأمين عند تطبيق " التهيئة الكسولة " في بيئة متعددة الخيوط، وخاصةً كجزء من نمط Singleton . تتجنب التهيئة الكسولة تهيئة القيمة حتى يتم الوصول إليها لأول مرة.
الدافع والنمط الأصلي
على سبيل المثال، انظر إلى مقطع الكود هذا في لغة برمجة جافا : [ 4 ]
// نسخة أحادية الخيوط public class Foo { private static Bar bar ;public Bar bar () { if ( bar == null ) { bar = new Bar (); } return bar ; }// وظائف وأعضاء أخرى... }تكمن المشكلة في أن هذا لا ينجح عند استخدام عدة خيوط. يجب الحصول على قفل في حال استدعاء خيطين bar()في الوقت نفسه. وإلا، فقد يحاول كلاهما إنشاء الكائن في الوقت نفسه، أو قد ينتهي الأمر بأحدهما بالحصول على مرجع لكائن غير مهيأ بالكامل.
يمكن حل هذه المشكلة عن طريق المزامنة مع القفل، كما هو موضح في المثال التالي:
// نسخة متعددة الخيوط صحيحة ولكنها قد تكون مكلفة public class Foo { private Bar bar ;public synchronized Bar bar () { if ( bar == null ) { bar = new Bar (); } return bar ; }// وظائف وأعضاء أخرى... }هذا صحيح، ومن المرجح أن يكون الأداء كافيًا. مع ذلك، فإن الاستدعاء الأول للدالة bar()سيُنشئ الكائن، ولن تحتاج سوى الخيوط القليلة التي تحاول الوصول إليه خلال تلك الفترة إلى مزامنة synchronized؛ بعد ذلك، تحصل جميع الاستدعاءات على مرجع لمتغير العضو. بما أن مزامنة دالة ما قد تُقلل الأداء في بعض الحالات القصوى بمقدار 100 ضعف أو أكثر، [ 5 ] فإن عبء الحصول على قفل وتحريره في كل مرة تُستدعى فيها هذه الدالة بعد اكتمال التهيئة يبدو غير ضروري. وقد حاول العديد من المبرمجين، بمن فيهم مُبتكرو نمط تصميم القفل المُتحقق منه مرتين، تحسين هذا الوضع بالطريقة التالية:
- تحقق من تهيئة المتغير (دون الحصول على القفل). إذا تمت تهيئته، فأرجعه فوراً.
- احصل على القفل.
- تحقق جيدًا مما إذا كان المتغير قد تم تهيئته مسبقًا: إذا حصل خيط آخر على القفل أولًا، فربما يكون قد قام بالتهيئة بالفعل. في هذه الحالة، أعد المتغير المُهيأ.
- وإلا، قم بتهيئة المتغير وإعادته.
// نسخة متعددة الخيوط معطلة // اصطلاح "التأمين المزدوج" الأصلي public class Foo { private Bar bar ;public Bar bar () { if ( bar == null ) { synchronized ( this ) { if ( bar == null ) { bar = new Bar (); } } } return bar ; }// وظائف وأعضاء أخرى... }يبدو هذا الخوارزمية حلاً فعالاً للمشكلة، لكن إذا لم يُكتب النمط بعناية، فسيحدث تضارب في البيانات . على سبيل المثال، لننظر إلى تسلسل الأحداث التالي:
- يلاحظ الخيط A أن القيمة لم يتم تهيئتها، لذلك يحصل على القفل ويبدأ في تهيئة القيمة.
- بسبب دلالات بعض لغات البرمجة، يُسمح للكود المُولّد بواسطة المُصرّف بتحديث المتغير المشترك ليشير إلى كائن مُنشأ جزئيًا قبل أن تُنهي الدالة A عملية التهيئة. على سبيل المثال، في لغة جافا، إذا تم تضمين استدعاء الدالة البانية، فقد يتم تحديث المتغير المشترك فورًا بمجرد تخصيص مساحة التخزين ولكن قبل أن تُهيئ الدالة البانية المُضمنة الكائن. [ 6 ]
- يلاحظ الخيط B أن المتغير المشترك قد تم تهيئته (أو هكذا يبدو)، ويعيد قيمته. ولأن الخيط B يعتقد أن القيمة مهيأة بالفعل، فإنه لا يحصل على القفل. إذا استخدم B الكائن قبل أن يرى B جميع عمليات التهيئة التي أجراها A (إما لأن A لم ينتهِ من تهيئته أو لأن بعض القيم المهيأة في الكائن لم تصل بعد إلى الذاكرة التي يستخدمها B ( تناسق ذاكرة التخزين المؤقت ))، فمن المرجح أن يتعطل البرنامج.
تعتمد معظم بيئات التشغيل على حواجز الذاكرة أو غيرها من الوسائل لإدارة رؤية الذاكرة بين وحدات التنفيذ. وبدون فهم دقيق لسلوك اللغة في هذا المجال، يصعب تنفيذ الخوارزمية بشكل صحيح. من مخاطر استخدام آلية القفل المزدوج التحقق أن حتى أبسط تطبيق لها سيبدو ناجحًا في معظم الأحيان، إذ يصعب التمييز بين التطبيق الصحيح والتطبيق الذي يعاني من مشاكل دقيقة. اعتمادًا على المُصرّف ، وتداخل الخيوط بواسطة المُجدوِل، وطبيعة أنشطة النظام المتزامنة الأخرى ، قد تحدث حالات الفشل الناتجة عن تطبيق غير صحيح لآلية القفل المزدوج التحقق بشكل متقطع فقط. وقد يكون من الصعب إعادة إنتاج هذه الحالات.
الاستخدام في لغة C++
بالنسبة لنمط الكائن الأحادي، لا حاجة إلى تأمين مزدوج التحقق:
إذا دخل التحكم في الإعلان بشكل متزامن أثناء تهيئة المتغير، فيجب على التنفيذ المتزامن الانتظار حتى اكتمال عملية التهيئة.
— § 6.7 [stmt.dcl] ص4
Singleton & instance () { static Singleton s ; return s ; }توفر لغة C++11 والإصدارات الأحدث نمط قفل مزدوج التحقق مدمج على std::once_flagشكل std::call_once:
استيراد std ؛باستخدام std :: once_flag ؛ باستخدام std :: optional ؛class Singleton { private : Singleton () = default ;static optional < Singleton > inst ; static once_flag flag ; public : static Singleton * instance () { std :: call_once ( Singleton :: flag , [] -> void { inst . emplace ( Singleton ()); } ); return & inst ; } };إذا رغب المرء في تنفيذ أسلوب التحقق المزدوج الصريح والصحيح يدويًا بدلاً من المثال البسيط أعلاه (على سبيل المثال لأن Visual Studio قبل إصدار 2015 لم يطبق لغة معيار C++11 المتعلقة بالتهيئة المتزامنة المذكورة أعلاه [ 7 ] )، فإنه يحتاج على الأقل إلى عمليات ذرية مع ترتيب اكتساب/تحرير الذاكرة كما في هذا الكود (تعمل عبارات سياج الذاكرة الصريحة أيضًا ولكنها قد تستخدم تعليمات وحدة المعالجة المركزية الأبطأ على سبيل المثال على ARM64): [ 8 ]
استيراد std ؛باستخدام std :: atomic ؛ باستخدام std :: lock_guard ؛ باستخدام std :: memory_order ؛ باستخدام std :: mutex ؛class Singleton { private : Singleton () = default ;static atomic < Singleton * > inst ; static mutex m ; public : static Singleton * instance () { Singleton * s = inst.load ( memory_order :: acquire ); // التحقق الأول if ( ! s ) { lock_guard <mutex> lock ( m ) ; s = inst.load ( memory_order :: relaxed ); // التحقق الثاني ( المزدوج ) if ( ! s ) { s = new Singleton ( ) ; inst.store ( s , memory_order :: release ) ; } } return s ; }~ Singleton () { // منطق التنظيف } };الاستخدام في نظام POSIX
pthread_once()يجب استخدامها لتهيئة كود المكتبة (أو الوحدة الفرعية) عندما لا تحتوي واجهة برمجة التطبيقات الخاصة بها على إجراء تهيئة مخصص مطلوب استدعاؤه في وضع أحادي الخيوط.
الاستخدام في لغة Go
الحزمة الرئيسيةاستيراد "sync"var arrOnce sync . Once var arr [] int// تسترجع الدالة getArr المصفوفة arr، مع تهيئتها عند أول استدعاء. يتم التحقق المزدوج من القفل باستخدام دالة المكتبة sync.Once. أول روتين فرعي يفوز في سباق استدعاء Do() سيقوم بتهيئة المصفوفة، بينما // ستتوقف الروتينات الأخرى حتى يكتمل Do(). بعد تشغيل Do، ستكون هناك حاجة إلى مقارنة ذرية واحدة فقط للحصول على المصفوفة. func getArr () [] int { arrOnce . Do ( func () { arr = [] int { 0 , 1 , 2 } }) return arr }func main () { // بفضل آلية القفل المزدوجة، لن تتسبب محاولتان من نوع goroutines للوصول إلى getArr() // في حدوث تهيئة مزدوجة go getArr () go getArr () }الاستخدام في جافا
ابتداءً من J2SE 5.0 ، تم تعريف الكلمة المفتاحية volatile لإنشاء حاجز ذاكرة. يتيح ذلك حلاً يضمن معالجة عدة سلاسل ترابط لنسخة Singleton بشكل صحيح. تم شرح هذا الأسلوب الجديد فيو.
// يعمل مع دلالات الاقتناء/الإفراج للخاصية volatile في Java 1.5 والإصدارات الأحدث // لا يعمل في Java 1.4 والإصدارات الأقدم مع دلالات الخاصية volatile public class Foo { private volatile Bar bar ;public Bar bar () { Bar local = bar ; if ( bar == null ) { synchronized ( this ) { local = bar ; if ( local == null ) { bar = local = new Bar (); } } } return local ; }// وظائف وأعضاء أخرى... }لاحظ المتغير المحليlocal ، الذي يبدو غير ضروري. نتيجةً لذلك، في الحالات التي barيكون فيها المتغير مُهيأً مسبقًا (أي في معظم الأحيان)، لا يتم الوصول إلى الحقل المتقلب إلا مرة واحدة (بسبب استخدام return local;" بدلاً من return bar;")، مما قد يُحسّن الأداء العام للطريقة بنسبة تصل إلى 40%. [ 9 ]
قدمت Java 9 VarHandleفئة تسمح باستخدام العمليات الذرية المرنة للوصول إلى الحقول، مما يوفر قراءة أسرع نوعًا ما على الأجهزة ذات نماذج الذاكرة الضعيفة، على حساب آليات أكثر تعقيدًا وفقدان الاتساق التسلسلي (لم تعد عمليات الوصول إلى الحقول تشارك في ترتيب التزامن، وهو الترتيب العام للوصول إلى volatileالحقول). [ 10 ]
استيراد java.lang.invoke.MethodHandles ؛ استيراد java.lang.invoke.VarHandle ؛// يعمل مع دلالات الاستحواذ/الإفراج لمقابض المتغيرات التي تم تقديمها في Java 9 public class Foo { private volatile Bar bar ; private static final VarHandle BAR ;private Bar getBarAcquire ( ) { return ( Bar ) BAR.getAcquire ( this ) ; } private void setBarRelease ( Bar value ) { BAR.setRelease ( this , value ) ; }static { try { MethodHandles.Lookup lookup = MethodHandles.lookup ( ) ; BAR = lookup.findVarHandle ( Foo.class , " bar " , Bar.class ) ; } catch ( ReflectiveOperationException e ) { throw new ExceptionInInitializerError ( e ) ; } }public Bar bar () { Bar local = barAcquire (); if ( local == null ) { synchronized ( this ) { local = getBarAcquire (); if ( local == null ) { local = new Bar (); setBarRelease ( local ); } } } return local ; }// وظائف وأعضاء أخرى... }إذا كان الكائن المساعد ثابتًا (واحد لكل محمل فئة)، فإن البديل هو أسلوب حامل التهيئة عند الطلب [ 11 ] (انظر القائمة 16.6 [ 12 ] من النص المذكور سابقًا).
// تصحيح التهيئة الكسولة في جافا public class Foo { private static class Holder { public static final Bar INSTANCE = new Bar (); }public static Bar bar () { return Holder . INSTANCE ; } }يعتمد هذا على حقيقة أن الفئات المتداخلة لا يتم تحميلها إلا عند الإشارة إليها.
يمكن استخدام دلالات finalالحقول في Java 5 لنشر كائن المساعد بأمان دون استخدام volatile: [ 13 ]
class FinalWrapper <T> { public final T value ;public FinalWrapper ( T value ) { this . value = value ; } }public class Foo { private FinalWrapper < Bar > bar ;public Bar bar () { FinalWrapper < Bar > temp = bar ;إذا كان ( temp == null ) { synchronized ( this ) { إذا كان ( bar == null ) { bar = new FinalWrapper <Bar> ( new Bar ( ) ) ; } temp = bar ; } } return temp.value ; } }tempيُعدّ المتغير المحلي ضروريًا لضمان صحة العملية: فاستخدامه فقط barللتحقق من القيم الفارغة ولإرسال عبارة الإرجاع قد يؤدي إلى فشل العملية بسبب إعادة ترتيب القراءة المسموح بها في نموذج ذاكرة جافا. [ 14 ] ولا يُعدّ أداء هذا التنفيذ بالضرورة أفضل من volatileالتنفيذ السابق.
الاستخدام في لغة C#
في .NET Framework 4.0، تم تقديم الفئة، والتي تستخدم داخليًا القفل المزدوج التحقق بشكل افتراضي ( الوضع) لتخزين إما الاستثناء الذي تم طرحه أثناء الإنشاء، أو نتيجة الدالة التي تم تمريرها إلى : [ 15 ]Lazy<T>LazyThreadSafetyMode.ExecutionAndPublicationLazy<T>
باستخدام النظام ؛public class MySingleton { private static readonly Lazy <MySingleton> _mySingleton = new (() => new MySingleton ( ) );private MySingleton () { }public static MySingleton Instance => _mySingleton . Value ; }انظر أيضاً
- مصطلح " الاختبار والاختبار والضبط" لآلية قفل منخفضة المستوى.
- نموذج حامل التهيئة عند الطلب كبديل آمن للخيوط في جافا.
مراجع
- ↑ شميدت، د وآخرون. هندسة البرمجيات الموجهة بالأنماط، المجلد 2، 2000، الصفحات 353-363
- ↑ لغات أنماط تصميم البرامج. 3 (ملف PDF) ( تحرير: ناتشر). ريدينغ، ماساتشوستس: أديسون-ويسلي. 1998. ISBN 978-0201310115.
- ↑ غريغوار، مارك (24 فبراير 2021). لغة سي++ الاحترافية . جون وايلي وأولاده. ISBN 978-1-119-69545-5.
- 1 2 ديفيد بيكون وآخرون. إعلان "تم كسر القفل المزدوج التحقق" .
- ↑ بوهم، هانز-ج (يونيو 2005). "لا يمكن تنفيذ الخيوط كمكتبة" (ملف PDF) . إشعارات ACM SIGPLAN . 40 (6): 261-268 . doi : 10.1145/1064978.1065042 . مؤرشف من الأصل (ملف PDF) بتاريخ 30 مايو 2017. تم الاطلاع عليه بتاريخ 12 أغسطس 2014 .
- ↑ هاغار، بيتر (1 مايو 2002). "التأمين المزدوج التحقق ونمط Singleton" . شركة IBM. مؤرشف من الأصل بتاريخ 27 أكتوبر 2017. تم الاطلاع عليه بتاريخ 19 مايو 2022 .
- ↑ "دعم ميزات C++11-14-17 (لغة C++ الحديثة)" .
- ↑ تم إصلاح مشكلة القفل المزدوج في C++11
- ↑ بلوخ، جوشوا (2018). جافا الفعالة ( الطبعة الثالثة). أديسون-ويسلي. ص 335. ISBN 978-0-13-468599-1على جهازي ،
الطريقة المذكورة أعلاه أسرع بحوالي 1.4 مرة من النسخة الواضحة بدون متغير محلي.
- ↑ "الفصل 17. الخيوط والأقفال" . docs.oracle.com . تم الاطلاع عليه بتاريخ 28-07-2018 .
- ↑ برايان غوتز وآخرون، التزامن في جافا عمليًا، 2006، ص 348
- ↑ غوتز، برايان؛ وآخرون . "التزامن في جافا عمليًا - قوائم على موقع الويب" . تم الاسترجاع في 21 أكتوبر 2014 .
- ↑قائمة بريدية لمناقشة نموذج ذاكرة جافا
الصفحة غير موجودة - يرجى تحديث الرابط - ↑مانسون، جيريمي (14 ديسمبر 2008). "التهيئة الكسولة مع مراعاة تضارب البيانات لتحسين الأداء - التزامن في جافا (&c)" . تم الاطلاع عليه بتاريخ 3 ديسمبر 2016 .
- ↑ ألباهاري، جوزيف (2010). "الخيوط في لغة سي شارب: استخدام الخيوط" . سي شارب 4.0 باختصار . دار نشر أورايلي ميديا. رقم ISBN 978-0-596-80095-6في الواقع ،
يتم تطبيق آلية القفل المزدوج. يقوم القفل المزدوج بقراءة متغيرة إضافية لتجنب تكلفة الحصول على قفل إذا كان الكائن مهيأً بالفعل.
Lazy<T>
روابط خارجية
- تم توثيق مشاكل آلية القفل المزدوجة في مدونات جيو جورج.
- وصف "القفل المزدوج" من مستودع أنماط بورتلاند
- وصف "تم التحقق مرتين، القفل معطل" من مستودع أنماط بورتلاند
- ورقة بحثية بعنوان " لغة C++ ومخاطر القفل المزدوج " (475 كيلوبايت) من تأليف سكوت مايرز وأندريه ألكسندرسكو
- مقال بعنوان " القفل المُدقّق مرتين: ذكي، لكنه معيب " بقلم برايان غوتز
- مقال بعنوان " تحذير! تعدد الخيوط في عالم المعالجات المتعددة " بقلم ألين هولوب
- تم التحقق مرتين من القفل ونمط Singleton
- نمط الخيط الفردي وسلامة الخيط
- الكلمة المفتاحية volatile في VC++ 2005
- أمثلة جافا وتوقيت حلول القفل المزدوج
- "جافا أكثر فعالية مع جوشوا بلوخ من جوجل" .
- التحكم في التزامن
- أنماط تصميم البرمجيات
