Почему lock (this) {...} плохой?

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

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

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

15 ответов


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

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

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

и, наконец, существует распространенное заблуждение, что lock(this) фактически изменяет объект, переданный в качестве параметра, и каким-то образом делает его доступным только для чтения или недоступным. Это false. Объект передается в качестве параметра в 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'.

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

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

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


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

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


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

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

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

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

для чего это стоит.


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

lock(typeof(SomeObject))

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

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

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


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

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

    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 Рико Мариани, архитектор производительности для Microsoft ® .NET runtime

выдержка:

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


здесь также есть хорошая дискуссия об этом:это правильное использование мьютекса?


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

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


вот пример кода, который проще следовать (IMO): (будет работать в помощью linqpad, ссылка на следующие пространства имен: System.Net и система.Нарезка резьбы.Задачи)

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/b/bclteam/archive/2004/01/20/60719.aspx

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

lock (lockObject)
{
...
}

вот гораздо более простая иллюстрация (взято из вопрос 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();
        }

чтобы обойти, этот парень использовал нить.TryMonitor (с таймаутом) вместо lock:

            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


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

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

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

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


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