Почему блокировка (это) {...} плохая?

документация MSDN говорит, что

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

является "проблемой, если к экземпляру можно получить доступ публично". Мне интересно, почему? Это потому, что замок будет удерживаться дольше, чем необходимо? Или есть еще более коварная причина?

Ответ 1

Плохая форма использования this в операторах блокировки, потому что обычно вы не можете контролировать, кто еще может блокировать этот объект.

Чтобы правильно планировать параллельные операции, следует проявлять особую осторожность, чтобы рассмотреть возможные ситуации взаимоблокировки, и наличие неизвестного количества точек входа в замок препятствует этому. Например, любой, кто ссылается на объект, может заблокировать его, не зная об этом конструктор/создатель объекта. Это увеличивает сложность многопоточных решений и может повлиять на их правильность.

Частное поле, как правило, является лучшим вариантом, поскольку компилятор будет применять к нему ограничения доступа, и он будет инкапсулировать механизм блокировки. Использование this нарушает инкапсуляцию, подвергая часть реализации блокировки публике. Также не ясно, что вы приобретете блокировку на this, если она не была задокументирована. Даже тогда, полагаясь на документацию, чтобы предотвратить проблему, является неоптимальным.

Наконец, существует общее заблуждение, что lock(this) фактически изменяет объект, переданный как параметр, и каким-то образом делает его доступным только для чтения или недоступным. Это false. Объект, переданный как параметр lock, просто служит ключом. Если блокировка уже удерживается на этом ключе, блокировка не может быть выполнена; в противном случае блокировка разрешена.

Вот почему плохо использовать строки в качестве ключей в операторах lock, поскольку они являются неизменяемыми и являются общими/доступными для разных частей приложения. Вместо этого вы должны использовать личную переменную, экземпляр Object будет делать красиво.

Выполните следующий код С# в качестве примера.

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

Потому что, если люди могут получить указатель на свой экземпляр объекта (т.е. ваш this), тогда они также могут попытаться заблокировать тот же объект. Теперь они могут не знать, что вы запираетесь на this внутренне, так что это может вызвать проблемы (возможно, тупик)

В дополнение к этому, это также плохая практика, потому что она блокирует "слишком много"

Например, у вас может быть переменная-член List<int>, и единственное, что вам нужно заблокировать, это переменная-член. Если вы заблокируете весь объект в своих функциях, то другие функции, вызывающие эти функции, будут заблокированы в ожидании блокировки. Если эти функции не нуждаются в доступе к списку участников, вы будете вынуждать другой код ждать и замедлять свое приложение без каких-либо причин.

Ответ 3

Взгляните на тему MSDN Синхронизация потоков (руководство по программированию на С#)

Как правило, лучше избегать блокировки по публичному типу или по объекту случаи, находящиеся вне выражение. Например, блокировка (это) может быть проблематичным, если экземпляр может получить доступ публично, потому что код вне вашего контроля может блокировать объект. Это может создать тупиковые ситуации, когда два или более потоки ожидают выхода тот же объект. Блокировка на публике тип данных, в отличие от объекта, может вызвать проблемы для одного и того же причина. Блокировка на литеральных строках особенно рискованно, потому что буквально строки интернированы общим (CLR). Это означает что есть один экземпляр любого заданный строковый литерал для всего программа, тот же самый объект представляет собой буквальный пробег доменов приложений, для всех потоков. В результате блокировка, помещенная на строку с тем же содержимым в любом месте процесс приложения блокирует все экземпляры этой строки в выражение. В результате это лучше всего для блокировки частного или защищенного члена который не интернирован. Некоторые классы предоставлять членам специально для замок. Тип массива, например, обеспечивает SyncRoot. Многие коллекции типы предоставляют член SyncRoot как хорошо.

Ответ 4

Я знаю, что это старый поток, но поскольку люди все еще могут это понять и полагаться на него, важно отметить, что lock(typeof(SomeObject)) значительно хуже, чем lock(this). Было сказано, что; искреннее признание Алану за указание, что lock(typeof(SomeObject)) - плохая практика.

Экземпляр System.Type является одним из наиболее общих, крупнозернистых объектов. По крайней мере, экземпляр System.Type является глобальным для AppDomain, а .NET может запускать несколько программ в AppDomain. Это означает, что две совершенно разные программы могут потенциально вызвать помехи друг в друге даже в случае создания взаимоблокировки, если они оба попытаются получить блокировку синхронизации на экземпляре того же типа.

Итак, lock(this) не особенно надежная форма, может вызвать проблемы и всегда должна поднимать брови по всем причинам, указанным. Тем не менее, широко используется, относительно хорошо уважаемый и, по-видимому, стабильный код, такой как log4net, который широко использует шаблон блокировки (этого), хотя я лично предпочел бы видеть, что изменение шаблона.

Но lock(typeof(SomeObject)) открывает совершенно новую и улучшенную банку червей.

За что это стоит.

Ответ 5

... и те же самые аргументы применимы и к этой конструкции:

lock(typeof(SomeObject))

Ответ 6

Представьте, что у вас есть квалифицированный секретарь в вашем офисе, что общий ресурс в отделе. Время от времени вы бросаетесь к ним, потому что у вас есть задача, только чтобы надеяться, что еще один из ваших сотрудников еще не потребовал их. Обычно вам остается только подождать короткое время.

Потому что забота об обмене, ваш менеджер решает, что клиенты могут напрямую использовать секретаря. Но это имеет побочный эффект: клиент может даже потребовать их, пока вы работаете с этим клиентом, и вам также понадобится выполнить часть задач. Замыкает тупик, потому что требование больше не является иерархией. Этого можно избежать, если не позволить клиентам требовать их в первую очередь.

lock(this) плохо, как мы видели. Внешний объект может блокировать объект, и поскольку вы не контролируете, кто использует этот класс, любой может заблокировать его... Какой именно пример, как описано выше. Опять же, решение заключается в ограничении воздействия объекта. Однако, если у вас есть класс private, protected или internal, который вы уже можете контролировать, кто блокирует ваш объект, потому что вы уверены, что сами создали свой код. Итак, сообщение здесь: не выставляйте его как public. Кроме того, обеспечение того, что блокировка используется в подобном сценарии, позволяет избежать взаимоблокировок.

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

Типы разделяются в домене приложений, как это указывается большинством людей. Но есть даже лучшие вещи, которые мы можем использовать: строки. Причина в том, что строки объединены. Другими словами: если у вас есть две строки, которые имеют одинаковое содержимое в домене приложения, есть вероятность, что они имеют тот же самый указатель. Поскольку указатель используется как ключ блокировки, то, что вы в основном получаете, является синонимом "готовятся к поведению undefined".

Аналогично, вы не должны блокировать объекты WCF, HttpContext.Current, Thread.Current, Singletons (в общем) и т.д. Самый простой способ избежать всего этого? private [static] object myLock = new object();

Ответ 7

Блокировка этого указателя может быть плохим, если вы блокируете общий ресурс. Общий ресурс может представлять собой статическую переменную или файл на вашем компьютере - то есть то, что разделяется между всеми пользователями класса. Причина в том, что этот указатель будет содержать другую ссылку на местоположение в памяти каждый раз, когда будет создан экземпляр вашего класса. Таким образом, блокировка этого в экземпляре экземпляра одного экземпляра отличается от блокировки по сравнению с другим экземпляром класса.

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

    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

Ответ 8

Есть очень хорошая статья об этом http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects Рико Мариани, архитектор производительности для среды выполнения Microsoft®.NET

Выдержки:

Основная проблема заключается в том, что у вас нет объекта типа, и вы не знаю, кто еще может получить к нему доступ. В общем, это очень плохая идея полагаться на блокировку объекта, который вы не создали, и не знаете, кто еще может быть доступен. Это делает тупик. Самый безопасный способ - блокировать только частные объекты.

Ответ 10

Потому что любой фрагмент кода, который может видеть экземпляр вашего класса, также может блокировать эту ссылку. Вы хотите скрыть (инкапсулировать) свой объект блокировки, чтобы ссылаться на него только на код, который должен ссылаться на него. Ключевое слово this относится к текущему экземпляру класса, поэтому любое количество вещей может иметь ссылку на него и может использовать его для синхронизации потоков.

Чтобы быть ясным, это плохо, потому что другой фрагмент кода может использовать экземпляр класса для блокировки и может помешать вашему коду получить своевременную блокировку или может создать другие проблемы синхронизации потоков. Лучший случай: ничто другое не использует ссылку на ваш класс для блокировки. Средний случай: что-то использует ссылку на ваш класс, чтобы делать блокировки, и это вызывает проблемы с производительностью. Худший случай: что-то использует ссылку вашего класса, чтобы делать блокировки, и это вызывает очень плохие, действительно тонкие, действительно трудно отлаживаемые проблемы.

Ответ 11

Вот пример кода, который проще всего отслеживать (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);
    }
}

Ответ 12

Пожалуйста, обратитесь к следующей ссылке, которая объясняет, почему блокировка (это) не является хорошей идеей.

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

Итак, решение состоит в том, чтобы добавить частный объект, например lockObject, в класс и поместить область кода внутри оператора блокировки, как показано ниже:

lock (lockObject)
{
...
}

Ответ 13

Извините парней, но я не могу согласиться с аргументом, что блокировка этого может привести к тупиковой ситуации. Вы смешиваете две вещи: тупик и голодание.

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

Здесь - изображение, которое иллюстрирует разницу.

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

Ответ 14

Возникнет проблема, если к экземпляру можно получить доступ публично, потому что могут быть другие запросы, которые могут использовать один и тот же экземпляр объекта. Лучше использовать частную/статическую переменную.