오늘은 TLS라는 아이이다.
이게 뭐 사실은 개념적으로 어려운 부분은 아니고
이게 사실은 전역 변수인데
Thread 마다 "고유" 하게 접근 할 수 있는 전역 변수라고 생각하면된다.
그래서 이게 뭔지 알아보기전에
이게 "왜"필요한지에 대해서 부터 알아보고 시작을 하도록 하자.
그래서 식당 스토리로 와보면은
우리가 식당을 운영하면서 "락"이라는 것을 배웟는데
락을 사용을해서 화장실을 사용 ( 들어가고 잠구고 나오면서 풀고 )
하면 될 거같은데
현실적으로 보면은 이게 그렇게 간단하지가 않다.
우리가 사진처럼 주방, 손님테이블, 결제프론트 에다가 직원을 배치를 이렇게 했다고 가정을 했는데
주방에서 일어나는 일들,
테이블에서 일어나는 일들,
결제 프론트에서 일어나는 일든 서로 어느정도 연관성이 있을 것이다.
그래서 막 이론처럼
주방에서 하는 일은 딱 주방만 하고
테이블에서 하는 일은 테이블에서만 하고
그런식이 아니라
노란색 선처럼 직원들이 어느정도 왔다갔다 하면서 일을 처리를 해야한다는
문제가 많다.
그래서 이런식으로 "경합"이 일어날만한 모든 장소에다가 이렇게
락을 잡으면 어떻까? 라는 생각이 든다.
그런데 사실은
더 자세하게 보면은
"주방"안에서도 "동기화"작업이 필요할 수 있다.
주방에 이렇게 요리사가 두명있는데
만약 같은 요리를하거나 뭐 그런식이면 방해가 될 것이다.
그러니까 넓직넓직 떨어지고 자기만의 공간이 필요할 수도 있으니까
락이 필요하게 될 것이고
반대로
서빙을 하는 부분에서도 락을 안 걸고 동시다발 적으로 서빙을 한다고 하면은
1번테이블에 서빙 주문이 왔는데 직원 둘다 1번 테이블로 가서 서빙을 할 수도 있는 문제가 있다.
그래서 멀티쓰레드에서 이런 부분이 어렵다 == 일감 분배를 어떻게 해야할지가 멀티 쓰레드의 핵심이라는건데
사실, 정해진 방법은 없다. 그냥 최선의 방법을 찾아 나서는 것 밖에 없다.
그리고
다시 게임으로 예를들면은
이녀석들도 코드 로직적으로보면 어느정도 다 연관성이 있다.
게임로직을 실행해서 어떤 아이템을 강화하거나 삿을때
그것을 DB에다가 저장도 해야 될 것이고 강화가 성공적으로 이루어 졌다고 하면은
클라세션을 통해서 클라한테도
네트워크를 통해서 전송을 해야되니까
이런부분에서 연관성이 있을 수 밖에 없다.
그런데 이런식으로
락을 모든 부분에다가 하나씩 다 배치를 해서 하면
간단하게 생각하면 해결이 될 수도 있을거 같지만
이렇게 하면 사실 치명적인 문제가 있다.
바로 한쪽에다가 몰리는 경우
처리가 이제 굉장히 어려워 진다는 건데
이게 무슨말인가 하면 예를 들면
와우같은 게임에서
유져들이 드문드문 분산해서 위치해있으면 굉장히 좋을 텐데
MMO특성상 그것이 안된다.
다른 진영간에 전쟁이 일어나가지고
몇백명의 유져가 모여가지고 어떤 마을에 처들어 가거나 그런일도 있을 것이다.
근데 그렇게 되면 어떤일이 벌어지냐면은
이런 모든 클라 세션에서
모든 유져들이 같은 공간으로 "패킷"을 쏘개 될 것이다.
"게임로직"자체는 실행할 수 있는 영역이 굉장히 넓은데
특정 zone 특정 구역으로만 모든 일감이 다 쏠리는 현상이 발생을 하면은
이것을 처리를 하고싶으니까
모든 직원이 다 달려들어가지고
저 사진처럼 저곳에 모이게 될 것이다.
그런데 우리가 만약 모든 행위들을 전부다 락을 걸고 처리를 하고있다면은
문제가 되는게 뭐냐하면은
락은 상호배타적이라고 했는데
아무리 직원들이
이렇게 열심히 일을 하고싶어가지고 여기 모인다 한들
결국에는 한번에 처리를 할 수 있는 것은 한명밖에 못들어간다는 얘기가 된다.
(한번에 한명씩)
그렇다면 이런식으로 멀티 쓰레드로 직원을 많이 채용을 해봤자
결국 처리를 할 수 있는 공간은 워낙 좁다 보니까
한명이 처리해도 똑같은 분량을 괜히 여러명이 다 모여가지고 처리를 하는 그런 상황이다.
이런 경우에는 오히려 멀티쓰레드로 돌리는 경우 더 안좋아지는 경우가 많다.
문을 열고 잠구고 하는 부븐도 상당히 부화가 나기 때문에
그래서
결국에는 여기서 하고싶은 말은 뭐냐하면은
무조건 "멀티 쓰레드"환경이라고 해서
락만 걸고 들어가는게
최선의 방법이라고 장담할 수는 없다! 는것이 가장 큰 문제라고 볼 수 있다.
사실 이부분을 많은 사람들이 햇갈려한다 ( 사실은 경험이 없어서 )
서버 그냥 락만 걸고 하면 다 되는거 아니냐?? <- 바로 대가리 깨버림
이렇게 생각하고 하면 안됨!
사실, 어떤 게임을 만들지에 따라서 굉장히 달라지기는 하는데
만약 와우같은 게임을
게임로직 부분에서 하나하나 다 락을 걸고
들어가게된다면
으마으마한 일들이 벌어 질 것이다.
그래서 유져들이 한쪽 구역으로 몰릴때 처리가 어려워진다는 얘기가 되겠고
그래서 이렇게
일감들을 "분배"하는 것이 굉장히 중요한데
여기서 이제 "Thread Local Storage"가 굉장히 중요하게 사용이 되가지고
그래서 이렇게 알아보았다.
그래서 TLS에대해서 설명을 해보자면
이렇게 일감들이 모이는 부분에서
다시 식당에 비유를 함 해보도록 하겠다.
만약에 클라세션 == 테이블 에서 다 같은 메뉴 == 한식메뉴를 싹다 시켯다고 가정을 해보자.
그래가지고 이쪽에 모든 일들이 다 몰르게 된것인데
그런데 일반적으로 한식이라고 한다면 반찬이 굉장히 많을 것이다.
그 반찬이 한그릇당에 하나씩 담겨 있을 것이다.
그것을 서빙을 한다고 했을 때
직원들이 한그릇만 가져가서 테이블에 전달하고 또 한그릇만 그런식으로 하게된다고 하면
굉장히 비효율적으로 될 것이다.
그래서 보통 우리가 어떻게 하는지 생각을 해보면
큰 다라이에 다 담아가지고 한번에 서빙을 한다.
그래서 결국
이런식으로 일감이 몰릴 때는
최대한 한번에 일감을 많이 가져가가지고
여기서 다시 분배를 한것도 유용할 것이다.
우리가 이전에 쓰레드들이 이렇게 공유하는 영역이 있고
독립적인 영역이 있다고 했었는데
Heap, Data영역은 모든 Thread들이 공유를 해서 사용하고 있는데
스택같은 경우에는 제각각 따로 사용 할 수 있다고 했다.
그렇다면 아까 말한 큰 다라이를 스택에다가 두면 될까? 라는 생각이 들기는 하는데
"스택" 같은 경우에는 대대분 이제 함수에서 사용하는 용도로,
임시적인 "메모리"로 사용을 하고 있었다.
그래서 함수 호출이 완료가 되면은 스택 메모리의 일부가 날라가면서 (스택 프레임이 올라가면서)
일부 영역은 사용하지 않는다.
즉, 스택 메모리는 사용하기에는 굉장히 불안정한 메모리라고 보면 된다.
그래서 스택 자체를 저장하기 위한 공간으로 보면은 말이 안되고
그래서 우리에게 필요한것은 "초록색 박스" 언제 어디서나 접근을 할 수있는 전역 메모리 이기는 한데
이런식으로 모든 애들이 공유를 해서 사용을 하는 것이 아니라
이제는 이런식으로 각자
따로따로 사용할 수 있는 "전역 공간"이 있으면 굉장히 편리 할거 같다는 생각이 든다.
이것이 "TLS"의 개념이다.
그래서 이 상황이 뭐냐하면은
초록색 박스에 일감이 어마어마하게 많이 몰렸는데
그것을 하나씩 하나씩 하나씩 꺼내가기 보다는
엄청나게 큰 다라이 쨰로 (TLS로) 가져가가지고 (TLS로 가져온다음에)
이제 그 들고온 직원이 하나씩 처먹는(까먹는) 그런 비유를 할 수 있을 것이다.
그래서 이렇게 ServerCore로 돌아와가지고
이제는 쓰래드마다 우리가 이름을 붙여 주고 싶다.
이런식으로 static으로 선언을 하면은
전역 메모리가 되는 것이다. 근데
아쉽게도 전역으로 사용을 하는 이 메모리 공간은
모든 쓰레드들이 공유를 해서 사용을 하고 있을 것이다.
그래서 이것을 고치는 순간 다른 애들한테도 영향을 주니까, 쓰레드들에게 문제가 생길 것이다.
그러면 이것을 TLS영역으로 넣고싶으면 어떻게 하냐면은
ThreadLocal < > 이라고 이렇게 매핑해서 사용을 하면 된다.
(간단간단)
static ThreadLocal<string> ThreadName = new ThreadLocal<string>();
이렇게 만들어 주도록 하자.
그래서 이렇게 매핑은 되어있으니까 이제 이름을 넣어주기는 해야된다.
그래서 쓰래드마다 ThreadName에게 접근을 하면은
자신만의 공간에다가 ThreadName를 저장하기 때문에
어떤 특정 쓰레드에서 ThreadName을 고친다고 해도
다른 애들한테는 이제 영향을 주지 않게 된다.
그래서 이런 함수를 하나 만들어 보면은
빨간 줄이 뜨는데
지금 ThreadName은 string타입이 아니라 ThreadLocal< string > 타입이기 때문에
ThreadName . 해보면은
이렇게 Value로 값을 설정을 해 줄 수 도 있다.
ThreadName.Value = $"my name is {Thread.CurrentThread.ManagedThreadId}";
그래서 이렇게 수정을 하면은
다른애들한테는 영향을 안주는 상태로
"나의" 영역에서만 이름을 바꾸는 그런 상태가 되는 것이다.
그래서 이렇게 하면은 당연히 나의 버젼만 나오게 될 것이다.
그래서 이것은 소고쳐도 다른애들 한테 영향은 안주는데
혹시나 영향을 주는지 궁금하니까
잠시 1초 쉬고 실행을 한다고하면
그 1초 사이에 누군가가 고쳤으면은
자신의 이름이 아니라 이상한 다른 이름들이 곂처서 나올 것이다.
우리가 이때까지는
이런식으로 Taks를 만들어서 사용을 했었는데
Parallel.
라는 라이브러리가있는데
이중에서도 Invoke를 이용하면은
여기다가 넣어주는 액션 만큼을
Task로 만들어서 실행을 해줄 것인데
이런식으로
WhoAmI 를 계속 넣어주게 되면은
쓰레드 풀에 있는 애들을 알아서 꺼내서 사용하게 될 것이다.
그래서 실행을 해보면
이렇게 나온다.
이런식으로 제각각 다른애들이 나오는 것을 볼 수 있다.
그래서 만약
static ThreadLocal<string> ThreadName = new ThreadLocal<string>();
이렇게 안하고
static string ThreadName;
이럴경우에는 그냥 전역메모리에서 사용을 하게 한다면
이런식으로 똑같은 애들이 뜨는 것을 볼 수 있을 것이다.
그래서
ThreadLocal이라는 애를 사용을하면은
전역변수이기는 한 전역변수인데
thread마다 고유한 공간이 생겼다고 보면 되겠다.
그런데 지금 사용을 할때
하나의 문제점은 무었이냐하면은
Parallel.Invoke(WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI);
이렇게 쓰레드풀에서 일감을 던져줄때
무조건
ThreadName.Value = $"my name is {Thread.CurrentThread.ManagedThreadId}";
이녀석을 덮어 쓰고 있다.
근데 사실은 이미 다른애가 ( 같은 아이디로 한번)
이부분을 실행을 한 상태여가지고
이런식으로 나의 Name을 가지고 있으면
굳이
이부분을 다시 실행을 안해도 되는데
한번더 실행을 하고 있다.
( 먼말?? )
이게 무슨말이가 하면은
우리가 예전에 ThreadPool에서 이런식으로 min max를 조절 할 수가 있었는데
그래서 이런식으로 바꿔보도록 하자.
그리고 실행을 해보면
이런식으로 곂치는 이름이 나오는데 5, 4, 6 이런거
이것은 애당초 쓰래드를 풀링을 해서 사용을 하고 있는 것이기때문에
ManagedThreadId 가 일을 끝내자마자 돌아와서
다른애를 열심히 실행해주는 그런 상태가 될텐데
지금 코드의 상태에서는
그럴때마다
매번 이렇게 덮어 써주고 있는 상태이다.
그래서 만약 이부분이 마음에 들지 않는다고 하면은
static ThreadLocal<string> ThreadName = new ThreadLocal<string>();
여기 인자에다가 추가를 해줄 수 있다.
그래서 ThreadLocal의 다른 버젼을 찾아보면은
valueFactory라는 묘한애가 있는데
func타입이라 람다로 만들어 줄것인데
이렇게 해주고
그냥 여기다가
이렇게 해주면
무조건 100%확률로
Thread가 새로 실행 될때마다
이녀석을 만들어 주는게 아니라
ThreadName 가 셋팅이 안됬으면은
이부분을 실행을 해서 Value에다가 넣어주게 되는 것이다.
bool repeat = ThreadName.IsValueCreated;
라는 것을 통해서 ( 직관적인데 ThreadName이 만들어 졌다면 true 반환하는 것임 )
이렇게 반복된것은 if문으로 해주고 실행을 해보면
이런식으로 나오게된다.
repeat가 붙은애들은 기존에 한번 만든 애들이다.
그리고 이상태에서 유심히 보면은
IsValueCreated가 true인 상태라고 하면은
굳이 다시 만들지는 않는다.
만들어지는애는 재시용을 한다.
그래서 이버젼이 아까전 것보다는 조금더 효율적이라고 생각하면 될 것이다.
그리고
여기다가 breakPoint를 잡고 실행을 해보면은
여기 break 잡혀서 else 경우 Value를 보면은
repeat이 false라는 것은 맨처음으로 만든다는 말인데
지금null이라고 되어잇는데
이상태에서 ThreadName을 출력을 하면 null이 나올거같지만
null인상태에서 바로
{ return $"my name is {Thread.CurrentThread.ManagedThreadId}"; } );
이녀석이 실행이 되어서
만든다음에 이름이 나오게 되는것이다.
그리고 ThreadLocal을 사용을 할 때는
이렇게 꼭 static상황에서만 사용해야 되는 것은 아니지만
대부분의 경우에는 이렇게 전역을 붙여서 사용을 한다.
그리고 마지막으로 조금 디테일 이기는 한데
필요 없어졌다면
이렇게 날려버릴 수 있다.
여
여기서 추가해서 만들어 준것을 날려주는 것임
이정도가 우리가 알아야할 TLS의 거의 전부이고
그래서 이것을 이제 어떻게 응용을 할 것이냐?
만약 일감들이 어마어마하게 많게 큐에다가 저장이 되어있다고 하면은
그것을 큐에서 하나씩만 꺼내서 처리를 하는 것이 아니라
한 100개씩 한 뭉틍거리를 뽑아와가지고
일단 자기만의 공간에 넣어둔다음에(TLS에 넣어 둔다음에)
그 다음 TLS에서 사용을 하는 것은 굳이 락을 걸지 않더라도
아무런 부담 없이 뽑아서 쓸 수 있다.
여기는 나의 공간이니까 필요할때마다 일감을 하나씩 꺼내서 사용하면 될 것이다.
그래서 어떤 공용 공간에다가 접근을 하는 횟수를 줄이는데도 큰 의미가 있는 것이다.
예를들어 잡큐라는 애가 static공간에 있다고 하면은
모든 쓰레드들이 동시 다발적으로 막 잡큐에 경합을 해야 할것이다.
이전에는 예한테 접근을 할때 락을 잠궜다가 풀었다가 다시 잠궜다가 이런식으로 접근을 했어야됬는데
한번 락을 잡을때 일감을 하나만 빼오는게 아니라
이런 TLS공간에다가 실컷 많이 뽑아 오면 된다는 얘기가 된다.
그럼 결과적으로 보면은
이곳에 락을 한번 건다음에
많은 일감을 빼온 셈이 되는 거니까
일감을 처리하기 전까지는
잡큐에 접근을 할 필요가 없어지는 것이다.
그런식으로 부화를 줄일 수도 있다는 얘기이다.
물론 이런 상황이 아니다 하더라도
TLS는 정말 다양하게 사용이된다.
지금처럼
이런상황 즉,
쓰레드의 고유한 아이디를 만든다거나
그 쓰레드 에서만 사용할 쓰레드 고유의 전역변수를 사용한다고 하면은
ThreadLocal을 사용하면 될 것이다.
그래서 우리는 이제 기본기는 이제 아는 상태라고 봐도 된다.