سؤال

ال وثائق MSDN يقول ان

public class SomeObject
{
  public void SomeOperation()
  {
    lock(this)
    {
      //Access instance variables
    }
  }
}

هي "مشكلة إذا كان من الممكن الوصول إلى المثيل بشكل عام".أنا أتساءل لماذا؟هل لأن القفل سيبقى لفترة أطول من اللازم؟أم أن هناك سببا أكثر غدرا؟

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

المحلول

إنه شكل سيء للاستخدام this في عبارات القفل لأنه خارج عن سيطرتك عمومًا، من قد يقوم بتأمين هذا الكائن.

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

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

وأخيرا، هناك فكرة خاطئة شائعة مفادها أن lock(this) يقوم فعليًا بتعديل الكائن الذي تم تمريره كمعلمة، ويجعله بطريقة ما للقراءة فقط أو لا يمكن الوصول إليه.هذا هو خطأ شنيع.تم تمرير الكائن كمعلمة إلى lock مجرد بمثابة مفتاح.إذا كان هناك قفل بالفعل على هذا المفتاح، فلا يمكن إجراء القفل؛وإلا يُسمح بالقفل.

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

قم بتشغيل كود C# التالي كمثال.

public class Person
{
    public int Age { get; set;  }
    public string Name { get; set; }

    public void LockThis()
    {
        lock (this)
        {
            System.Threading.Thread.Sleep(10000);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var nancy = new Person {Name = "Nancy Drew", Age = 15};
        var a = new Thread(nancy.LockThis);
        a.Start();
        var b = new Thread(Timewarp);
        b.Start(nancy);
        Thread.Sleep(10);
        var anotherNancy = new Person { Name = "Nancy Drew", Age = 50 };
        var c = new Thread(NameChange);
        c.Start(anotherNancy);
        a.Join();
        Console.ReadLine();
    }

    static void Timewarp(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // A lock does not make the object read-only.
        lock (person.Name)
        {
            while (person.Age <= 23)
            {
                // There will be a lock on 'person' due to the LockThis method running in another thread
                if (Monitor.TryEnter(person, 10) == false)
                {
                    Console.WriteLine("'this' person is locked!");
                }
                else Monitor.Exit(person);
                person.Age++;
                if(person.Age == 18)
                {
                    // Changing the 'person.Name' value doesn't change the lock...
                    person.Name = "Nancy Smith";
                }
                Console.WriteLine("{0} is {1} years old.", person.Name, person.Age);
            }
        }
    }

    static void NameChange(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // You should avoid locking on strings, since they are immutable.
        if (Monitor.TryEnter(person.Name, 30) == false)
        {
            Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string \"Nancy Drew\".");
        }
        else Monitor.Exit(person.Name);

        if (Monitor.TryEnter("Nancy Drew", 30) == false)
        {
            Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!");
        }
        else Monitor.Exit("Nancy Drew");
        if (Monitor.TryEnter(person.Name, 10000))
        {
            string oldName = person.Name;
            person.Name = "Nancy Callahan";
            Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name);
        }
        else Monitor.Exit(person.Name);
    }
}

إخراج وحدة التحكم

'this' person is locked!
Nancy Drew is 16 years old.
'this' person is locked!
Nancy Drew is 17 years old.
Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew".
'this' person is locked!
Nancy Smith is 18 years old.
'this' person is locked!
Nancy Smith is 19 years old.
'this' person is locked!
Nancy Smith is 20 years old.
Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!
'this' person is locked!
Nancy Smith is 21 years old.
'this' person is locked!
Nancy Smith is 22 years old.
'this' person is locked!
Nancy Smith is 23 years old.
'this' person is locked!
Nancy Smith is 24 years old.
Name changed from 'Nancy Drew' to 'Nancy Callahan'.

نصائح أخرى

ولأنه إذا كان يمكن للناس الحصول على مثيل الكائن الخاص بك (أي: this الخاص بك) المؤشر، ثم يمكنهم أيضا محاولة لقفل هذا الكائن نفسه. الآن أنها قد لا تكون على علم بأنك تأمين على this داخليا، وحتى هذا قد يسبب مشاكل (ربما الى طريق مسدود)

وبالإضافة إلى ذلك، كما انها ممارسة سيئة، لأنه تأمين "أكثر من اللازم"

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

ألق نظرة على موضوع MSDN تزامن الخيط (دليل البرمجة C#)

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

وأنا أعلم أن هذا هو موضوع قديم، ولكن لأن الناس لا تزال تبدو هذا الأمر وتعتمد على ذلك، يبدو من المهم أن نشير إلى أن lock(typeof(SomeObject)) هو أسوأ بكثير من lock(this). وقد قلت ذلك؛ مجد الصادقة إلى آلان لافتا إلى أن lock(typeof(SomeObject)) هو ممارسة سيئة.

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

وهكذا lock(this) ليس شكل قوي بشكل خاص، يمكن أن يسبب المشاكل ويجب دائما رفع الحاجبين لجميع الأسباب المذكورة. ومع ذلك، هناك يستخدم على نطاق واسع، نسبيا بشكل جيد محترم ورمز مستقر على ما يبدو مثل log4net التي يستخدمها القفل (هذا) نمط نطاق واسع، على الرغم من أنني شخصيا أفضل أن نرى أن تغيير نمط.

ولكن lock(typeof(SomeObject)) يفتح علبة جديدة ومحسنة كاملة من الديدان.

لما يستحق.

... وتنطبق نفس الحجج الدقيقة عن هذا التصور أيضا:

lock(typeof(SomeObject))

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

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

وlock(this) هو سيء كما رأينا. قد قفل كائن الخارجي على كائن وبما انك لا تتحكم الشخص الذي يستخدم الطبقة، يمكن لأي شخص قفل على ذلك ... ما هو المثال بالضبط كما هو موضح أعلاه. مرة أخرى، والحل هو للحد من التعرض للكائن. ومع ذلك، إذا كان لديك private، protected أو internal درجة التي <م> يمكن السيطرة بالفعل الذي قفل على وجوه الخاص بك ، لأنك على يقين من أن كنت قد كتبت كود بنفسك. وبالتالي فإن الرسالة هنا هي: لا تعرضها كما public. أيضا، وضمان أن يتم استخدام قفل في سيناريو مشابه ليتجنب المآزق.

والعكس تماما من ذلك هو قفل على الموارد المشتركة عبر المجال التطبيق - أسوأ سيناريو. انها مثل وضع السكرتير الخاص بك خارج والسماح الجميع هناك على المطالبة بها. والنتيجة هي فوضى مطلقة - أو من حيث شفرة المصدر: انها فكرة سيئة. رميها بعيدا والبدء من جديد. فكيف نفعل ذلك؟

ويجري تقاسم

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

وبالمثل، يجب أن لا قفل على الأشياء WCF، HttpContext.Current، Thread.Current، سنغلتونس (بشكل عام)، وما أسهل طريقة لتجنب كل هذا؟ private [static] object myLock = new object();

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

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

    static void Main(string[] args)
    {
         TestThreading();
         Console.ReadLine();
    }

    public static void TestThreading()
    {
        Random rand = new Random();
        Thread[] threads = new Thread[10];
        TestLock.balance = 100000;
        for (int i = 0; i < 10; i++)
        {
            TestLock tl = new TestLock();
            Thread t = new Thread(new ThreadStart(tl.WithdrawAmount));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
        Console.Read();
    }

وإنشاء فئة جديدة مثل أدناه.

 class TestLock
{
    public static int balance { get; set; }
    public static readonly Object myLock = new Object();

    public void Withdraw(int amount)
    {
      // Try both locks to see what I mean
      //             lock (this)
       lock (myLock)
        {
            Random rand = new Random();
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
            }
            else
            {
                Console.WriteLine("Can't process your transaction, current balance is :  " + balance + " and you tried to withdraw " + amount);
            }
        }

    }
    public void WithdrawAmount()
    {
        Random rand = new Random();
        Withdraw(rand.Next(1, 100) * 100);
    }
}

وهنا يتم تشغيل البرنامج قفل على هذه .

   Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  94400
    Balance before Withdrawal :  100000
    Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  88800
    Withdraw        : -5600
    Balance after Withdrawal  :  83200
    Balance before Withdrawal :  83200
    Withdraw        : -9100
    Balance after Withdrawal  :  74100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance after Withdrawal  :  55900
    Balance after Withdrawal  :  65000
    Balance before Withdrawal :  55900
    Withdraw        : -9100
    Balance after Withdrawal  :  46800
    Balance before Withdrawal :  46800
    Withdraw        : -2800
    Balance after Withdrawal  :  44000
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  41200
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  38400

وهنا يتم تشغيل البرنامج قفل على myLock .

Balance before Withdrawal :  100000
Withdraw        : -6600
Balance after Withdrawal  :  93400
Balance before Withdrawal :  93400
Withdraw        : -6600
Balance after Withdrawal  :  86800
Balance before Withdrawal :  86800
Withdraw        : -200
Balance after Withdrawal  :  86600
Balance before Withdrawal :  86600
Withdraw        : -8500
Balance after Withdrawal  :  78100
Balance before Withdrawal :  78100
Withdraw        : -8500
Balance after Withdrawal  :  69600
Balance before Withdrawal :  69600
Withdraw        : -8500
Balance after Withdrawal  :  61100
Balance before Withdrawal :  61100
Withdraw        : -2200
Balance after Withdrawal  :  58900
Balance before Withdrawal :  58900
Withdraw        : -2200
Balance after Withdrawal  :  56700
Balance before Withdrawal :  56700
Withdraw        : -2200
Balance after Withdrawal  :  54500
Balance before Withdrawal :  54500
Withdraw        : -500
Balance after Withdrawal  :  54000

وهناك مادة جيدة للغاية حول هذا HTTP: //bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects التي كتبها ريكو مارياني، مهندس أداء لوقت التشغيل مايكروسوفت. NET

مقتطف:

<اقتباس فقرة>   

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

وهناك أيضا بعض المناقشات الجيدة حول هذا هنا: هل هذا هو الصحيح استخدام مزامنة؟

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

<اقتباس فقرة>
class SomeClass
{
    public void SomeMethod(int id)
    {
        **lock(this)**
        {
            while(true)
            {
                Console.WriteLine("SomeClass.SomeMethod #" + id);
            }
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        SomeClass o = new SomeClass();

        lock(o)
        {
            for (int threadId = 0; threadId < 3; threadId++)
            {
                Thread t = new Thread(() => {
                    o.SomeMethod(threadId);
                        });
                t.Start();
            }

            Console.WriteLine();
        }

لتغلب، وتستخدم هذا الرجل Thread.TryMonitor (مع مهلة) بدلا من القفل:

<اقتباس فقرة>
            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
            if (lockWasTaken)
            {
                doAction();
            }
            else
            {
                throw new Exception("Could not get lock");
            }

https://blogs.appbeat.io/post / ج-كيف لقفل من دون-المآزق

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

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

وهنا بعض نموذج التعليمات البرمجية التي هي أبسط لمتابعة (IMO): (سوف تعمل في <قوية> LinqPad ، أو الإشارة التالية النطاقات: System.Net وSystem.Threading.Tasks)

void Main()
{
    ClassTest test = new ClassTest();
    lock(test)
    {
        Parallel.Invoke (
            () => test.DoWorkUsingThisLock(1),
            () => test.DoWorkUsingThisLock(2)
        );
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine("Before ClassTest.DoWorkUsingThisLock " + i);
        lock(this)
        {
            Console.WriteLine("ClassTest.DoWorkUsingThisLock " + i);
            Thread.Sleep(1000);
        }
        Console.WriteLine("ClassTest.DoWorkUsingThisLock Done " + i);
    }
}

يرجى الرجوع إلى الرابط التالي وهو ما يفسر لماذا القفل (هذا) ليست فكرة جيدة.

http://blogs.msdn.com/ ب / bclteam / أرشيف / 2004/01/20 / 60719.aspx

وبالتالي فإن الحل هو إضافة كائن الخاص، على سبيل المثال، lockObject إلى فئة ووضع رمز المنطقة داخل بيان القفل كما هو موضح أدناه:

lock (lockObject)
{
...
}

آسف يا شباب ولكن لا أستطيع أن أتفق مع الحجة القائلة بأن قفل هذا قد يسبب طريق مسدود.أنت تخلط بين أمرين:الجمود والجوع.

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

هنا هي الصورة التي توضح الفرق.

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

يمكنك إنشاء قاعدة تقول أن فئة يمكن أن يكون الرمز الذي أقفال على 'هذا' أو أي كائن أن التعليمات البرمجية في فئة instantiates. لذلك ليست سوى المشكلة إذا لم يتم اتباع النمط.

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

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

وعندما لا يهتمون عدد من مخصصات أو أثر الذاكرة، وتجنب القفل (هذا) هو الأفضل للأسباب المشار إليها في إجابات أخرى.

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

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