سؤال

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

ومع ذلك، كنت أبحث في System.Web.Security.Membership باستخدام Reflector ووجدت رمزًا مثل هذا:

public static class Membership
{
    private static bool s_Initialized = false;
    private static object s_lock = new object();
    private static MembershipProvider s_Provider;

    public static MembershipProvider Provider
    {
        get
        {
            Initialize();
            return s_Provider;
        }
    }

    private static void Initialize()
    {
        if (s_Initialized)
            return;

        lock(s_lock)
        {
            if (s_Initialized)
                return;

            // Perform initialization...
            s_Initialized = true;
        }
    }
}

لماذا تتم قراءة الحقل s_Initialized خارج القفل؟ألا يمكن أن يحاول موضوع آخر الكتابة إليه في نفس الوقت؟ هل عمليات القراءة والكتابة للمتغيرات ذرية؟

هل كانت مفيدة؟

المحلول

للحصول على إجابة نهائية انتقل إلى المواصفات.:)

ينص القسم الأول، القسم 12.6.6 من مواصفات واجهة سطر الأوامر (CLI):"يجب أن تضمن واجهة سطر الأوامر المطابقة أن الوصول للقراءة والكتابة إلى مواقع الذاكرة المحاذاة بشكل صحيح والتي لا يزيد حجمها عن حجم الكلمة الأصلية يكون ذريًا عندما تكون جميع عمليات الوصول للكتابة إلى موقع ما بنفس الحجم."

وهذا يؤكد أن s_Initialized لن يكون أبدًا غير مستقر، وأن القراءة والكتابة للأنواع الأولية الأصغر من 32 بت هي ذرية.

بخاصة، double و long (Int64 و UInt64) نكون لا مضمونة لتكون ذرية على منصة 32 بت.يمكنك استخدام الطرق الموجودة على Interlocked الطبقة لحماية هذه.

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

يؤدي التشابك إلى إنشاء حاجز للذاكرة لمنع المعالج من إعادة ترتيب عمليات القراءة والكتابة.يقوم القفل بإنشاء الحاجز الوحيد المطلوب في هذا المثال.

نصائح أخرى

هذا نموذج (سيئ) لنمط قفل الفحص المزدوج لا موضوع آمن في C #!

هناك مشكلة كبيرة في هذا الكود:

s_Initialized ليس متقلبًا.وهذا يعني أن عمليات الكتابة في رمز التهيئة يمكن أن تنتقل بعد تعيين s_Initialized على true ويمكن لسلاسل الرسائل الأخرى رؤية رمز غير مهيأ حتى لو كان s_Initialized صحيحًا بالنسبة لهم.لا ينطبق هذا على تطبيق Microsoft لإطار العمل لأن كل كتابة هي كتابة متقلبة.

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

على سبيل المثال:

Thread 1 reads s_Provider (which is null)  
Thread 2 initializes the data  
Thread 2 sets s\_Initialized to true  
Thread 1 reads s\_Initialized (which is true now)  
Thread 1 uses the previously read Provider and gets a NullReferenceException

يعد نقل قراءة s_Provider قبل قراءة s_Initialized أمرًا قانونيًا تمامًا لأنه لا توجد قراءة متقلبة في أي مكان.

إذا كانت s_Initialized متقلبة، فلن يُسمح لقراءة s_Provider بالتحرك قبل قراءة s_Initialized وأيضًا لا يُسمح لتهيئة الموفر بالتحرك بعد تعيين s_Initialized على true وكل شيء على ما يرام الآن.

كتب جو دافي أيضًا مقالًا حول هذه المشكلة: المتغيرات المكسورة عند القفل المزدوج

انتظر - السؤال الموجود في العنوان ليس بالتأكيد هو السؤال الحقيقي الذي يطرحه روري.

السؤال الفخري لديه إجابة بسيطة هي "لا" -- ولكن هذا لا يساعد على الإطلاق، عندما ترى السؤال الحقيقي -- والذي لا أعتقد أن أي شخص قد أعطى إجابة بسيطة عليه.

يتم تقديم السؤال الحقيقي الذي يطرحه روري في وقت لاحق وهو أكثر صلة بالمثال الذي يقدمه.

لماذا يتم قراءة الحقل s_initialized خارج القفل؟

الإجابة على ذلك بسيطة أيضًا، على الرغم من أنها لا علاقة لها تمامًا بذرية الوصول المتغير.

تتم قراءة الحقل s_Initialized خارج القفل لأنه الأقفال باهظة الثمن.

نظرًا لأن الحقل s_Initialized هو في الأساس "اكتب مرة واحدة"، فلن يُرجع أبدًا نتيجة إيجابية خاطئة.

من الاقتصادي قراءتها خارج القفل.

هذا ال تكلفة منخفضة النشاط مع أ عالي فرصة الحصول على فائدة.

ولهذا السبب تتم قراءته خارج القفل - لتجنب دفع تكلفة استخدام القفل ما لم تتم الإشارة إلى ذلك.

إذا كانت الأقفال رخيصة، فسيكون الرمز أبسط، وتجاهل هذا الفحص الأول.

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

يبدو أن الإجابة الصحيحة هي: "نعم، في الغالب".

  1. تشير إجابة John التي تشير إلى مواصفات CLI إلى أن الوصول إلى المتغيرات التي لا يزيد حجمها عن 32 بت على معالج 32 بت هو أمر ذري.
  2. مزيد من التأكيد من مواصفات C#، القسم 5.5، ذرية المراجع المتغيرة:

    القراءة والكتابة لأنواع البيانات التالية هي ذرية:الأنواع bool وchar وbyte وsbyte وshort وushort وuint وint وfloat والأنواع المرجعية.بالإضافة إلى ذلك، فإن عمليات القراءة والكتابة لأنواع التعداد ذات النوع الأساسي في القائمة السابقة هي أيضًا ذرية.لا يُضمن أن تكون عمليات القراءة والكتابة للأنواع الأخرى، بما في ذلك الأنواع الطويلة والمزدوجة والعشرية، بالإضافة إلى الأنواع المعرفة من قبل المستخدم، ذرية.

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

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

وظيفة التهيئة معيبة.يجب أن تبدو أكثر مثل هذا:

private static void Initialize()
{
    if(s_initialized)
        return;

    lock(s_lock)
    {
        if(s_Initialized)
            return;
        s_Initialized = true;
    }
}

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

قراءة وكتابة المتغيرات ليست ذرية.تحتاج إلى استخدام واجهات برمجة تطبيقات المزامنة لمحاكاة عمليات القراءة/الكتابة الذرية.

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

"هل الوصول إلى متغير في C# عملية ذرية؟"

لا.وهو ليس شيئًا يتعلق بـ C#، ولا حتى شيئًا يتعلق بـ .net، إنه شيء يتعلق بالمعالج.

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

يمكن أن تحدث "القراءات الممزقة" على أي قيمة يصل مجموع حقولها إلى أكثر من حجم المؤشر.

@ ليون
أفهم وجهة نظرك - الطريقة التي طرحتها، ثم علقت عليها، تسمح للسؤال بأن يتم تناوله بطريقتين مختلفتين.

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

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

خطأي.

يمكنك أيضًا تزيين s_Initialized بالكلمة الأساسية المتقلبة والتخلي عن استخدام القفل تمامًا.

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

أعتقد أنك تسأل إذا s_Initialized يمكن أن تكون في حالة غير مستقرة عند قراءتها خارج القفل.الجواب القصير هو لا.سوف تتلخص مهمة/قراءة بسيطة في تعليمات تجميع واحدة تكون ذرية في كل معالج يمكنني التفكير فيه.

لست متأكدًا من حالة تخصيص متغيرات 64 بت، فهذا يعتمد على المعالج، وأفترض أنه ليس ذريًا ولكن من المحتمل أن يكون موجودًا على معالجات 32 بت الحديثة وبالتأكيد على جميع معالجات 64 بت.لن يكون تخصيص أنواع القيم المعقدة ذريًا.

اعتقدت أنهم - لست متأكدًا من المغزى من القفل في المثال الخاص بك إلا إذا كنت تفعل شيئًا ما لـ s_Provider في نفس الوقت - فإن القفل سيضمن حدوث هذه المكالمات معًا.

يفعل ذلك //Perform initialization غطاء التعليق خلق s_Provider؟على سبيل المثال

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

وإلا فإن تلك الخاصية الثابتة ستعود خالية على أي حال.

ربما متشابكة يعطي فكرة.وعلى خلاف ذلك هذا أنا جيد جدًا.

كنت سأخمن أن هذه ليست ذرية.

لجعل التعليمات البرمجية الخاصة بك تعمل دائمًا على بنيات مرتبة بشكل ضعيف، يجب عليك وضع MemoryBarrier قبل كتابة s_Initialized.

s_Provider = new MemershipProvider;

// MUST PUT BARRIER HERE to make sure the memory writes from the assignment
// and the constructor have been wriitten to memory
// BEFORE the write to s_Initialized!
Thread.MemoryBarrier();

// Now that we've guaranteed that the writes above
// will be globally first, set the flag
s_Initialized = true;

عمليات الكتابة في الذاكرة التي تحدث في منشئ MembershipProvider ولا يمكن ضمان حدوث الكتابة إلى s_Provider قبل الكتابة إلى s_Initialized على معالج ضعيف الترتيب.

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

يحرر: في الواقع، أنا أخلط بين المنصات في تصريحاتي.في لغة C#، تتطلب مواصفات CLR أن تكون عمليات الكتابة مرئية عالميًا ومرتبة (باستخدام تعليمات المتجر باهظة الثمن لكل متجر إذا لزم الأمر).لذلك، لا تحتاج إلى وجود حاجز الذاكرة هذا بالفعل.ومع ذلك، إذا كان C أو C++ حيث لا يوجد مثل هذا الضمان لترتيب الرؤية العالمية، وقد يكون النظام الأساسي المستهدف الخاص بك به ذاكرة مرتبة بشكل ضعيف، وهو متعدد الخيوط، فستحتاج إلى التأكد من أن عمليات الكتابة المنشئة مرئية عالميًا قبل تحديث s_Initialized ، والذي تم اختباره خارج القفل.

ان If (itisso) { تحقق من منطقية ذرية ، ولكن حتى لو لم تكن هناك حاجة لقفل الشيك الأول.

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

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

ما تسأله هو ما إذا كان الوصول إلى الحقل بطريقة ذرية عدة مرات - والإجابة عليه هي لا.

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

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        if (s_Initialized)
            return;
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

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

يمكنك أيضًا تزيين s_Initialized بالكلمة الأساسية المتقلبة والتخلي عن استخدام القفل تمامًا.

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