Чому замок (це) {…} поганий?


484

Документація MSDN говорить про це

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

є "проблема, якщо доступ до примірника може бути відкритим". Цікаво, чому? Це тому, що замок буде триматися довше, ніж потрібно? Або є якась більш підступна причина?

Відповіді:


508

Це погана форма для використання thisв операторах блокування, оскільки це, як правило, поза вашим контролем, хто ще може заблокувати цей об'єкт.

Для правильного планування паралельних операцій слід особливо обережно враховувати можливі ситуації з тупиком, а наявність невідомої кількості точок входу блокування перешкоджає цьому. Наприклад, будь-який із посиланням на об'єкт може заблокувати його, не знаючи про це дизайнера / творця об'єкта. Це збільшує складність багатопотокових рішень і може вплинути на їх правильність.

Приватне поле зазвичай є кращим варіантом, оскільки компілятор буде застосовувати обмеження доступу до нього, і він інкапсулює механізм блокування. Використання thisпорушує інкапсуляцію, викриваючи частину вашої реалізації блокування. Також незрозуміло, що ви придбаєте замок, thisякщо він не буде задокументований. Вже тоді, покладаючись на документацію для запобігання проблеми, є неоптимальним.

Нарешті, існує поширена помилкова думка, яка lock(this)фактично модифікує об'єкт, переданий як параметр, і певним чином робить його лише для читання або недоступним. Це помилково . Об'єкт, переданий як параметр, lockпросто служить ключем . Якщо блокування вже тримається на цій клавіші, блокування неможливо зробити; в іншому випадку блокування дозволено.

Ось чому погано використовувати рядки як ключі у lockвисловлюваннях, оскільки вони незмінні та поділяються / доступні в усіх частинах програми. Ви повинні використовувати приватну змінну замість цього, 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'.

2
Як я grok: (1) Ненсі знаходиться в thread1 з замком (це). (2) SAME Ненсі знаходиться в потоці2, поки вона все ще зафіксована в потоці1 - доказ блокованого об'єкта не є лише для читання. ТАКОЖ (2a), перебуваючи в потоці 2, цей об’єкт Ненсі також заблокований у Імені. (3) Створіть РІЗНИЙ об’єкт з тим же ім'ям . (4) Перейдіть до теми3 та спробуйте заблокувати ім’я. (великий фініш) АЛЕ "рядки незмінні", тобто будь-який об'єкт, на який посилається рядок "Ненсі Дрю", дивиться на буквально той самий рядковий екземпляр в пам'яті. Отже, object2 не може отримати блокування на рядок, коли object1 заблоковано за однаковим значенням
radarbob

Використання стандартної змінної замість lock(this)є стандартною порадою; важливо зауважити, що це, як правило, унеможливлює зовнішній код, через що блокування, пов’язане з об'єктом, переноситься між викликами методу. Це може бути, а може і не бути хорошою справою . Існує певна небезпека, що дозволяє зовнішньому коду тримати замок на довільну тривалість, і класи, як правило, повинні бути розроблені таким чином, щоб зробити таке використання непотрібним, але не завжди є практичні альтернативи. Як простий приклад, якщо колекція не реалізує власний метод ToArrayчи ToListметод ...
supercat

4
(На відміну від `методи розширення IEnumerable <T>), єдиний шлях для потоку , який хоче зробити знімок колекції , щоб отримати один може перерахувати його в той час блокування всі зміни . Для цього він повинен мати доступ до блокування, яке отримує будь-який код, який міняв би колекцію. Якщо не виставити блокування, можливо, неможливо, наприклад, програма періодично виконує асинхронний знімок колекції (наприклад, оновлення користувальницького інтерфейсу для перегляду колекції).
supercat

there is the common misconception that lock(this) actually modifies the object passed as a parameter, and in some way makes it read-only or inaccessible. This is false- Я вважаю, що ці розмови про біт SyncBlock в об'єкті CLR, тому формально це правильно - заблокуйте сам модифікований об'єкт
sll

@Esteban, я дуже люблю твій приклад, це приголомшливо. У мене є до Вас питання. Ваш код методу NameChange (..) закінчується на: <code> if (Monitor.TryEnter (person.Name, 10000)) {. . . } else Monitor.Exit (person.Name); </code> Якщо він не закінчується на: <code> if (Monitor.TryEnter (person.Name, 10000)) {. . . Monitor.Exit (person.Name); } </code>
AviFarah

64

Тому що, якщо люди можуть потрапити на ваш об'єкт (наприклад: ваш this) вказівник, вони також можуть спробувати заблокувати цей самий об’єкт. Тепер вони можуть не усвідомлювати, що ви thisвсередині блокуєтесь, тому це може спричинити проблеми (можливо, тупик)

На додаток до цього, це також погана практика, тому що вона блокується "занадто багато"

Наприклад, ви можете мати змінну члена List<int>, і єдине, що вам потрібно заблокувати, - це змінна член. Якщо ви заблокуєте весь об'єкт у своїх функціях, інші речі, які викликають ці функції, будуть заблоковані в очікуванні блокування. Якщо цим функціям не потрібно отримувати доступ до списку членів, ви змусите інший код чекати і сповільнити вашу програму без жодних причин.


44
Останній абзац цієї відповіді невірний. Блокування жодним чином не робить об'єкт неприступним чи доступним лише для читання. Блокування (це) не заважає іншому потоку викликати або змінювати об'єкт, на який посилається цей посилання.
Естебан Бренес

3
Це робиться, якщо інші методи, що викликаються, також роблять блокування (це). Я вважаю, що це він вирішив. Зверніть увагу на "Якщо ви заблокуєте весь об'єкт у своїй
функціїS

@Orion: Це зрозуміліше. @Herms: Так, але вам не потрібно використовувати "це" для досягнення цієї функціональності, наприклад, властивість SyncRoot у списках виконує цю мету, наприклад, при цьому чітко синхронізація повинна бути виконана за цим ключем.
Естебан Бренес

Re: блокування "занадто багато": Це тонкий балансуючий акт, який вирішує, що заблокувати. Майте на увазі, що зняття блокування пов'язане з кеш-процесом операцій з процесором і є дещо дорогим. Іншими словами: не блокуйте та не оновлюйте кожне окреме ціле число. :)
Зан Лінкс

Останній абзац все ще не має сенсу. Якщо вам потрібно лише обмежити доступ до списку, чому б інші функції мали блокування, якщо вони не мають доступу до списку?
Йоаким MH

44

Погляньте на синхронізацію теми MSDN з теми (Посібник з програмування C #)

Як правило, найкраще уникати блокування на загальнодоступному типі або на об'єктах, що не підпадають під контроль вашої програми. Наприклад, блокування (це) може бути проблематичним, якщо доступ до екземпляра може бути відкритим, оскільки код, що знаходиться поза вашим контролем, може також заблокувати об'єкт. Це може створити тупикові ситуації, коли два або більше потоків чекають виходу одного і того ж об'єкта. Блокування типу загальнодоступних даних, на відміну від об’єкта, може викликати проблеми з тієї ж причини. Блокування буквальних рядків є особливо ризикованим, оскільки буквальні рядки інтернуються загальною мовою виконання (CLR). Це означає, що є один екземпляр будь-якого заданого літерального рядка для всієї програми, точно той самий об'єкт представляє літерал у всіх запущених областях додатків у всіх потоках. Як результат, блокування, розміщене на рядку з однаковим вмістом у будь-якому місці процесу заявки, блокує всі екземпляри цього рядка в додатку. Як результат, найкраще заблокувати приватного або захищеного члена, який не інтернований. Деякі класи надають учасникам спеціально для блокування. Наприклад, тип масиву забезпечує SyncRoot. Багато типів колекцій також забезпечують член SyncRoot.


34

Я знаю, що це стара тема, але оскільки люди все ще можуть це переглянути і покластися на неї, важливо зазначити, що lock(typeof(SomeObject))це значно гірше, ніж lock(this). Сказавши, що; щирі кудо Алану за те, що вони зазначили, що lock(typeof(SomeObject))це погана практика.

Екземпляр System.Typeодного з найбільш загальних, крупнозернистих об'єктів. Принаймні, екземпляр System.Type є глобальним для AppDomain, і .NET може запускати кілька програм у AppDomain. Це означає, що дві абсолютно різні програми потенційно можуть спричиняти втручання одна в одну навіть у міру створення тупикової ситуації, якщо вони обидва намагаються отримати блокування синхронізації в одному і тому ж екземплярі типу.

Тому lock(this)не особливо міцна форма, може викликати проблеми і завжди повинна піднімати брови з усіх наведених причин. Тим не менш, є широко використовуваний, відносно шанований і, напевно, стабільний код, як log4net, який широко використовує схему блокування (цей), хоча я особисто вважаю за краще бачити цю зміну.

Але lock(typeof(SomeObject))відкриває зовсім нову та розширену банку глистів.

За свою ціну.


26

... і ці ж аргументи застосовуються і до цієї конструкції:

lock(typeof(SomeObject))

17
lock (typeof (SomeObject)) насправді набагато гірший, ніж lock (this) ( stackoverflow.com/a/10510647/618649 ).
Крейг

1
ну, блокування (Application.Current) ще гірше, але хто б спробував будь-яку з цих дурних речей? Блокування (це) здається логічним і стислим, але ці інші приклади цього не роблять.
Зар Шардан

Я не згоден, що lock(this)здається особливо логічним та лаконічним. Це надзвичайно грубо замок, і будь-який інший код може зняти замок на вашому об'єкті, що може спричинити втручання у ваш внутрішній код. Візьміть більш деталізовані замки та візьміть на себе жорсткіший контроль. Що lock(this)для цього потрібно - це набагато краще, ніж lock(typeof(SomeObject)).
Крейг

8

Уявіть, що у вас в офісі є кваліфікований секретар, який є спільним ресурсом у відділі. Час від часу ви кидаєтесь назустріч їм, тому що у вас є завдання, лише сподіватися, що інший з ваших колег вже не вимагав їх. Зазвичай вам потрібно лише почекати короткий проміжок часу.

Оскільки турботою є спільний доступ, ваш менеджер вирішує, що клієнти також можуть використовувати секретаря. Але це має побічний ефект: замовник може навіть вимагати їх, коли ви працюєте для цього замовника, і ви також потребуєте їх для виконання частини завдань. Трапляється тупик, тому що заявляти, що це більше не є ієрархією. Цього можна було б уникнути всім разом, не дозволяючи клієнтам в першу чергу вимагати їх.

lock(this)погано, як ми бачили. Зовнішній об’єкт може заблокувати об’єкт, і оскільки ви не контролюєте, хто використовує клас, кожен може заблокувати його ... Це точний приклад, як описано вище. Знову ж таки, рішення полягає в обмеженні експозиції об'єкта. Тим НЕ менше, якщо у вас є private, protectedабо internalклас , який ви вже могли контролювати , хто блокування вашого об'єкта , тому що ви впевнені , що ви написали свій код самостійно. Тож повідомлення тут: не піддавайте це як public. Крім того, забезпечення блокування в аналогічному сценарії дозволяє уникнути тупикових ситуацій.

Повна протилежність цьому - блокування ресурсів, які спільно використовуються в домені додатка - найгірший сценарій. Це як виносити свого секретаря на вулицю і дозволяти всім вимагати їх. Результат - повний хаос - або з точки зору вихідного коду: це була погана ідея; викиньте його і почніть спочатку. То як нам це зробити?

Типи поділяються у домені програми, як зазначає більшість людей тут. Але є ще кращі речі, які ми можемо використовувати: рядки. Причина в тому, що рядки об'єднані . Іншими словами: якщо у вас є два рядки з однаковим вмістом у домені програми, є ймовірність, що вони мають точно такий же покажчик. Оскільки вказівник використовується як ключ блокування, те, що ви в основному отримуєте, є синонімом «підготуватися до невизначеної поведінки».

Точно так само не слід блокувати об’єкти WCF, HttpContext.Current, Thread.Current, Singletons (загалом) тощо. Найпростіший спосіб уникнути всього цього? private [static] object myLock = new object();


3
Насправді наявність приватного класу це не заважає. Зовнішній код може отримати посилання на екземпляр приватного класу ...
Rashack

1
@Rashack, хоча ви технічно правильні (+1 для вказівки на це), я вважав, що ви повинні контролювати, хто заблокує екземпляр. Повернення подібних екземплярів це порушує.
атлас

4

Блокування цього вказівника може бути поганим, якщо ви блокуєтесь над спільним ресурсом . Загальний ресурс може бути статичною змінною або файлом на вашому комп'ютері, тобто тим, що ділиться між усіма користувачами класу. Причина полягає в тому, що цей покажчик буде містити різні посилання на місце в пам'яті кожного разу, коли ваш клас інстанціюється. Таким чином, блокування через це один раз екземпляр класу відрізняється блокування через це в іншому екземплярі класу.

Перевірте цей код, щоб побачити, що я маю на увазі. Додайте наступний код у свою основну програму в консольній програмі:

    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

1
що слід зазначити у вашому прикладі, як те, що ви показуєте, що невірно. важко помітити, що не так, коли ви використовуєте Random rand = new Random();nvm, я думаю, я бачу його повторний баланс
Seabizkit

3

Про це є дуже хороша стаття http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects Ріко Маріані, архітектор продуктивності Microsoft Microsoft .NET.

Витяг:

Основна проблема тут полягає в тому, що ви не володієте об'єктом типу і не знаєте, хто ще може отримати до нього доступ. Взагалі, дуже погана ідея покладатися на блокування об'єкта, який ви не створили, і не знати, хто ще може отримати доступ. Це запрошує тупик. Найбезпечніший спосіб - блокувати лише приватні об’єкти.



2

Ось набагато простіша ілюстрація (взята з питання 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/c-how-to-lock-without-deadlocks


Наскільки я бачу, коли я замінюю замок (це) на замок приватного члена примірника SomeClass, я все одно отримую той самий глухий кут. Крім того, якщо блокування в основному класі робиться на іншому приватному члені програми Program, відбувається той самий замок. Отже, не впевнений, чи ця відповідь не вводить в оману та неправильно. Дивіться таку поведінку тут: dotnetfiddle.net/DMrU5h
Bartosz

1

Тому що будь-який фрагмент коду, який може побачити екземпляр вашого класу, може також заблокувати цю посилання. Ви хочете приховати (інкапсулювати) ваш блокуючий об’єкт, щоб тільки по коду, на який потрібно посилатися, на нього можна було посилатися. Ключове слово це стосується поточного екземпляра класу, тому будь-яка кількість речей може мати посилання на нього і може використовувати його для синхронізації потоків.

Зрозуміло, що це погано, оскільки деякі інші фрагменти коду можуть використовувати екземпляр класу для блокування та може завадити коду отримати своєчасне блокування або може створити інші проблеми синхронізації потоку. Найкращий випадок: ніщо інше не використовує посилання на ваш клас для блокування. Середній регістр: щось використовує посилання на ваш клас, щоб робити блокування, і це спричиняє проблеми з продуктивністю. Найгірший випадок: щось використовує посилання вашого класу, щоб робити блокування, і це спричиняє дуже погані, дуже тонкі, дуже важкі проблеми налагодження.


1

Вибачте, хлопці, але я не можу погодитися з аргументом, що блокування цього може спричинити тупик. Вас плутають дві речі: тупик і голод.

  • Ви не можете скасувати тупик, не перервавши одну з ниток, тому після потрапляння в тупик ви не зможете вийти
  • Голодування закінчиться автоматично після того, як одна з ниток закінчить свою роботу

Ось малюнок, який ілюструє різницю.

Висновок
Ви все одно можете безпечно використовувати, lock(this)якщо голодування ниток не є проблемою для вас. Ви все ще повинні мати на увазі, що коли нитка, яка голодує за допомогою нитки, lock(this)закінчується у замці, у якого ваш предмет заблокований, він, нарешті, закінчиться вічним голодуванням;)


9
Існує різниця, але це абсолютно не має значення для цього обговорення. І перше речення вашого висновку - це неправильно.
Бен Войгт

1
Щоб було зрозуміло: я не захищаю lock(this)- такий код просто не так. Я просто думаю, що називати це тупиком - це трохи образливо.
SOReader

2
Посилання на зображення більше не доступне. :( Будь-який шанс ви можете повторно посилати на це? Thx
VG1

1

Перейдіть до наступного посилання, яке пояснює, чому блокування (це) не є хорошою ідеєю.

http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx

Таким чином, рішення полягає в тому, щоб додати приватний об'єкт, наприклад, lockObject до класу та розмістити область коду всередині оператора блокування, як показано нижче:

lock (lockObject)
{
...
}

посилання більше не діє.
Раульд

1

Ось приклад коду, який простіше дотримуватися (IMO): (Працюватиме в LinqPad , посилайтеся на такі простори імен: System.Net та System.Threading.Tasks)

Що слід пам’ятати, це те, що замок (x) в основному є синтаксичним цукром, і те, що він робить, це використовувати Monitor.Enter, а потім використовувати спробувати, ловити, нарешті блокувати, щоб викликати Monitor.Exit. Дивіться: https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter (розділ зауважень)

або використовувати оператор блокування C # (оператор SyncLock у Visual Basic), який обробляє способи Enter та Exit у спробі… нарешті блокувати.

void Main()
{
    //demonstrates why locking on THIS is BADD! (you should never lock on something that is publicly accessible)
    ClassTest test = new ClassTest();
    lock(test) //locking on the instance of ClassTest
    {
        Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Parallel.Invoke(new Action[]
        {
            () => {
                //this is there to just use up the current main thread. 
                Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
                },
            //none of these will enter the lock section.
            () => test.DoWorkUsingThisLock(1),//this will dead lock as lock(x) uses Monitor.Enter
            () => test.DoWorkUsingMonitor(2), //this will not dead lock as it uses Montory.TryEnter
        });
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        lock(this) //this can be bad if someone has locked on this already, as it will cause it to be deadlocked!
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
        }
        Console.WriteLine($"End ClassTest.DoWorkUsingThisLock Done {i}  CurrentThread {Thread.CurrentThread.ManagedThreadId}");
    }

    public void DoWorkUsingMonitor(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        if (Monitor.TryEnter(this))
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            Monitor.Exit(this);
        }
        else
        {
            Console.WriteLine($"Skipped lock section!  {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        }

        Console.WriteLine($"End ClassTest.DoWorkUsingMonitor Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine();
    }
}

Вихідні дані

CurrentThread 15
CurrentThread 15
Start ClassTest.DoWorkUsingMonitor 2 CurrentThread 13
Start ClassTest.DoWorkUsingThisLock 1 CurrentThread 12
Skipped lock section!  2 CurrentThread 13
End ClassTest.DoWorkUsingMonitor Done 2 CurrentThread 13

Зауважте, що нитка №12 ніколи не закінчується, оскільки її мертве заблоковано.


1
здається, що другий DoWorkUsingThisLockпотік не потрібен для ілюстрації проблеми?
Джек Лу

Ви не маєте на увазі зовнішній замок в основному, один потік просто чекатиме завершення іншого? що потім
скасує

@Seabizkit оновив код, щоб зробити його трохи зрозумілішим. Паралель є просто для створення нового потоку та запуску коду асинхронно. Насправді, другий потік можна було викликати будь-якою кількістю способів (натискання кнопки, окремий запит тощо).
Радж Рао

0

Ви можете встановити правило, яке говорить про те, що клас може мати код, який блокується на "це" або будь-який об'єкт, який містить код класу. Тож проблема лише в тому випадку, якщо шаблону не дотримуватися.

Якщо ви хочете захистити себе від коду, який не буде відповідати цій схемі, то прийнята відповідь правильна. Але якщо дотримуватися шаблону, це не проблема.

Перевага замка (це) - ефективність. Що робити, якщо у вас є простий "об'єкт значення", який містить одне значення. Це просто обгортка, і вона отримується миттєво мільйони разів. Вимагаючи створення приватного об’єкта синхронізації лише для блокування, ви в основному подвоїли розмір об'єкта і подвоїли кількість виділень. Коли важлива робота, це є перевагою.

Якщо вам не байдуже кількість виділень або слід пам'яті, уникати блокування (це) є кращим з причин, зазначених в інших відповідях.


-1

Виникне проблема, якщо доступ до екземпляра може бути відкритим, оскільки можуть бути інші запити, які можуть використовувати той самий об'єкт. Краще використовувати приватну / статичну змінну.


5
Не впевнений, що це додає людині, детальні відповіді вже існуючих, які говорять те саме.
Ендрю Барбер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.