쓰레드 간 공유되는 데이터가 있을 때, 항상 모든 쓰레드가 그 데이터를 읽고 쓰는 것은 아니다. 어떤 쓰레드는 읽기만, 어떤 쓰레드는 쓰기만 한다고 했을 때, 읽기만 하는 쓰레드가 상대적으로 많은 횟수로 실행된다고 한다면 딱히 데이터가 변경되지 않아도 일반적인 락을 구현해버린다면 성능상 손해라고 할 수 있다.
ReaderWriterLock은 데이터 쓰기를 위해 접근할 때는 락을 설정하고, 읽기만 할 때는 락을 설정하지 않도록 해주어서 성능상 이득을 챙길 수 있도록 한다.
rivate ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
private void WriterThreadBody()
{
try
{
rwLock.EnterWriteLock();
// Do something
}
finally
{
rwLock.ExitWriteLock();
}
}
private void ReaderThreadBody()
{
try
{
rwLock.EnterReadLock();
// Do something
}
finally
{
rwLock.ExitReadLock();
}
}
// 구현방식 : 재귀적 락 허용 x (같은 쓰레드에서 acquire를 할 때 또 허용을 해줄 것인지)
// 만약 재귀적 락을 허용한다면 WriteLock하는 중에 WriteLock 허용, WriteLock하는 중에 ReadLock 허용
// 스핀락 정책 (5000번 한 후 yield)
// WriteLock -> ReadLock -> ReadUnlock -> WriteUnlock
class Lock
{
// [Unused(1)] [WriteThreadId(15)] [ReadCount(16)]
// Unused(1비트) : 음수가 될 가능성
// WriteThreadId(15비트) : 하나의 쓰레드만 write lock을 획득할 수 있는데 그 쓰레드의 아이디
// ReadCount(16비트) : 여러 쓰레드들이 read lock을 획득할 수 있는데, 그때 lock을 가진 쓰레드의 수
const int EMPTY_FLAG = 0x00000000;
const int WRITE_MASK = 0x7FFF0000;
const int READ_MASK = 0x0000FFFF;
const int MAX_SPIN_COUNT = 5000;
int _flag = EMPTY_FLAG;
int _writeCount = 0; // 하나의 쓰레드가 lock을 잡으면 그 쓰레드만 이 변수에 접근할 수 있기 때문에 멀티쓰레딩에서 문제가 없다
public void WriteLock()
{
// 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
{
_writeCount++; // 재귀적 락.
return;
}
// 아무도 WriteLock 또는 ReadLock을 획득하고 있지 않을 때, 경함해서 소유권을 얻는다.
int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
while (true)
{
for (int i = 0; i < MAX_SPIN_COUNT; i++)
{
if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
{
_writeCount = 1;
return;
}
// 두 단계로 분리되는건 안됨. 만약 Id가 다른 두 쓰레드가 동시다발적으로 if에서 참이 되어 _flag 갱신이 일어나면 첫번째 쓰레드는 무시
//if(_flag == EMPTY_FLAG)
// _flag = desired;
}
Thread.Yield();
}
}
public void WriteUnlock()
{
int lockCount = --_writeCount;
if (lockCount == 0)
Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}
public void ReadLock()
{
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
{
Interlocked.Increment(ref _flag);
return;
}
// 아무도 write lock 획득하고 있지 않으면 ReadCount++
while (true)
{
for (int i = 0; i < MAX_SPIN_COUNT; i++)
{
// 만약 쓰레드 여러개가 동시에 들어오면 먼저 들어온 쓰레드는 1을 늘린 값을 _flag에 갱신시켜 주고 리턴을 할 것이고, 그와 동시에(i가 같은 상태) 그 다음 쓰레드는
// if문 하기 전에 expected로 받아온 값과 expected + 1한 값이 다를 것이므로, false가 되어 그 루프를 돌 것이다.
int expected = (_flag & READ_MASK); // 만약 WRITE lock부분이 0이 아니였으면,
if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
return;
}
/* 직접 WRITE_MASK와 &연산해서 0이면 현재 아무도 락을 획득 안한 상태
if ((_flag & WRITE_MASK) == 0)
{
_flag = _flag + 1;
return;
}
*/
Thread.Yield();
}
}
public void ReadUnlock()
{
Interlocked.Decrement(ref _flag);
}
}
class Program
{
static volatile int count = 0;
static Lock _lock = new Lock();
static void Main(string[] args)
{
Task t1 = new Task(delegate ()
{
for(int i=0; i<100000; i++)
{
_lock.WriteLock();
_lock.WriteLock();
count++;
_lock.WriteUnlock();
_lock.WriteUnlock();
}
});
Task t2 = new Task(delegate ()
{
for (int i = 0; i < 100000; i++)
{
_lock.WriteLock();
count--;
_lock.WriteUnlock();
}
});
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(count);
}
}