1. 자동차 배터리와 컴퓨터 스택 메모리

자동차의 배터리 방전과 컴퓨터 프로그램의 스택 오버플로는 서로 완전히 다른 분야이긴 하지만 공돌이의 심상 면에서 공통점이 느껴지는 것도 좀 있는 것 같다.
지난 수십 년 동안 자동차는 엔진 효율이 크게 향상됐고 컴퓨터의 메모리도 동적 힙(heap)이야 넘사벽 급으로 용량이 뻥튀기 됐지만, 저것들은 용량이 획기적으로 크게 올라간 적이 없다. (특별히 그럴 필요가 없어서..)

그리고 저 둘은 남은 용량이나 고갈 징후를 미리 알려 주는 메커니즘도 딱히 존재하지 않는다.
자동차의 황산-납 배터리는 스마트폰이나 노트북의 리튬이온 배터리처럼 용량이 몇 % 남았다고 알려주는 기능이 없다. 블랙박스도 배터리의 전력 용량이 감소하면서 전압도 같이 감소하는 걸 감지해서 간접적으로 꺼진다거나 할 뿐이다.

컴퓨터 프로그램의 스택 메모리도 지역변수의 주소를 이용해서 남은 용량을 아주 간접적으로 유추할 수 있고, 시스템 exception을 이용해서 일이 벌어졌을 때 스택 overflow를 감지할 수도 있다. 하지만 이식성 있는 일반적인 방법은 존재하지 않는다. 그걸 일일이 체크하는 건 아주 비효율적이다. 이런 식으로 유사점이 있다.

2. TLS 슬롯

메모리와 관련하여 이렇게 아기자기하게 제약이 될 수 있는 게 본인이 보기엔 TLS 슬롯 공간이다.
사실, 한 사람이 만드는 프로그램이 수십~수백 종류의 코드가 동시에 실행되는 프로그램을 만들 일은 극히 드물겠지만, 문제는 내가 만들지 않은 프로그램(특히 DLL)들이 한 프로세스에 왕창 붙어서 실행될 때이다. 이런 코드들이 전부 TLS 슬롯을 하나씩만 요청하더라도 TLS 공간은 수십~수백 개씩 소모될 수 있다.

Windows 95 내지 NT 3.x시절에는 현실적으로 필요한 양과 그 당시 컴퓨터들의 평균적인 사양을 감안해서 스레드마다 TLS 슬롯이 64개씩 배당되었다. 이게 바로 TLS_MINIMUM_AVAILABLE라는 상수값의 의미이다. 역대 win32 환경 중에 TLS 슬롯이 제일 적었던 구현체가 제공했던 최소 개수가 64라는 뜻이다.

세월이 흐를수록 기본 제공되는 TLS 슬롯 수는 점차 늘어나서 Windows XP/2000부터는 64에다가 1024가 더해진 1088개라고 한다. 후대의 운영체제에서 슬롯 수가 이것보다 더 늘어났다는 얘기는 본인은 들어 보지 못했다. 힙 메모리처럼 엿가락처럼 한없이 더 늘어나야 할 필요는 없는 물건이기 때문이다.

스레드 단위로 전역적으로 공유돼야 하는 데이터가 많이 있거나 그 데이터의 길이가 들쭉날쭉 변한다면 별도의 heap 메모리를 할당하고 TLS 슬롯에다가는 포인터만 저장해야 한다. 즉, 응용 프로그램은 언제나 고정된 개수만의 슬롯을 사용하고, 슬롯 사용량을 최소화해야 한다.

내가 보기에 TLS가 꼭 필요한 때 중 하나는 사용하는 라이브러리가 너무 구닥다리여서 콜백 함수에 context 데이터를 넘겨주는 게 없어서 context 정보를 오로지 전역변수에 의지해야 할 때(+ 그런데 thread-safety가 보장돼야 할 때) 정도이다.
한 프로그램이 스레드 10개로 동작하건 100개로 동작하건 그건 소모되는 TLS 슬롯 개수와는 전혀 무관하다. 이건 그냥 실행되는 코드의 종류하고만 관계가 있다.

3. 복합 스레드 동기화

조금 부끄러운 얘기를 하자면.. 본인은 오래 전 학교의 컴공 운영체제 시간에 졸았는지, 아니면 다른 변고가 있었는지는 모르겠지만 나만의 스레드 동기화 오브젝트를 새로 만든다는 개념 내지 필요성을 지금까지 별로 이해하지 못했다. 그냥 운영체제가 제공하는 critical section이나 뮤텍스, 세마포어만 쓰면 끝이지 않은가..?? 그랬는데 회사 코드를 들여다보면서 뒤늦게야 '아...!!' 현타 비스무리한 걸 경험하게 됐다.

평범한 데이터 컨테이너가 멀티스레드 동작에 대비하여 곳곳에 뮤텍스 기반의 lock이 걸려 있는데.. 데이터를 건드리는 set쪽뿐만 아니라 단순히 read만 하는 get 메소드들까지도 내부가 전부 동일한 lock이 일일이 걸려 있었다. 흠, 이건 read일 뿐인데 여러 스레드가 동시에 접근해도 상관없지 않나? 저건 불필요한 삽질 오버헤드 아닌가? 이 lock은 빼 버려도 되겠는데?

그런데 알고 보니 그 생각은 절반만 맞았다. reader만 있을 때는 여러 스레드들이 동시에 접근해도 괜찮지만, 한 스레드라도 write를 할 때는 다른 writer는 물론이고 reader들도 다 줄 서서 기다려야 하기 때문이다. 그러니 get 함수도 lock이 전혀 없어서는 안 된다.

하지만 이렇게 정말 간단하고 범용적인 원리대로 lock의 종류를 구분하는 것이 운영체제 API로는 의외로 직통 구현돼 있지 않았다. 기존 동기화 오브젝트를 조합해서 사용자가 직접 구현해도 되며, 이런 복합 동기화는 읽기/쓰기 중 한쪽으로만 CPU가 너무 쏠리고 있을 때 분배를 어떻게 할지 같은 정책이 일방적으로 획일화 가능하지 않기 때문이다.

그래도 C++ 라이브러리의 스레드/동기화 클래스 중에는 이런 "다수 reader/단일 writer" 동기화를 구현한 놈이 혹시 있는지 모르겠다. 이런 클래스는 그냥 lock와 unlock만 있는 게 아니라 lock이 lockForRead와 lockForWrite로 세분화된다.

이렇게 custom 동기화 오브젝트를 만들면 기존 운영체제 API만으로는 바로 구현 가능하지 않은 복합 동기화를 시전할 수 있다. reader와 writer가 여러 놈이 동시에 경합할 때, 그리고 read와 write의 소요 시간이 차이가 많이 날 때 어떤 원칙으로 CPU를 배분할지 같은 세부 원칙도 손수 정할 수 있다.

그리고 디버그 빌드에서는 각종 참고 정보를 알려주고 오류를 검출하는 기능도 넣을 수 있다. 현재 포커스를 잡은 스레드, 또는 대기 중인 스레드들을 얻어 온다거나..
lock을 건 스레드가 아직 실행 중인데 그 동기화 오브젝트가 소멸되는 건 잘못된 상황이므로 곧장 예외나 assertion failure를 날린다.

(이 글을 쓰고 나서 나중에 알게 된 사실: Windows Vista에서부터 Slim Reader/Writer (SRW) Lock이라는 게 도입됐다. 크리티컬 섹션처럼 동일 프로세스 안에서만 사용 가능한 대신.. reader lock과 writer lock을 구분해서 요청 가능한 작고 가벼운 동기화 오브젝트이다. 역시 범용성이 있으니 2000년대 이후에야 도입됐구나 싶다.)

그리고 디버깅에 제일 도움이 되는 정보는.. 아무래도 deadlock 감지일 것이다.
응답이 멎은 프로그램을 강제로 break시킨 뒤, 스레드들의 스택 상태를 추적하다 보면 memory leak보다는 쉽게 찾을 수 있겠지만 크고 복잡하고 스레드를 왕창 많이 만드는 프로그램에서 이런 걸 프로그램이 디버그 정보를 통해 자동으로 찾아주면 디버깅에 큰 도움이 될 것이다. 물론 이 경우, 동기화 오브젝트가 락에 진입한 스레드들의 실행 정보를 일일이 관리하고 있어야 할 것이다.

Posted by 사무엘

2024/01/24 08:35 2024/01/24 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2256


블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/05   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

Site Stats

Total hits:
2686472
Today:
947
Yesterday:
1316