pthread 프로그래머가 Win32에서 밟기 쉬운 지뢰 하나

미리 밝혀두지만 rein이 정말 본격적으로 Windows 시스템 프로그래밍을 한 것은 작년 부터다. 그런 의미에서 겪었던 삽질 하나를 밝혀둔다.1

Linux system에서 널리 사용되는 posix thread (이하 pthread) 라이브러리의 가장 기본적인 동기화 메커니즘은 pthread_mutex_t 라는 타입으로 불리는 일종의 mutex다.2 이걸 쓰던 사람이 Windows의 CRITICAL_SECTION 이나 Win32 mutex를 사용할 때 가장 실수하기 쉬운 것.

Win32의 CRITICAL_SECTION이나 mutex는 recursive하게3 lock을 잡는 것을 허용한다.4

거꾸로 Win32 프로그래머가 linux pthread를 쓴다면 주의할 것,

pthread의 mutex는 기본적으로 recursive–locking을 허용하지 않는다

즉, 다음과 같은 코드는 영원히 정지한다(=deadlock).

pthread_mutex_lock(&lock);
pthread_mutex_lock(&lock); // 이 줄을 벗어날 수 없다

pthread_mutex_unlock(&lock);
pthread_mutex_unlock(&lock);

Win32에 익숙한 프로그래머라면 내가 주석처리한 저 코드를 보고 갸웃할지도 모른다. 그렇지만 pthread_mutex기본 동작은 recursive–locking에 대한 거부다. 그렇지만 다음과 같은 방식으로 mutex를 생성하면 recursive–locking을 허용하게 만들 수 있다.

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
int val = PTHREAD_MUTEX_RECURSIVE_NP;
pthread_mutexattr_settype(&attr, val);
pthread_mutex_init(&lock, &attr);

이런 조금 귀찮은 과정을 거치면 recursive–locking을 지원하는 — win32 프로그래머가 좀 더 익숙해 할, 혹은 그런 종류의 lock이 필요한 상황에서 써야하는 — pthread용 mutex가 생성된다.

프로그래밍을 주로 *nix에서 시작한 나는 pthread의 기본 동작을 조금 더 좋아한다. 이유는 예전에 설명했던 Dead-lock;데드락을 막는 locking protocol 5때문이다. Win32의 기본적인 recursive–locking에서는 다음 시나리오에서 필패한다(잠재적인 dead–lock을 허용한다)

  1. lock L2
  2. … some code block …
  3. lock L1
  4. lock L2
  5. lock L3

이 경우에 pthread_mutex라면 4에서 이미 프로그램이 멈추기 때문에(기본적인 pthread_mutex 동작에서), 상대적으로 빨리 눈치채고 대응할 수 있다. (아마도 프로그램이 릴리즈 되기 전에). 반면에 Win32환경에서는 운만 좋으면6 릴리즈되기 전에 데드락이 숨어있다는 것을 알 수 없게 된다.

물론 win32도 좀 쓰기 시작한지라 lock 전체의 잡는 순서를 로깅하는 간단한 per-thread 객체를 하나 써서 lock순서를 추적하기 때문에, 이런 일은 바로 눈치채고 있긴하지만, 가끔은 pthread_mutex가 그리워지기도 해서 이런 글을 끄적대 본다.

ps. 덤으로 intel TBB의 hash_table의 숨겨져있는 lock들은 pthread의 convention을 따른다. 즉 두 번 잡으려고하면 해당 스레드는 거의 영원히 멈추게 된다. 그런 의미에서 주의해야.


  1. 덤으로 금요일 저녁에 Rica한테 잘못된 사실을 하나 말했던 것을 정정하려한다. ↩︎

  2. 물론 spin–lock도 pthread_spinlock_t로 제공된다. ↩︎

  3. 재귀적으로. 즉 thread A가 어떤 mutex나 CS의 락을 얻은 상태에서 또 해당 락을 얻으려고 시도하면 잡을 수 있다. ↩︎

  4. 내가 Rica한테 잘못 설명한 부분이 여기. 난 Mutex는 재귀적으로 못잡는다고 생각했다. 근데 되더라고… ↩︎

  5. Lock들에 번호를 부여하고, 특정 순서로만 lock잡는걸 허용하는 것. ↩︎

  6. 3이 실행되기전에 L1을 잡는 스레드가 없기만 하면된다. ↩︎