ReaderWriterLock 구현연습 + BitFlag

CJB_ny·2022년 2월 16일
0

Unity_Server

목록 보기
17/55
post-thumbnail

지난 시간에 예고한대로 구현을 함 해보도록 하겠다.

오늘 해볼 방식이

Lock Free Programming이랑 사실 굉장히 비슷한데

오늘 하는 내용이 어렵기 때문에 한번듣고 모르겠다 싶으면

구경만 하고 넘어가도 상관은 없다.

Servercore -> 새 항복 추가를 해서

Lock이라는 파일을 하나 따로만들자 ( 코드가 길어 질 수 있기 때문에 )

이렇게 추가를 하자.

잘됨!

일단 여기서 작업을 할 것인데

flag를 하나 사용을 할 것이다.

_flag는 int 형이라 32bit인데 _flag를

이런식으로 사용을 할 것이다.

여기서 말하는 ReadCount는 ReadLock을 획득했을 때,

여러 쓰레드들이 동시에 Read를 잡을 수 있다고 했었는데,

그 Counting을 하는 것이고

( 왜냐하면 WriteLock을 누군가 잡으면 ReadLock들은 얼씬도 못하는데

WriteLock은 굉장히 자본주의 적인 Lock이라서 -> 이전시간 복습 함 해봐라

ReadLock은 평소에 아무일 없는 것처럼 쿨하게 지나가는 그런 애라고 했다 )

WriteThreadId(WrtieLock) 의 경우 한번에 한 쓰레드만 획득이 가능하다고 했었다.

그래서 획득한 아이가 누구인지를 WriteThreadId에다가 기록을 하게 될 것이다.

그리고 이런 락들을 만들기 전에

몇가지 정책을 정해야 되는데

1. 락 정책 정하기

1-1. 재귀적 락 허용

먼저,

재귀적 락을 허용할 것인지 허용을 안 할 것인지를 정해야 한다 -> (No)

이게 무슨말인가 하면은

WriteLock을 Acquire을 한 상태에서

또다시 재귀적으로 다시한번, 같은 쓰레드에서 또 Acquire를 할 때 그거를 또 다시 허용할 것인지 아닌지에 대한 문제인데

일단은 조금 쉽게 하기 위해서 허용을 하지 않은 상태로 놔두고

1-2. SpinLock

SpinLock 같은 경우에도

5000번 정도 시도를 한다음에 계속 대기를 해야하는 상황이라면 Yield를 하도록 하겠다

SpinLock 5000 -> Yield


2. Bit Flag

그리고 이제 다시 코드로 와서 _flag를 자주 사용할 것이니까

이런식으로 아무 값도 안들어 있는 EMPTY_FLAG를 16진수로 만들어 주고

WRITE_MASK라는 녀석은

이녀석은 사용 안하고

여기 있는 15개만 사용을 할 것이니까

0x7FFF0000 이 될 것이다.

READ_MASK는 16비트만 사용할 예정이기 때문에

0x0000FFFF가 될 것이다.

이렇게 설정을 해주자.

지금 그리고 16진수 이해가 안가는 부분 잠깐 설명을 하자면은

그래서 이렇게 계산기를 꺼내와가지고

확인을 해보도록 하자.

우리가 일단은 _flag라는 int형을 하나 사용을 할것이다.(32bit)

_flag는 32bit로 구성이 되어있다.

그래서 이 32개를 사용을 할것인데

우리같은 경우에는

이 마지막 부분으(지금은 양수이지만) 음수가 될 가능성이 있기 때문에

마지막은 사용을 안할 것이고

이렇게 15랑 16을 각각 사용을 한다고 했었다.

그래서

여기 아래에있는 16개를 READ_COUNT로 사용을 할 것인데

만약 어떤 flag가 있는데

READ_COUNT만 쏙 빼오고 싶다면은

여기에서 위에 부분

이녀석들은 다 무시를 하고 ( 위에부분을 다 0으로 만들고 )

아래 부분

이부분만 추출을 하면 될것이다.

그런데 "추출"하기 위해서는

이런식으로 "1"로 켜두어야 된다는 얘기가 되는데

그래서

여기에서 0x0000FFFF가 오게 되는 것이다.

16진수로 표현하는 숫자 하나는

2진수 4개짜리였다.

그러니까

이렇게 4개를 다킨 상태가 F라는 상태인 것인데

( 16진수가 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f 인데

1 1 1 1 (2진수가) == 15인데 이게 16진수의 f에 해당한다.

그래서 1111 이렇게 다키면 f임

그래서 이것을 1111을 4번 다키니까

FFFF가 되는 것이고

반대로 WRITE_MAKS 는

어떤 _flag가 있을 때 WRITE_THREADID를 추출 하고 싶은 경우가 생길텐데

그것을 추출하기 위해서는 반대로

여기 아래에 있는 READ_COUNT는 싸그리 다 무시를 해야되니까

밑에 부분은 다 0이 될테고

나머지 부분은

이렇게 다 1로 켜진 상태 여야된다.

그래서 1111 == F 였으니까

FFF가 세개가 있는 것이고

마지막 경우 0111은 10진수로 표현하면 7인데 이것은 16진수로도 7이기 때문에

WRITE_MASK는 0x7FFF0000이 되는 것이다.

그래서 이것이 익숙하지 않아도 ㄱㅊ음

한번만 계산을 하고나서 그대로 사용을 할 것이기 떄문에

3. WriteLock 구현

그리고 lock이랑 unlock을 하는

Acquire, Release를 하나씩 만들어 줄 것이다.

우리가 구현을 할 것이

ReaderWriterLock이니까

Reader Lock 한쌍

Writer Lock 한쌍 이렇게 인터페이스를 맞춰서 만들어 주도록 하자.

이렇게 쌍을 맞춰 주도록 하자.

아 그리고 그전에 _flag는

처음값은 0이니까 이렇게 맞춰 주도록 하자.

먼저 WriteLock부터 구현을 해보자면은

이녀석은

"아무도 WriteLock을 or ReadLock을 획득하고 있지 않을 때, 경합해서 "소유권"을 얻는다. 가 될 것이다.

그러면 안에서는 일단 SpinLock구조로 만들것이기 때문에

뺑뺑이를 계속 돌다가

MAX_SPIN_COUNT만큼 돌다가 안된다면

Yield를 해주면된다.

그래서 결국에는

이 안에 있는 부분을 채워야되는데

아무도 WriteLock을 or ReadLock을 획득하고 있지 않을 때, 경합을 한다는 의미는

이것을 우리가 간단한 코드로 풀어 쓰면은

                _flag == EMPTY_FLAG

인 상황을 말하는 것이다.

그래서 EMPTY_FLAG라면 우리가 이제 원하는 값을 넣어 주면되는데

우리가 원하는 것은

여기다가 원하는 값을 넣어주기만 하면되는데,

여기있는 WriteThreadId를

자신의 ThreadID로 _flag 에다가 채워주고싶은것이다.

        Thread.CurrentThread.ManagedThreadId

라는 녀석이 있는데

이것을 그대로 사용 하도록 하자.

애가 1부터 쭉 늘어나는 그런 숫자인데

어떤 쓰레드인지 구분하기 편하니까

그대로 사용을 할 것이고,

다만 READ_COUNT를 WRITE_THREADID에다가 밀어 넣어야 될 것이다.

그래서 16비트만큼 밀어 주도록 하겠다.

그리고 혹시라도 & WRITE_MASK를 해주자, 초반 값( == unused 값이 있다면 밀어 버리기위해서)

        int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MAKS;
        

이렇게 해주도록 하자.


이해가 잘 되지 않았기 때문에

이렇게 혼자 값이 어떻게 변화하는지를 연습해보자


지금

    const int WRITE_MAKS = 0x7FFF0000;

이니까

         int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MAKS;

라는 뜻은

Thread.CurrentThread.ManagedThreadId << 16비트 만큼 옮기면

이렇게 되는데

여기서 논리합 연산자로

& WRITE_MAKS;뒤에 이렇게 해주게 되면은

WRITE_MAKS는 지금 현재 0x7FFF0000 이니까


이상태 이다.

그러면

int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MAKS;

의 말 뜻은 즉,

desired이라는 것안에

1 << 16 의 현재 값이고

WRITE_MAKS;는 지금


라는 값이다. 실제로 출력을 해보면


이런 상황임

그러면 비트연산자 비트 플래그 논리합을 하게되면

해당하는 1 << 16 의 위치만 남게되는것이다

예제로

이렇게 비트 플래그를 사용을 해보면

이상태와

이상태의 논리합을 구하면

이렇게 4가 나온다.

그래서 0으로 밀린다는 얘기가 같이 1로 켜져있는 부분이 아니면 0으로 된다는 말이다.

그래서 아무튼 우리가 원하는 값을

_flag에다가 넣어주면 우리가 원하는 값이 완성될 것이다.

그래서 이렇게하면 될지 안 될지가 굉장히 궁금한데

이렇게 하면 안된다.

이게 멀티쓰레드 부분에서 계속 나오는 부분인데

이렇게 먼저 비어있는지 확인을 하고

값을 이렇게 집어 넣는 행위는

두단계로 분리가 되어있기 때문에

멀티쓰레드 환경에서

이 WriteLock을 여러개가 실행한다고 가정을 하면은

이부분을 일단 비교를 해볼건데

_flag가 비어있다면 동시 다발적으로 실행을 해가지고

if문을 동시에 통과를 해서 _desired값에 하나하나씩 값을 넣어주게 될 것이다.

이렇게 되면 count++했었던 것과 마찬가지로 굉장히 이상한 값이 들어가게될 것이다.

동시다발적으로 여러명이 자기네들이 성공했다고 착각을 하는 그런 상황이 발생할 것이다.

그래서 이부분을 나누어서 하면 안되고

InterLock 계열의 함수로 바꿔치기를 해주어야 한다.

그래서 이렇게 우선은 우리가 다룰 값인 _flag넣어주고

원하는 값인 _desired넣어주고

세번째 인자에 예상한값을 넣어주어야 하는데

comparand가 내가 예상한 값이다.

(내가 원하는 값은 무엇이냐하면은 )

지금 같은 경우가 EMPTY_FLAG 가 될 것이다.

그래서 _flag와 EMPTY_FLAG가 같다면 _desired를 _flag에 넣겠다라는 것이다.

그리고 CompareExcahnge가 뱉어 주는 값은 이전값을 뱉어 준다고 했으니까


이녀석이 성공했는지 안했는지 알고 싶으면은

이런식으로 값이 똑같은지 확인을 하면

실제로 _flag가 EMPTY_FLAG였구나 라는것을 알 수 있다.

그래서 성공을 하면 return 을 때려주면 된다.

그래서 결국

이 밑에 두줄이 한방에 처리가 된 셈이다.

그런데 만약 동시다발적으로

두쓰레드가 WriteLock에 들어왔다면 어떻게 해야할까??

두 쓰레드마다 ThreadId가 다를 테니까

결국 _desired값이 다른 상태 일것이다.

그렇다는 것의 거의 아무리 동시다발적으로 들어온다고해도 승자는 분명히 존재하기 마련일텐데

그 승자가 _flag값을 _desired값으로 넣어줄 테니까

두번째로 실행하는 녀석은 if문에서_flag와 EMPTY_FLAG와 같다는 부분에서 실패를 하기 때문에

동시다발적으로 두개다 들어 올 수 없다라는 게 된다.

그리고 WriteUnLock같은 경우는 조금 간단한데

이녀석같은 경우에는 경합 할 필요도 없이

애당초 WriteLock을 한 녀석만 unLock을 할 수 있었다.

그래서 이렇게 _flag값을 원래의 꺠긋한 값인 초기 값으로 만들어 주면 될 것이다.

이제

4. ReadLock 구현

ReadLock은

인데

즉,

WriteThreadId가 아무도 없다, 즉, 0이라고 하면은

여기 ReadCount를 1만 늘리면된다는 말이다.

그리고 그렇게 하는 이유는

ReadLock같은 경우에는

여러 Thread가 ReadLock을 동시에 잡을 수 있었기 떄문에

ReadCount는 쿨하게 계속 1씩 늘려도된다는 얘기이다.

이렇게 구현을 해주는데

(WriteLock과 굉장히 유사하다)

그리고 여기서도 의사코드를 간단하게 표현을 해보자면은

"아무도 WriteLock을 획득하고 있지 않다는 의미"는 결국

이말과 같은데

이게 왜 그런 것이냐면은

현재 만약 _flag가 0x00000000이라면

이상태이고

WriteMask가 0x7fff0000인데


이값이다

이거두개 논리합하면

0이 나옴 즉,

누군가가 WriteLock을 잡았다면 _flag에 desired값이 들어가 다른 값일텐데

아무도 안잡은 상태니까 0x00000000이니까

                if ( (_flag & WRITE_MAKS) == 0)

이럴 경우 아무도 WriteLock을 안잡은 상태가 되는 것이다.

그래서 if문 통과했다면 _flag를 1늘려주면된다.

그런데 아까와 마찬가지로

체크를 하고


1을 늘리면 문제가된다 ( why? => 멀티쓰레드환경이라서 )

만약 if문으로 체크를 하는 상황에서 누군가가

WriteLock을 잡아버리면

WriteLock이랑 ReadLock이 동시에 잡혀버리게 되어서

오류가 난다.

그래서 이부분도 한방에 처리를 할 수 있게 묶어야된다.

그래서 아까와 마찬가지로 InterLock 계열의 함수를 사용을 하면 된다는 것이다.

이렇게 _flag를 넣어주고 이제 expected값과 desired값을 넣어 주어야하는데

expected는 무엇이 되냐하면 ( 여기서 부터 굉장히 까다롭다 )

이게 Lock Free프로그래밍의 기초라고 할 수 있는데

                int expected = (_flag & READ_MAKS);

한 값이 예상하는 값이다.

즉, 현재 상황에서 보는 값은 expected 이녀석이라고 가정을 해보자.

그렇다면

이상황에서

이렇게 넣어주게 될 것인데

의미가 햇갈린다

여기서

_flag & WRITE_MAKS 하는 부분이 아예 없어졌는데

왜냐하면 애당초 우리가 "예상"하던 값이라는것은

_flag에다가 READ_MASK를 씌웠으니까

READ_MASK는 이부분만 추출을 하는 것이였는데

이부분 싸그리 다 날리고

이부분만 뽑아와서 사용을 하는

부분이 바로

이 expected라는 것인데

내가 예상한 값 ( == 원하는 값은 WRITE_MASK 가 없는값)이니까

WRITE_MASK에 해당하는 부분을

                int expected = (_flag & READ_MAKS);

이렇게 0으로 다 밀어 준것이다.

그래서 결국에는 내가 예상한 값이 expected고

거기에서 내가 원하는 값은 expected + 1 인 값이 될텐데

이게 성공을 했다는 의미는 무엇이냐하면은 결국

이 의미가 된다.

그래서 WRITE_MASK가 없다면은 expected + 1을 해주세요 == ( _flag = _flag + 1 ) 과 같다는 의미이다.

그래서 처음에는 이게 이해가 안 갈것인데

보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보다보니까 이해가 갔다고 한다.

그래서 내가 원하는 값을 이렇게 넣어 줄 것인데

얘가 한방에 통과 할 수도 있고 못통과 할 수도 있다.

그래서

만약에, WriteLock은 아무도 안하고 있는데

동시다발적의 거의 똑같으 여러개의 Thread가 ReadLock을 하고 있다고 가정을 해보자.

처음에는 여기 expected라는 값을 뽑아오기 전까지는 똑같겠지만

여기서

또 이렇게 경합을 하는 것이다.

그러면 맨먼저 들어온 애는


이렇게 1을 늘릴 수 있을 것이다.

그렇다고 하면은

다음에 들어온 쓰레드는 내가 예상한 값이 틀어지게 된다.

그래서 그 다음 들어온애는 값이 틀려지니까

다시 while문을 도는 그런 작업을 해야되게 될것이다.


다시 설명을 하자면

A, B라는 Thread가 거의 동시 다발적으로 ReadLock에 들어왔다라고 가정을 하면은

_flag가 텅 빈 상태라고 가정을 하고,

A 가 expected라는 값을 계산을 하면 0이 나오게 될것이고

B 가 expected라는 값을 계산을 하면 0이 나오게 될것이다.

근데 그 상태에서

                if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)

이녀석을 하려고 하는데

여기서 내가 예상한 _flag값이 0이라면은

0 + 1 인 1인 값을 동시다발적으로 실행을 할 것이다.

그래서

A (0 -> 1), B (0 -> 1)를 실행을 할 것인데

둘중 승자가 분명히 나올 것이다.

만약에 A가 먼저 실행이 되었다고 가정을 하면은

그렇게 되면 expected + 1한 값이 _flag에 들어가게되니까

_flag값은 1로 바뀌게 되니까 결국,

A는 성공을 했는데,

B가 요청을 한것은 만약에 _flag값이 0이라면 그녀석을(_flag를) 1로 바꿔주세요(expected + 1)이 였는데

애당초 A에 의해서 _flag값이 1로 바뀐 상태 이니까

B의 요청은 실패를 하게된다.

그렇게되면 B는

다음턴에 다시 경주를 해가지고(경합을 해가지고)

다시한번 실행을 하게 될 것이다.

이렇게 해서 계속 될 때까지 뺑뺑이를 도는 것이 Lock Free 프로그래밍의 기본이라고 할 수 있겠다.

자 이렇게해가지거

ReadLock을 하나 늘렸다고 가정을 하면은

UnLock하는부분은 이렇게 그냥 1을 줄여주면 될 것이다.


그리고 만약 이까지 이해를 했다면 하나 이상하게 생각할 수도 잇는게

왜 굳이 WriteMask 값을

이렇게 넣어주는지

그냥 0, 1 과같은 값을 넣어주어도 될거같은데 라는 생각이 든다.

그것은 사실

혹시라도 "재귀적 락"을 필요로 하게된다면

누가 가지고 있는지를 알아야 할 필요가 생기기때문에

이렇게 굳이 작업을 해본것이다.

5. 재귀적 락 허용할 경우(Yes)

이것은

WriteLock을 잡고있는 상태에서도 -> WriteLock (ok)
WriteLock을 잡고있는 상태에서도 -> ReadLock (ok)
ReadLock을 잡고있는 상태에서 -> WriteLock (No)

(애당초 ReadLock은 독점하는 상태가 아니니까 WriteLock을 잡는것은 말이 좀 안되는 것이다)

WriteLock을 잡고있는 상태에서도 -> WriteLock (ok)
WriteLock을 잡고있는 상태에서도 -> ReadLock (ok)

이 두 경우만 된다고 가정을 해보도록 하자.

그러면 이제 어떻게하면 되냐? 이렇게 _writeCount를 추가를 해주면된다.

WriteCount는 애당초 독점을 해서 사용을 하는 것이기 떄문에

이렇게 사용을 해도 ㄱㅊ다.

_flag에다가 WRITEMASK를 씌운다음에

16을 끌어오면은

lock ThreadID가 뽑혀 올 것이다.

그래서 비교를 해보는 것이다.

그래서 같다고 하면은

카운트 늘려주고 끝내면 될 것이다.

그리고 여기서도 바로 끝내는 것이 아니라

이렇게 writeCount를 1로 바꿔주고 끝내면 된다.

그리고

의 경우에도 이렇게 해주어야 한다.

ReadLock같은 경우에도

똑같이

복붙해서

ReadLock에서는 writeCount를 늘리는 것이 아니라

이렇게 해주자.

그런데 만약 WriteLock을 한다음에

ReadLock을 했다면 반드시 순서도 ReadUnLock을 한다음에

WriteUnLock을 해주어야 한다.

어쩃든 대충 거의다 구현을 했는데

가장 중요했던 부분이

이부분인데

이부분을 계속 분석을 해보아라

이게 Lock free 프로그래밍 == 성공할 떄 까지 계속 뺑뺑이른 도는 느낌이라고 볼 수 있을 것이다.

그래서 WriteLock만 테스트를 해보도록하자.

이렇게 만들어주고

함수를 만들기 귀찮으니까 delegate로

이렇게 t1, t2를 만들어 주도록 하자.

그리고 이렇게 하고 테스트를 해보면 될 것이다.

정상적으로 실행이 잘 되었다면

이렇게 0이 잘나온다.

그리고

중첩으로

이렇게 하고 실행을 하더라도

이렇게 값이 잘 나온다.


만약 이렇게 짝을 안 맞춰주면

영영 뺑뺑이를 돌게된다.

그래서 오늘은 이정도로 끝내겠다

이해가 안가도 노상관임

이런류의 프로그래밍을 자주하는것은 아니다.

어쩌다 한번씩한다.


ㅈㄴ 너무 어렵다 ㅅㅂ


profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글