[C#] lock (this) {…}이 나쁜 이유는 무엇입니까?

MSDN 설명서를 말한다

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

“인스턴스에 공개적으로 액세스 할 수있는 경우 문제”입니다. 왜 궁금해? 잠금이 필요 이상으로 오래 걸리기 때문입니까? 아니면 더 교활한 이유가 있습니까?



답변

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'.


답변

사람들이 객체 인스턴스 (예 🙂 this포인터를 얻을 수 있으면 동일한 객체를 잠그려고 시도 할 수도 있습니다. 이제 그들은 당신이 this내부에서 잠겨 있다는 것을 알지 못할 수도 있으므로 문제가 발생할 수 있습니다 (교착 상태)

이 외에도 “너무 많이”잠겨 있기 때문에 나쁜 습관입니다.

예를 들어의 멤버 변수가있을 수 있으며 List<int>실제로 잠그 어야하는 것은 해당 멤버 변수뿐입니다. 함수에서 전체 객체를 잠그면 해당 함수를 호출하는 다른 것들이 잠금을 기다리는 동안 차단됩니다. 이러한 함수가 멤버 목록에 액세스 할 필요가없는 경우 다른 코드로 인해 응용 프로그램이 아무런 이유없이 대기하고 속도가 느려질 수 있습니다.


답변

MSDN Topic Thread Synchronization (C # 프로그래밍 가이드)을 살펴보십시오.

일반적으로 공개 유형 또는 애플리케이션이 제어 할 수없는 오브젝트 인스턴스에서는 잠금을 피하는 것이 가장 좋습니다. 예를 들어, 공개적으로 인스턴스에 액세스 할 수있는 경우 lock (this)은 문제가 될 수 있습니다. 제어 할 수없는 코드도 객체에서 잠길 수 있기 때문입니다. 이로 인해 둘 이상의 스레드가 동일한 객체의 릴리스를 기다리는 교착 상태 상황이 발생할 수 있습니다.. 개체와 달리 공개 데이터 형식을 잠그면 같은 이유로 문제가 발생할 수 있습니다. 리터럴 문자열은 CLR (공용 언어 런타임)에 의해 삽입되므로 리터럴 문자열에 대한 잠금이 특히 위험합니다. 이것은 전체 프로그램에 대해 주어진 문자열 리터럴의 인스턴스가 하나 있음을 의미하며, 정확히 동일한 객체는 모든 스레드에서 실행중인 모든 응용 프로그램 도메인의 리터럴을 나타냅니다. 결과적으로 응용 프로그램 프로세스의 어느 위치에서나 동일한 내용을 가진 문자열에 대한 잠금은 응용 프로그램에서 해당 문자열의 모든 인스턴스를 잠급니다. 결과적으로 인턴되지 않은 개인 또는 보호 된 구성원을 잠그는 것이 가장 좋습니다. 일부 클래스는 잠금을 위해 특별히 멤버를 제공합니다. 예를 들어 배열 유형은 SyncRoot를 제공합니다. 많은 컬렉션 유형은 SyncRoot 멤버도 제공합니다.


답변

나는이 오래된 스레드 알지만, 사람들은 여전히이 위로보고에 의존 할 수 있기 때문에, 그 지적하는 것이 중요합니다 것 lock(typeof(SomeObject))보다 훨씬 더 나쁘다 lock(this). 라고 한; 그것이 lock(typeof(SomeObject))나쁜 습관 임을 지적한 Alan에게 진심으로 찬사를 보냅니다 .

의 인스턴스는 System.Type가장 일반적이고 거친 입자 중 하나입니다. 최소한 System.Type의 인스턴스는 AppDomain에 대해 전역 적이며 .NET은 AppDomain에서 여러 프로그램을 실행할 수 있습니다. 이것은 두 개의 완전히 다른 프로그램이 동일한 유형 인스턴스에서 동기화 잠금을 얻으려고 할 경우 교착 상태가 발생할 때까지 서로 간섭을 일으킬 수 있음을 의미합니다.

따라서 lock(this)특히 견고한 형태는 아니며 문제를 일으킬 수 있으며 인용 된 모든 이유로 항상 눈썹을 올리십시오. 그러나 개인적으로 패턴 변경을보고 싶어하지만 lock (this) 패턴을 광범위하게 사용하는 log4net과 같이 널리 사용되며 상대적으로 존경 받고 안정적으로 안정적인 코드가 있습니다.

그러나 lock(typeof(SomeObject))완전히 새롭고 강화 된 웜 캔을 엽니 다.

그만한 가치가 있습니다.


답변

…이 구문에도 똑같은 주장이 적용됩니다.

lock(typeof(SomeObject))


답변

사무실에 부서의 공유 리소스 인 숙련 된 비서가 있다고 가정합니다. 가끔, 당신은 당신의 동료 중 한 명이 이미 그것을 요구하지 않기를 바라는 임무 만 있기 때문에 그들에게 달려갑니다. 일반적으로 짧은 시간 동안 만 기다려야합니다.

돌보는 것은 공유하기 때문에 관리자는 고객이 비서를 직접 사용할 수도 있다고 결정합니다. 그러나 이것은 부작용이 있습니다. 고객이이 고객을 위해 일하는 동안 고객에게 소유권을 주장 할 수도 있고 작업의 일부를 실행해야 할 수도 있습니다. 소유권 주장이 더 이상 계층 구조가 아니기 때문에 교착 상태가 발생합니다. 고객이 처음부터 고객에게 소유권을 주장하지 못하게함으로써 이러한 문제를 모두 방지 할 수있었습니다.

lock(this)우리가 본 것처럼 나쁘다. 외부 객체가 객체를 잠글 수 있으며 클래스를 사용하는 사람을 제어하지 않기 때문에 누구나 객체를 잠글 수 있습니다. 이는 위에서 설명한 정확한 예입니다. 다시, 해결책은 물체의 노출을 제한하는 것입니다. 그러나, 당신이있는 경우 private, protected또는 internal클래스는 당신은 이미 개체에 고정되어있는 사용자를 제어 할 수 있습니다 당신은 당신이 스스로 코드를 작성한 것이니까. 따라서 메시지는 다음과 같이 노출하지 마십시오 public. 또한 유사한 시나리오에서 잠금을 사용하면 교착 상태를 피할 수 있습니다.

이것과 완전히 반대되는 것은 최악의 시나리오 인 앱 도메인 전체에서 공유되는 리소스를 잠그는 것입니다. 그것은 당신의 비서를 외부에 두어 거기에있는 모든 사람이 그들에게 요구할 수있게하는 것과 같습니다. 결과는 완전히 혼돈입니다-또는 소스 코드 측면에서 : 그것은 나쁜 생각이었습니다. 버리고 다시 시작하십시오. 어떻게 그렇게합니까?

대부분의 사람들이 지적한 것처럼 유형은 앱 도메인에서 공유됩니다. 그러나 우리가 사용할 수있는 더 좋은 것들이 있습니다 : 문자열. 그 이유는 문자열 이 풀링되기 때문입니다 . 즉, 앱 도메인에서 동일한 내용을 가진 두 개의 문자열이있는 경우 정확히 동일한 포인터를 가질 가능성이 있습니다. 포인터가 잠금 키로 사용되므로 기본적으로 “정의되지 않은 동작 준비”와 동의어입니다.

마찬가지로 WCF 객체, HttpContext.Current, Thread.Current, Singleton (일반) 등을 잠그면 안됩니다.이 모든 것을 피하는 가장 쉬운 방법은 무엇입니까? 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