[OS/OSTEP] 27.threads-api
# 시작하며
저번 챕터에서는 쓰레드에대한 전반적인 개념과 쓰레드가 어떻게 형성되고 사용되는지, 또 락은 무엇인지에 대한 간단한 개념을 배웠다. 이번장에서 또한 락, 컨디션 변수에 대해서 짚고 넘어간다.
# 쓰레드 생성 - 쓰레드는 어떻게 구성될까?
위와 같이 쓰레드 생성시에는 4개의 인자를 보내주어야 한다. 첫번째 인자 thread는 타입 구조체를 가르키는 포인터이다. 두번째 인자 attr은 쓰레드를 초기화할때 필요한 인자인데, 기본적으로 디폴트 값인 NULL을 사용하면 된다고 한다. 세번째인자 start_routine은 쓰레드가 실행할 함수를 나타낸다. 마지막으로 네번째 인자 arg은 실행할 함수에게 전달할 인자가 들어간다.
# 쓰레드 종료 기다리기
쓰레드의 종료를 기다리기 위해서는 join을 이용한다.
첫번째 인자는 어떤 쓰레드를 기다릴것인지를 나타내며, 두번째 인자는 반환 값에 대한 포인터이다.
# 쓰레드 사용시 주의할 점
쓰레드는 반환값이 있다는것을 위에서 알 수 있었다. 쓰레드 사용시 이 반환값때문에 주의해야 할 이유가 생긴다. 먼저 쓰레드는 프로세스의 메모리영역에서 쓰레드별 스택을 가지게 된다. 쓰레드 내에서 사용했던 지역변수를 리턴해버리면, 해당 쓰레드는 더이상 사용하지 않는 메모리 영역이 되지만, 리턴해서 그 값을 쥐고 사용하게 된다면 원치 않는 결과를 야기할 것이다.
새로 생성된 쓰레드가 가지는 mythread루틴에서 r은 쓰레드가 리턴(종료)될때 자동적으로 해체된다. 해체된 값을 가르키는 포인터를 반환하게 된다면 원하는 결과를 얻지는 못할 것이다.
# 락 ( Lock )
쓰레드라는 것을 공부했거나 들어봤다면, 락이라는 용어를 한번씩은 접해 봤을 것이다. 여러 쓰레드가 공통적으로 사용하는 값을 임계 영역이라고 했는데, 이러한 값들에 여러 쓰레드가 동시에 접근하면서 생기는 원치않는 결과를 방지하기 위해서 쓰인다. 즉 임계 영역에 대한 상호 배제를 위해 사용된다. 만약 A라는 쓰레드가 락(lock)을 지니고 있고, B쓰레드도 동시에 그 값에 대한 접근이 필요하다면, B쓰레드는 락(lock)을 가지지 못했기 때문에 락을 얻을 때까지 호출에서 리턴하지 않는다. 즉 임계 영역 부분을 B쓰레드는 실행 할 수 없는 것이다. 아래 그림을 참고하면서 제시한 상황을 머릿속에 그려보면 좋을 것 같다.
이렇게 락을 사용하기 위해서는, 초기화를 해주어야 한다.
초기화를 했다면 당연히 해당 락이 잘 초기화 됐는지 확인하는 과정을 거쳐야 한다.
그러나 앞으로 이러한 과정은 Pthread_mutex_lock()이라는 래퍼함수를 만들어 생략하게 된다.
# 컨디션 변수 ( Condition Variable )
자세한 설명은 뒷 단원에서 다뤘던 것 같았다. 이 컨디션 변수 챕터를 공부하면서부터 헷갈려지기 시작했다. 교수님께서는 이 컨디션 변수(Condition Variable)를 큐라고 생각하면 된다고 했다. 컨디션 변수는 아래와 같이 wait와 signal을 통해 사용된다.
wait가 호출되면 해당 쓰레드는 sleep(수면)상태로 들어가고, 다른 쓰레드가 깨워주기를 기다린다. 이렇게 wait가 되는 쓰레드들이 뒷 부분에서 자세히 다루겠지만 큐에 주렁줄어 매달리고, signal이라는 깨워주는 신호가 오며 순서대로 깨워졌다.
wait의 두번째 인자로는 락(lock)이 들어가게 된다. 그 이유는 잠들기 직전에 락을 해체(unlock)해 주어야 하기 때문이다. 락을 반납하지 않고 잠들게 된다면 어떤 쓰레드도 락을 얻지 못할 것이다. 깨워 주려면 임계영역에 들어가야하고 임계영역에 들어가기 위해서는 락을 얻어야하기 때문이다.
위 코드는 sleep상태의 쓰레드를 깨우기 위해 signal을 실행하는 코드이다. 코드를 보면 알 수 있듯이 lock을 얻어야만 하기 때문에 wait전 락을 반납하는 것은 아주 중요한 루틴 중 하나이다.
## 대기하는 쓰레드의 조건을 검사할때는 while 사용
위에서 ready상태를 검사하는 일련의 과정을 거쳤는데 while을 사용한 것을 알 수 있다. 이렇게 if가 아닌 while을 사용해서 조건을 검사해야 한다고 한다. while을 사용하는 방법이 안전하고 간단하다고 교재에서는 다루고 있으며, signal의 실행은 변경 사실을 알리는 것이 아닌, 변경 된 것 같으니 확인해보라는 뜻이라고 한다. 즉 signal로 깨워졌지만 그 signal이 잘못됐을 수 있기 때문에 wait하고 나서도 한번더 조건이 변경 됐는지를 확인해 봐야한다. if문을 사용하게 된다면 이러한 재검사의 과정이 없이 예정된 다음 코드가 실행 될 것이다.
## wait하지 않고 while을 사용해서 조건을 검사하기
위에서 처럼 wait를 하지않고, ready가 변화되는지를 계속해서 while문을 돌면서 검사할 수 있다. 이를 spin이라고 했는데 이 경우에는 코드의 성능이 좋지 않다고 한다. 왜냐하면 cpu가 단순히 ready의 변화를 감지하기 위해서 계속해서 일을하게 되기 때문이다. wait를 사용했을때는 sleep되어 cpu의 제어권을 넘겨줬던거와는 상반된다.
또 오류가 발생하기 쉽다고한다. 그 오류는 코드를 작성하는 사람이 실수하여 버그를 유발시킨다고 한다.
# 마치며
쓰레드 단원에서 공부하게 될 것들을 간단하게 살펴 봤다. 여기서 나왔던 키워드들 하나하나를 조금 더 심도있게 다루게 될 것이다.