방프리

24.04.20 Chapter4. 병렬 처리 (Item 40) 본문

C#/More Effective C#

24.04.20 Chapter4. 병렬 처리 (Item 40)

방프리 2024. 4. 20. 13:37

Item 40 : 동기화에는 lock()을 최우선으로 사용하라

동기화 요소는 특정 스레드가 임계영역 내에서 연산을 수행하는 동안 다른 스레드로부터 이를 보호하는 역할을 수행한다.
.NET 기본 클래스 라이브러리에서는 다양한 기능을 통해 공유 데이터에 대한 접근을 안전하게 동기화하는데 특히 Monitor.Enter()와 Monitor.Exit()는 특별하게 다루어진다. 하지만 lock()이 가장 기본이 되는 기능이므로 최우선적으로 고려 시에는 lock()을 먼저 사용하는 것이 좋다.

lock()은 값 타입을 지원하지 않는다. 그러기에 Monitor.Enter()을 사용하게 되는데 이 때 주의할 점이 있다.
하단의 코드는 정상적으로 컴파일은 되지만 문제가 발생할 수 있다.

public void IncrementTotal()
{
    Monitor.Enter(total);
    try
    {
        total++;
    }
    finally
    {
        Monitor.Exit(total);
    }
}

Monitor.Enter와 Monitor.Exit에 전달되는 객체는 total 변수를 박싱한 객체가 전달되므로 객체가 동일하지 않아 잠금이 제대로 되지 않을 뿐더러 Enter와 Exit에 전달되는 객체 또한 서로 다르다.

이를 해결한 제네릭 클래스를 살펴보자

public sealed class LockHolder<T> : IDisposable
    where T : class
{
    private T Handle;
    private bool holdsLock;
    
    public LockHolder(T handle, int milliSecondTimeout)
    {
        this.handle = handle;
        holdsLock = System.Threading.Monitor.TryEnter(
            handle, miliiSecondTimeout);
    }
    
    public bool LockSuccessful
    {
        get { return holdsLock; }
    }
    
    public void Dispose()
    {
        if (holdsLock)
            System.Threading.Monitor.Exit(handle);
        holdsLock = false;
    }
}

만약 수치타입에 대한 동기화를 진행하게 된다면 lock()보다는 Interlocked을 참고해보도록 하자!
Monitor 클래스에도 Consumer/Producer 패턴을 설계할 때 유용한 기능들도 제공하고 있으니 참고하는 것이 좋다.

Comments