나의 삽질 유산 답사기... #2. 멀티 쓰레드에서 정적 객체를 사용할 땐 조심해야 합니다.
2편은 쓸지 안쓸지 몰랐는데 2편도 씁니다. 과연 3편은 쓸지 모르겠습니다. 모이다 보면 어떤 사람들에겐 꿀 팁이 될지도 모르고 어떤 사람들에겐 별것 아닌 글만 올리는 것이 될 수도 있다는 생각이 드네요.
그래도 별것 아닌 삽질이지만 기억해 둘 만한 꺼리라고 생각되어 정리합니다.
동시에 동작하는 멀티 쓰레드 프로그래밍에서 다수의 쓰레드가 하나의 정적 객체에 접근할 때, 특정 상황에서 신경써야 할 부분이 있습니다. 어떤 상황이냐면, 쓰레드의 로컬 데이터를 정적 객체에 던져주고 그 데이터를 정적 객체가 사용할 때입니다.
문장으로 써 놓으니 제가 생각해도 의미 전달이 다 안된것 같군요. 그럼 코드를 봅시다. 코드는 편의상 의사코드로 작성했습니다. 제가 삽질했던 코드를 가져올 순 없는 상황이네요..^^;
class 정적객체 { public static 쓰레드객체 날부른녀석 = null; public static void 매번실행(void); }; class 쓰레드객체 { public 쓰레드객체(void); public string 나라이름; public void 쓰레드본체(void); };
이렇게 두 클래스를 선언해 놓습니다. 구현은 크게 신경쓸 부분이 없습니다. 관계와 흐름에 대한 이야기를 하고 싶어서 이 글을 쓰고 있으니까요. 그래도 구현이 너무 없으면 무슨 말을 하고 싶은건지 알수 없겠죠? 그리고 main() 함수도 없습니다.
일단 main() 함수는 이렇습니다.
void main() { string[] 나라이름들 = {"한국", "미국", "일본", "중국", "러시아"}; for(int i = 0 ; i < 나라이름들.Length ; i++){ 쓰레드객체 쓰레드하나 = new 쓰레드하나(); 쓰레드하나.나라이름 = 나라이름들[i]; Thread thread = new Thread(쓰레드하나); thread.Start(); } }
아직은 제가 작성하는 프로그램의 정확한 목적이 보이지 않습니다. main() 함수만 봐서는 쓰레드 5개를 만들어서 각각 쓰레드에 한국, 미국, 일본, 중국, 러시아라는 이름을 넣어준것 뿐입니다. 그리고 쓰레드를 바로 시작해 버리는 군요. 쓰레드 구현을 봐야 알겠습니다.
void 쓰레드객체::쓰레드객체(void) { 정적객체.날부른녀석 = this; } void 쓰레드객체::쓰레드본체(void) { while(true){ 정적객체.매번실행(); } } void 정적객체::매번실행(void) { print 정적객체.날부른녀석.나라이름 + "에 가보았나요?"; }
이게 필요한 코드의 전부입니다.
쓰레드객체의 생성자에서 정적객체에 자기 자신(this)를 넘겨주고 실제 쓰레드 본체에는 정적객체의 실행 함수를 호출해서 main()에서 할당받은 자신의 나라이름을 출력하는 것입니다.
제가 의도한 동작은 쓰레드 다섯 개가 동시에 돌면서 "한국에 가보았나요?" , "미국에 가보았나요?" 같은 문자열을 계속 출력하는 것입니다. 하지만 실제로 돌려보면 의도한대로 동작하지 않습니다. 코드를 보자마자 문제를 발견하고 '뭐 이런걸 가지고 삽질이라고 글을 써 놓은겨?' 하는 분도 많으실겁니다.
만약 문제가 된 코드가 저정도 수준의 짧은 코드였다면 저도 삽질 안했겠죠? ^^;
그리고 저라면 아예 저런식으로 코딩하지도 않았을겁니다. 쓰레드에서 왜 정적 객체를 동시에 접근해서 서로 다른 레퍼런스를 참조해 가며 동작하게 만드나요? 쓰레드의 내부 컨택스트를 철저하게 분리해서 다른 쓰레드로부터 영향을 받지 않도록 만들어야지요.
이론은 이러하지만 실무에서 코딩은 참 많은 제약이 따릅니다. 반드시 써야만 하는 라이브러리가 저딴식으로 정적 호출만 지원하고 그 라이브러리를 써야하는 환경이 멀티 쓰레드를 써야만 하는 환경이라면 저런식으로 코드가 나올 수 밖에 없겠지요...
잠시 현실을 푸념했습니다..ㅠㅠ
다시 원래 글로 돌아와서, 그럼 문제는 무엇일까요? 바로 쓰레드 객체의 생성자에 있습니다. 코드를 다시 볼까요?
void 쓰레드객체::쓰레드객체(void) { 정적객체.날부른녀석 = this; }
이게 왜 문제지? 하시는 분들은 저의 글을 통해서 얻어가시는 것이 있을겁니다. ^^;
쓰레드는 정적객체가 "나에게 할당된 이름"을 출력하길 원하므로 정적객체에게 나 자신(this)를 넘겼습니다. 문제될게 없지요. 실제로도 쓰레드를 한 개만 돌리면 문제 없습니다. 하지만 두 개 이상부터 문제가 발생하지요. 무슨 문제나면 가장 나중에 등록된 이름만 나온다는 것입니다.
왜냐면 정적객체.날부른녀석 이라는 변수가 바로 "정적객체"이기 때문입니다. 이게 무슨 말 장난이냐... 하시지 말고 잘 생각해 보시면 정적 객체는 스코프 안에서 오직 한 개만 존재합니다. 그래서 저렇게 코딩하면 생성자가 불리울 때마다 정적객체.날부른녀석이 계속 덮어 써 지면서 결국엔 가장 마지막에 할당된 녀석만 인정되는 것입니다.
그럼 어떻게 고쳐야 할까요? 매우 간단합니다.
<code> void 쓰레드객체::쓰레드객체(void) { } void 쓰레드객체::쓰레드본체(void) { while(true){ 정적객체.날부른녀석 = this; 정적객체.매번실행(); } }
이렇게 고치면 됩니다. 정적객체.매번실행()을 호출하기 직전에 항상 정적객체.날부른녀석을 갱신해 주는 것이지요. 물론 이렇게 하더라도 정적객체.매번실행()이라는 함수가 thread safe하게 작성되어 있다는 전제가 필요하지만요. 일단 그렇다고 친다면 저렇게 수정해서 문제를 해결할 수 있습니다.
전형적인 하나는 잘 되는데 둘이나 셋을 돌리면 안도는 문제 중 하나입니다. 꽤나 흔한 상황이고 파악하기 쉬운 내용인데 일단 잘못 코딩해 놓으면 의외로 찾는데 시간이 걸립니다. 이런 상황이 있다는 것을 한 번 알아두면 이후에 비슷한 상황에 같은 패턴의 오류가 발생했을 때 원인을 쉽게 파악할 수 있을 것이라 생각합니다.
일정과 지원부서의 비협조 덕에 요구사항에 동시 동작이 있음에도 프로그램의 규모가 어느정도 커 질 때까지 동시 동작 테스트를 못하고 싱글 쓰레드만 테스트하다 보니 잘 돌아가는 줄 알았습니다. 결국 끝까지 협조는 요원한 상태에서 일정이 다가와 좀 억지로 에뮬레이팅 환경을 만들어서 멀티 쓰레드로 돌렸더니 예기치 못한 문제가 발생해서 저 버그 잡는데 꽤 시간을 소모했습니다. 시간보다 갑작스런 상황에 정신력을 더 소모한것 같군요. 이 글을 쓰면서 힐링합니다...ㅠㅠ
댓글
void
thread safe하게 동작하려면 lock으로 감싸야 할 것 같습니다. 안 그러면 다음처럼 동작할 가능성이 있기 때문입니다.
네! 맞습니다. 실제로는 정적 객체를 atomic하게
네! 맞습니다.
실제로는 정적 객체를 atomic하게 참조하지 않는 부분의 처음과 끝을 모두 critical section으로 만들어 주어야 합니다.
다만 본문에서 하고 싶은 이야기는 그게 아니어서 언급하지 않았습니다.
답글로라도 지적해 주셔서 감사합니다.:)
----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라
글 잘 봤습니다. ^.^
글 잘 봤습니다. ^.^
Lum7671's Weblog
댓글 달기