سؤال

هل هناك طريقة لتنفيذ كائن مفرد في C++ وهي:

  1. تم إنشاؤه بتكاسل بطريقة آمنة للخيط (قد يكون هناك خيطان في نفس الوقت هما المستخدم الأول للمفردة - يجب أن يتم إنشاؤهما مرة واحدة فقط).
  2. لا يعتمد على المتغيرات الثابتة التي يتم إنشاؤها مسبقًا (وبالتالي فإن الكائن المفرد آمن للاستخدام أثناء إنشاء المتغيرات الثابتة).

(لا أعرف لغة C++ الخاصة بي جيدًا بما فيه الكفاية، ولكن هل تتم تهيئة المتغيرات الثابتة الثابتة والمتكاملة قبل تنفيذ أي تعليمات برمجية (على سبيل المثال، حتى قبل تنفيذ المُنشئات الثابتة - قد تكون قيمها "مهيأة" بالفعل في البرنامج صورة)؟إذا كان الأمر كذلك - فربما يمكن استغلال ذلك لتنفيذ كائن مفرد مفرد - والذي يمكن استخدامه بدوره لحماية إنشاء المفرد الحقيقي..)


ممتاز، يبدو أن لدي إجابتين جيدتين الآن (من المؤسف أنني لا أستطيع تحديد 2 أو 3 على أنها الاجابة).يبدو أن هناك حلين واسعين:

  1. استخدم التهيئة الثابتة (بدلاً من التهيئة الديناميكية) لمتغير POD الثابت، وقم بتنفيذ كائن المزامنة الخاص بي باستخدام التعليمات الذرية المضمنة.كان هذا هو نوع الحل الذي كنت ألمح إليه في سؤالي، وأعتقد أنني كنت أعرفه بالفعل.
  2. استخدم بعض وظائف المكتبة الأخرى مثل pthread_once أو دفعة::call_once.من المؤكد أنني لم أكن أعرف عنها شيئًا - وأنا ممتن جدًا للإجابات المنشورة.
هل كانت مفيدة؟

المحلول

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

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

من مراجعة 2003 لمعيار C++:

يجب أن تتم تهيئة الكائنات ذات مدة التخزين الثابتة (3.7.1) صفرًا (8.5) قبل إجراء أي تهيئة أخرى.تسمى التهيئة الصفرية والتهيئة بتعبير ثابت بشكل جماعي بالتهيئة الثابتة؛جميع عمليات التهيئة الأخرى هي تهيئة ديناميكية.يجب أن تتم تهيئة الكائنات من أنواع POD (3.9) مع مدة تخزين ثابتة بتعبيرات ثابتة (5.19) قبل إجراء أي تهيئة ديناميكية.يجب أن تتم تهيئة الكائنات ذات مدة التخزين الثابتة المحددة في نطاق مساحة الاسم في نفس وحدة الترجمة والتي تمت تهيئتها ديناميكيًا بالترتيب الذي يظهر به تعريفها في وحدة الترجمة.

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

ومع ذلك، هذا يفترض أنك يعرف أنك ستستخدم هذا المفرد أثناء التهيئة الثابتة.هذا أيضًا غير مضمون بالمعيار، لذلك إذا كنت تريد أن تكون آمنًا تمامًا، فاستخدم كائن المزامنة (mutex) الذي تمت تهيئته بشكل ثابت.

يحرر:من المؤكد أن اقتراح كريس باستخدام المقارنة والمبادلة الذرية سينجح.إذا لم تكن قابلية النقل مشكلة (وإنشاء أحرف مفردة مؤقتة إضافية لا يمثل مشكلة)، فهذا حل أقل تكلفة قليلاً.

نصائح أخرى

لسوء الحظ، تتميز إجابة مات بما يسمى قفل مزدوج التحقق وهو غير مدعوم من قبل طراز الذاكرة C/C++.(وهو مدعوم بواسطة Java 1.5 والإصدارات الأحدث - وأعتقد .NET - نموذج الذاكرة.) وهذا يعني أنه بين الوقت الذي يتم فيه pObj == NULL يتم التحقق وعندما يتم الحصول على القفل (mutex)، pObj ربما تم تعيينها بالفعل على موضوع آخر.يحدث تبديل الخيط عندما يريد نظام التشغيل ذلك، وليس بين "أسطر" البرنامج (والتي ليس لها أي معنى بعد التجميع في معظم اللغات).

علاوة على ذلك، كما يعترف مات، فهو يستخدم int كقفل بدلاً من نظام تشغيل بدائي.لا تفعل ذلك.تتطلب الأقفال الصحيحة استخدام تعليمات حاجز الذاكرة، وربما عمليات مسح سطر ذاكرة التخزين المؤقت، وما إلى ذلك؛استخدم أساسيات نظام التشغيل الخاص بك للقفل.وهذا مهم بشكل خاص لأن البدائيات المستخدمة يمكن أن تتغير بين خطوط وحدة المعالجة المركزية الفردية التي يعمل عليها نظام التشغيل الخاص بك؛ما يعمل على وحدة المعالجة المركزية Foo قد لا يعمل على وحدة المعالجة المركزية Foo2.تدعم معظم أنظمة التشغيل سلاسل عمليات POSIX (pthreads) بشكل أصلي أو تقدمها كمجمّع لحزمة سلاسل عمليات نظام التشغيل، لذلك من الأفضل غالبًا توضيح الأمثلة على استخدامها.

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

MySingleton *MySingleton::GetSingleton() {
    if (pObj == NULL) {
        // create a temporary instance of the singleton
        MySingleton *temp = new MySingleton();
        if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
            // if the swap didn't take place, delete the temporary instance
            delete temp;
        }
    }

    return pObj;
}

يعمل هذا فقط إذا كان من الآمن إنشاء مثيلات متعددة للمفردة الخاصة بك (واحدة لكل مؤشر ترابط يحدث لاستدعاء GetSingleton() في وقت واحد)، ثم التخلص من الإضافات.ال OSAtomicCompareAndSwapPtrBarrier الوظيفة المتوفرة في نظام التشغيل Mac OS X - توفر معظم أنظمة التشغيل وظيفة بدائية مماثلة - تتحقق مما إذا كان pObj يكون NULL ويضبطه فعليًا فقط temp إليه إذا كان.يستخدم هذا دعم الأجهزة لإجراء عملية المبادلة فعليًا فقط مرة واحدة ومعرفة ما إذا كان قد حدث.

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

إليك أداة فردية بسيطة جدًا تم إنشاؤها بتكاسل:

Singleton *Singleton::self() {
    static Singleton instance;
    return &instance;
}

هذا كسول، ويتطلب معيار C++ التالي (C++0x) أن يكون آمنًا.في الواقع، أعتقد أن g++ على الأقل ينفذ هذا بطريقة آمنة.لذلك إذا كان هذا هو المترجم المستهدف الخاص بك أو إذا كنت تستخدم برنامجًا مترجمًا يقوم أيضًا بتنفيذ ذلك بطريقة آمنة لمؤشر الترابط (ربما يقوم برنامج التحويل البرمجي الأحدث لـ Visual Studio بذلك؟لا أعرف)، فقد يكون هذا هو كل ما تحتاجه.

انظر أيضا http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html حول هذا الموضوع.

لا يمكنك فعل ذلك بدون أي متغيرات ثابتة، ولكن إذا كنت على استعداد لتحمل أحدها، فيمكنك استخدامه Boost.Thread لهذا الغرض.اقرأ قسم "التهيئة لمرة واحدة" لمزيد من المعلومات.

ثم في وظيفة الوصول الفردي، استخدم boost::call_once لبناء الكائن وإعادته.

بالنسبة لدول مجلس التعاون الخليجي، هذا أمر سهل إلى حد ما:

LazyType* GetMyLazyGlobal() {
    static const LazyType* instance = new LazyType();
    return instance;
}

سوف تتأكد دول مجلس التعاون الخليجي من أن التهيئة ذرية. بالنسبة لـ VC++، هذا ليس هو الحال. :-(

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

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

رغم أن هذا السؤال قد تمت الإجابة عليه بالفعل، أعتقد أن هناك بعض النقاط الأخرى التي يجب الإشارة إليها:

  • إذا كنت تريد إنشاء مثيل كسول للمفردة أثناء استخدام مؤشر لمثيل مخصص ديناميكيًا، فسيتعين عليك التأكد من تنظيفه عند النقطة الصحيحة.
  • يمكنك استخدام حل مات، لكنك ستحتاج إلى استخدام قسم كائن المزامنة/الحرج المناسب للقفل، وعن طريق تحديد "pObj == NULL" قبل القفل وبعده.بالطبع، pObj يجب أن يكون كذلك ثابتة ؛).سيكون كائن المزامنة (mutex) ثقيلًا بلا داعٍ في هذه الحالة، ومن الأفضل أن تستخدم قسمًا مهمًا.

ولكن كما ذكرنا سابقًا، لا يمكنك ضمان التهيئة البطيئة الآمنة لمؤشر الترابط دون استخدام بدائية مزامنة واحدة على الأقل.

يحرر:نعم ديريك، أنت على حق.خطأي.:)

يمكنك استخدام حل مات، لكنك ستحتاج إلى استخدام قسم كائن المزامنة/الحرج المناسب للقفل، وعن طريق تحديد "pObj == NULL" قبل القفل وبعده.بالطبع، يجب أن يكون pObj أيضًا ثابتًا ;) .سيكون كائن المزامنة (mutex) ثقيلًا بلا داعٍ في هذه الحالة، ومن الأفضل أن تستخدم قسمًا مهمًا.

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

يحرر:لا مشكلة، الجريدة الرسمية.إنه أمر رائع حقًا في اللغات التي يعمل فيها.أتوقع أنه سيعمل في C++0x (على الرغم من أنني لست متأكدًا)، لأنه مصطلح مناسب.

  1. القراءة على نموذج الذاكرة الضعيفة.يمكنه كسر الأقفال ذات الفحص المزدوج والأقفال الدوارة.Intel هي نموذج ذاكرة قوي (حتى الآن)، لذا فالأمر أسهل بالنسبة لـ Intel

  2. استخدم كلمة "volatile" بعناية لتجنب التخزين المؤقت لأجزاء الكائن في السجلات، وإلا فستكون قد قمت بتهيئة مؤشر الكائن، ولكن ليس الكائن نفسه، وسيتعطل مؤشر الترابط الآخر

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

  4. من الصعب تدمير مثل هذه الأشياء بشكل صحيح

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

أفترض أن أقول لا تفعل هذا لأنه غير آمن ومن المحتمل أن يتعطل أكثر من مجرد تهيئة هذه الأشياء main() لن تحظى بهذه الشعبية

(ونعم، أعلم أن اقتراح ذلك يعني أنه لا ينبغي عليك محاولة القيام بأشياء مثيرة للاهتمام في مُنشئي الكائنات العالمية.هذا هو بيت القصيد.)

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top