본문 바로가기

이것 저것 정리

Lock 심화(1) - RW Lock

Lock 심화(1)

ReaderWriterLock 에 대해 정리

https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlock?view=net-7.0 

 

ReaderWriterLock Class (System.Threading)

Defines a lock that supports single writers and multiple readers.

learn.microsoft.com

 

Microsoft 에서 정의한 ReaderWriterLock 내용은 다음과 같다.

Defines a lock that supports single writers and multiple readers.

Single writers, Multiple readers 를 지원한다.

 

말 그대로 어떤(Single-1개) 스레드가 write 를 하고 있다면 다른 write/read thread 은 대기 상태로 block 된다.

(여기 까지는 다른 lock 들과 비슷하다.)

다만 write 중이지 않는 공유자원을 read 만 하는 것은 몇 번(Multiple)을 read 하든 문제가 없다.

바뀌지도 않은(write 중이지 않은) 공유자원은 몇 번이나 read 하든 상관 없다는 뜻.

그래서 생겨난 개념이 RW Lock.

 

그런데 대체 이건 어떻게 구현하고 동작하는 걸까?

다음은 Microsoft 에서 제공한 예제이다.

// Request and release a reader lock, and handle time-outs.
   static void ReadFromResource(int timeOut)
   {
      try {
         rwl.AcquireReaderLock(timeOut);
         try {
            // It is safe for this thread to read from the shared resource.
            Display("reads resource value " + resource);
            Interlocked.Increment(ref reads);
         }
         finally {
            // Ensure that the lock is released.
            rwl.ReleaseReaderLock();
         }
      }
      catch (ApplicationException) {
         // The reader lock request timed out.
         Interlocked.Increment(ref readerTimeouts);
      }
   }

   // Request and release the writer lock, and handle time-outs.
   static void WriteToResource(Random rnd, int timeOut)
   {
      try {
         rwl.AcquireWriterLock(timeOut);
         try {
            // It's safe for this thread to access from the shared resource.
            resource = rnd.Next(500);
            Display("writes resource value " + resource);
            Interlocked.Increment(ref writes);
         }
         finally {
            // Ensure that the lock is released.
            rwl.ReleaseWriterLock();
         }
      }
      catch (ApplicationException) {
         // The writer lock request timed out.
         Interlocked.Increment(ref writerTimeouts);
      }
   }

 

ReadFromResource

WriteToResource

순서대로 위의 내용들을 차근 차근 살펴보자.

 

  • ReadFromResource
  • AcquireReaderLock

AcquireReaderLock 은 다음의 Microsoft 페이지를 참고하였다.

https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlock.acquirereaderlock?view=net-7.0 

 

ReaderWriterLock.AcquireReaderLock Method (System.Threading)

Acquires a reader lock.

learn.microsoft.com

The following code example shows how to acquire and release a reader lock, and how to handle the exception thrown when a request times out.
...
AcquireReaderLock blocks if a different thread has the writer lock, or if at least one thread is waiting for the writer lock.

찬찬히 읽어보면 결국

이미 해당 공유 자원에 대해 Write 중인 스레드가 있다면 block, 대기 상태로 있는다.

또한 "at least one thread is waiting for the writer lock." 해석: writer lock 이 대기 중일 경우 block 된다.

이라는 부분도 명심해야 한다.

(그 이유에 대해서는 아래에서 다시 언급하겠다.)

 

해당 스레드가 공유 자원에 접근할 수 있는 상태가 되면 reader lock count 를 하나 올린다.

그 후, read 행위가 끝나면 ReleaseReaderLock 을 호출하여 reader lock count 를 하나 내린다.

이 reader lock count 가 0 이상이면 reader 가 얼만큼 접근 중인지 알 수 있는 것이다.

 

  • ReleaseReaderLock

https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlock.releasereaderlock?view=net-7.0 

 

ReaderWriterLock.ReleaseReaderLock Method (System.Threading)

Decrements the lock count.

learn.microsoft.com

 

 

여기까지는 개념이 매우 간단하다.

그러면 Write Lock 은 동작을 어떻게 할까?

 

  • WriteToResource
  • AcquireWriterLock

https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlock.acquirewriterlock?view=net-7.0 

 

ReaderWriterLock.AcquireWriterLock Method (System.Threading)

Acquires the writer lock.

learn.microsoft.com

This method blocks if another thread has a reader lock or writer lock.

Write Lock 은 read/write lock 을 전부 블락해버린다.

위에서 언급했었던, "at least one thread is waiting for the writer lock." 을 명심해야 하는 이유가 여기서 나온다.

대충 RW Lock 이 '음~ read 는 read 끼리 block 하지 않지~' 만 기억했다가 큰일이 생길수 있는 부분이 이 부분이다.

 

Write 는 Read 를 블락한다.

당연한 이야기인데 계속 반복하는 이유는 다음의 예시를 보자.

 

예를 들어 read 를 10번 시도하는 과정 중, write 를 시도해보자.

r1, r2, r3... r10 read 하는 과정 에서, write 가 끼어들게 된다면 어떻게 될까.

만약 r5 까지 진행한 상태에서 w1 이 끼어들게 된다면, r6 ~ r10 은 block 상태에서 w1 이 release 되기를 기다리게 된다.

 

원래 lock 개념은 줄을 서는 것으로 예를 들어보자면 (우선 순위에 따라 다르겠지만)

lock1, lock2... lock10 까지 순서대로 들어온 상태에서 새로운 lock11 이 시도를 하게되면,

lock11 은 lock10 뒤에 줄을 섰어야 하는 것이 정상이었을 것 이다.

 

그런데 read write lock 같은 경우 살짝 달라진다.

read(1~10) 중간에 write 를 요구하는 스레드가 생긴다면 write 에게 wait 요구를 하고,

read lock 은 write 가 wait 중인지 체크를 하여, write lock 에게 우선권을 넘겨준다.

이 개념 때문에 다음과 같은 문제가 발생할 수도 있다.

 

  • RW 주의사항

Read 는 여러 번 중첩 해서 불릴 수 있으니 다음과 같이 구현을 했다고 해보자.

(대충 수도코드 느낌으로 이해만 대략적으로 될 것이라 믿는다... )

1번 스레드

A.lock();

B.AcquireReaderLock();

B.RealeaseReaderLock();

A.unlock();



2번 스레드

B.AcquireReaderLock();

A.lock();

A.unlock();

B.RealeaseReaderLock();

 

1번 스레드와 2번 스레드가 동시에 불리게 되면 과연 데드락(deadlock) 이 발생할까?

 

1. 1번 스레드 A lock 진입 (성공)

2. 2번 스레드 B Read Lock 진입 (성공)

3. 1번 스레드 B Read Lock 진입 시도.....?

 

결론부터 말하자면 1번 스레드와 2번 스레드만 보면 데드락은 발생하지 않는다.

3번 과정에서 B는 Read Lock 만 잡혀 있는 상태이니 몇 번이든 Read 를 하는데는 문제가 없다.

그렇게 1번 스레드는 B Read Lock 진입 시도에 성공하게 되고 데드락이 걸리지 않고 로직은 잘 돌아간다.

 

그런데 만약 다음과 같은 상황이 있다고 해보자.

1번 스레드

A.lock();

B.AcquireReaderLock();

B.RealeaseReaderLock();

A.unlock();



2번 스레드

B.AcquireReaderLock();

A.lock();

A.unlock();

B.RealeaseReaderLock();


3번 스레드

B.AcquireWriterLock();
B.ReleaseWriterLock();

 

바로 직전의 상황에서 3번 스레드 로직이 추가 되었다.

겨우 3번 스레드에, 그것도 B Write Lock이 달랑 한 개가 추가되었는데

과연 데드락이 발생할까?

다음의 상황을 가정해보자.

 

1. 1번 스레드 A lock 진입

2. 2번 스레드 B Read Lock 진입

3. 3번 스레드 B Write Lock 진입 - 2번 스레드의 B Read Lock 이 아직 Release 전이니 대기(Wait) 한다.

 

엔드 게임이다... 데드락이 걸려버렸다.

1번 스레드는 B 의 Read Lock 진입을 시도하나, 3번 스레드에서 Write Lock 대기 중이니 대기해야한다.

2번 스레드는 A lock 이 1번 스레드에서 lock 잡고 있으니 대기 해야한다.

3번 스레드는 2번 스레드에서 B Read lock 을 잡고 있으니 대기 해야한다.

 

Write 하는 상황이 적고, Read 만 하는 경우가 많을 때 RW 는 좋은 Lock 이 될 수 있으나

위의 상황을 생각 못하고 Read Lock 을 마음 놓고 썼다가는 큰코 다칠 수 있다.

 

그러니 RW Lock 을 쓸 때는 꼭 주의하자. (to me)