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


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
내가 말한 것처럼 : (1) Nancy는 thread1에 잠금이 있습니다 (this). (2) SAME Nancy는 여전히 thread1에 잠겨있는 동안 thread2 에이징되어 있습니다. 잠긴 오브젝트가 읽기 전용이 아님을 증명하십시오. ALSO (2A) 동안 스레드 2에서,이 낸 객체는 이름에 고정된다. (3) 같은 이름 으로 다른 객체 만듭니다 . (4) 스레드 3에 전달하고 이름으로 잠금을 시도하십시오. (큰 마무리) 그러나 "문자열은 변하지 않는다"는 의미는 "Nancy Drew"라는 문자열을 참조하는 모든 객체가 문자 그대로 동일한 문자열 인스턴스를 메모리에서보고 있음을 의미합니다. 따라서 object1이 같은 값으로 잠겨있을 때 object2는 문자열을 잠글 수 없습니다.
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-그 대화는 CLR 객체의 SyncBlock 비트에 관한 것이라고 생각합니다. 공식적으로 이것은 수정 된 객체 자체입니다
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
이 답변의 마지막 단락이 올바르지 않습니다. 잠금은 어떤 식 으로든 객체를 액세스 할 수 없거나 읽기 전용으로 만들지 않습니다. Lock (this)은 다른 스레드가 이것에 의해 참조되는 객체를 호출하거나 수정하는 것을 막지 않습니다.
Esteban Brenes

3
호출 된 다른 메소드도 잠금 (this)을 수행하면됩니다. 그게 요점이라고 생각합니다 "함수에서 전체 객체를 잠그면"...
Herms

@Orion : 더 명확합니다. @Herms : 예. 그러나이 기능을 달성하기 위해 'this'를 사용할 필요는 없습니다. 목록의 SyncRoot 속성은 예를 들어, 해당 키에서 동기화를 수행해야한다는 것을 명료하게하면서 그러한 목적을 수행합니다.
Esteban Brenes

Re : "너무 많은"잠금 : 잠글 항목을 결정하는 훌륭한 균형 조정 작업입니다. 잠금을 수행하려면 캐시 플러시 CPU 작업이 필요하며 다소 비쌉니다. 다시 말해, 개별 정수를 잠 그거나 업데이트하지 마십시오. :)
Zan Lynx

마지막 단락은 여전히 ​​이해가되지 않습니다. 목록에 대한 액세스 만 제한해야하는 경우 다른 기능이 목록에 액세스하지 않으면 잠금이 발생하는 이유는 무엇입니까?
Joakim MH

44

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

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


34

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

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

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

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

그만한 가치가 있습니다.


26

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

lock(typeof(SomeObject))

17
lock (typeof (SomeObject))은 실제로 lock (this) ( stackoverflow.com/a/10510647/618649 ) 보다 훨씬 나쁩니다 .
Craig

1
글쎄, lock (Application.Current)은 그때보다 더 나쁘지만, 어쨌든 누가 이런 어리석은 것들을 시도 할 것입니까? lock (this)은 논리적이고 간결한 것처럼 보이지만 다른 예제는 그렇지 않습니다.
Zar Shardan

나는 그것이 lock(this)논리적이고 간결하게 보인다 는 것에 동의하지 않습니다 . 엄청나게 거친 잠금이며 다른 코드는 객체를 잠 가서 내부 코드에 간섭을 일으킬 수 있습니다. 더 세밀한 잠금을 취하고 더 세밀한 제어를 가정하십시오. 무엇을 lock(this)그것은 훨씬 더 이상의 점이다가는가 않습니다 lock(typeof(SomeObject)).
Craig

8

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

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

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

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

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

마찬가지로 WCF 객체, HttpContext.Current, Thread.Current, Singleton (일반) 등을 잠그면 안됩니다.이 모든 것을 피하는 가장 쉬운 방법은 무엇입니까? private [static] object myLock = new object();


3
실제로 개인 수업이 있어도 문제가 예방되지는 않습니다. 외부 코드는 개인 클래스의 인스턴스에 대한 참조를 얻을 수 있습니다 ...
Rashack

1
@Rashack은 기술적으로 정확하지만 (1을 지적하기 위해 +1), 내 요점은 누가 인스턴스를 잠그는 지 제어해야한다는 것입니다. 그런 인스턴스를 반환하면 문제가 발생합니다.
atlaste

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 을 사용할 때 무엇이 ​​잘못되었는지 파악하기가 어렵 습니다. 반복 된 잔액을 보는 것
같습니다

3

Microsoft® .NET 런타임의 성능 설계자 인 Rico Mariani의 http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects 관련 기사가 있습니다.

발췌 :

여기서 기본적인 문제는 type 객체를 소유하지 않으며 누가 다른 객체에 액세스 할 수 있는지 모른다는 것입니다. 일반적으로 생성하지 않은 객체를 잠그고 다른 사람이 액세스 할 수있는 사람을 모르는 것은 매우 나쁜 생각입니다. 교착 상태가 발생합니다. 가장 안전한 방법은 개인 개체 만 잠그는 것입니다.



2

lock (this)이 나쁜 이유와 클래스 소비자가 객체를 잠그려고 할 때 교착 상태가 발생할 수 있는 훨씬 간단한 그림이 있습니다 ( 여기 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


내가 아는 한 lock (this)을 개인 인스턴스 멤버의 잠금으로 SomeClass바꾸면 여전히 동일한 교착 상태가 발생합니다. 또한 기본 클래스의 잠금이 Program의 다른 개인 인스턴스 멤버에서 수행되면 동일한 잠금이 발생합니다. 따라서이 답변이 오도되고 부정확하지 않은지 확실하지 않습니다. 여기에서 그 동작을보십시오 : dotnetfiddle.net/DMrU5h
Bartosz

1

클래스의 인스턴스를 볼 수있는 코드 덩어리도 해당 참조를 잠글 수 있기 때문입니다. 참조해야하는 코드 만 참조 할 수 있도록 잠금 객체를 숨기고 (캡슐화) 싶습니다. 키워드 this는 현재 클래스 인스턴스를 참조하므로 많은 수의 항목이 참조 할 수 있으며 스레드 동기화를 수행하는 데 사용할 수 있습니다.

분명히 말하면, 다른 코드 덩어리가 클래스 인스턴스를 사용하여 잠글 수 있고 코드가 적시에 잠금을 얻지 못하게하거나 다른 스레드 동기화 문제를 일으킬 수 있기 때문에 이것은 나쁩니다. 가장 좋은 경우 : 클래스에 대한 참조를 사용하여 잠그는 것은 없습니다. 중간 사례 : 클래스에 대한 참조를 사용하여 잠금을 수행하면 성능 문제가 발생합니다. 최악의 경우 : 클래스의 참조를 사용하여 잠금을 수행하면 실제로 나쁜, 미묘하고 디버그하기 어려운 문제가 발생합니다.


1

미안하지만, 잠그면 교착 상태가 발생할 수 있다는 주장에 동의하지 않습니다. 교착 상태와 굶주림의 두 가지를 혼동하고 있습니다.

  • 스레드 중 하나를 방해하지 않으면 교착 상태를 취소 할 수 없으므로 교착 상태에 들어간 후에는 벗어날 수 없습니다
  • 스레드 중 하나가 작업을 완료하면 굶주림이 자동으로 종료됩니다.

여기에서 의 차이를 나타내는 사진이다.

결론 스레드 기아가 문제가되지 않는 경우
에도 안전하게 사용할 수 있습니다 lock(this). 굶주린 실을 사용하는 실이 lock(this)물체를 잠근 상태에서 자물쇠로 끝날 때, 결국에는 영원한 기아로 끝날 것이라는 점을 명심 해야합니다.)


9
차이점이 있지만이 토론과는 전혀 관련이 없습니다. 그리고 당신의 결론의 첫 문장은 잘못되었습니다.
벤 Voigt

1
분명히하기 위해 : 나는 방어하지 않고 있습니다 lock(this)-이런 종류의 코드는 단순히 잘못되었습니다. 교착 상태라고 부르는 것이 약간 모욕적이라고 생각합니다.
SOReader

2
더 이상 이미지 링크를 사용할 수 없습니다. :( 당신은 그것을 다시 참조 할 수 있습니까? Thx
VG1


1

다음은 더 간단한 (IMO) 샘플 코드입니다. ( LinqPad 에서 작동하며 다음 네임 스페이스를 참조하십시오 : System.Net 및 System.Threading.Tasks)

기억해야 할 것은 lock (x)은 기본적으로 구문 설탕이며 그것이하는 일은 Monitor.Enter를 사용한 다음 try, catch, finally 블록을 사용하여 Monitor.Exit를 호출한다는 것입니다. 참조 : https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter (비고 섹션)

또는 try… finally 블록에 Enter 및 Exit 메서드를 래핑하는 C # 잠금 문 (Visual Basic의 SyncLock 문)을 사용하십시오.

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스레드가 문제를 설명하는 데 필요하지 않은 것 같습니다 .
Jack Lu

메인의 외부 잠금을 의미하지 않습니까? 한 스레드는 다른 스레드가 완료 될 때까지 단순히 기다릴 것입니까? 그러면 병렬 처리가 무효화 될 것입니다. 더 나은 실제 사례가 필요하다고 생각합니다.
Seabizkit

@Seabizkit은 코드를 좀 더 명확하게 업데이트했습니다. Parallel은 새 스레드를 만들고 코드를 비동기 적으로 실행하기위한 것입니다. 실제로, 두 번째 스레드는 여러 가지 방법 (버튼 클릭, 별도 요청 등)으로 호출 할 수있었습니다.
Raj Rao

0

클래스에 'this'또는 클래스의 코드가 인스턴스화하는 객체를 잠그는 코드를 클래스에 가질 수 있다는 규칙을 설정할 수 있습니다. 따라서 패턴을 따르지 않는 경우에만 문제가됩니다.

이 패턴을 따르지 않는 코드로부터 자신을 보호하려면 허용 된 대답이 맞습니다. 그러나 패턴을 따르면 문제가되지 않습니다.

잠금 (이)의 장점은 효율성입니다. 단일 값을 보유한 간단한 "값 개체"가있는 경우 어떻게해야합니까? 랩퍼 일 뿐이며 수백만 번 인스턴스화됩니다. 잠금을 위해 전용 동기화 개체를 만들어야하므로 기본적으로 개체 크기가 두 배가되고 할당 수는 두 배가되었습니다. 성능이 중요 할 때 이점이 있습니다.

할당 횟수 나 메모리 풋 프린트에 신경 쓰지 않으면 다른 답변에 표시된 이유로 잠금을 피하는 것이 좋습니다.


-1

동일한 객체 인스턴스를 사용하는 다른 요청이있을 수 있으므로 인스턴스에 공개적으로 액세스 할 수 있으면 문제가 발생합니다. 개인 / 정적 변수를 사용하는 것이 좋습니다.


5
그것이 사람에게 무엇을 더하는지 확실하지 않고, 이미 존재하는 자세한 답변은 똑같은 것을 말합니다.
Andrew Barber
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.