Threads Considered Harmful
Summary
쓰레드(thread)의 문제점을 지적한 것으로 상당히 유명한 글 중에 하나가 바로 Kuro5hin에 올라온 ‘Threads Considered Harmful ‘이라는 글이다. 이 글의 주요 논지는 shared state에 대한 synchronization을 제대로 하는 것과 dead lock을 방지하는 것이 어렵고, 이를 탐지하기가 어렵기 때문에, 쓰레드(thread)를 사용하지 말아야하고, 그 대안으로 Multiple processes, Event-based programming, Co-operative threads (co-routines) 등의 방법을 사용해야한다는 것이다.
Detailed Review
맨 첫 문단에서 이 글의 저자는 다음과 같이 얘기하고 있다.
once you have spawned a thread, there is no way to know anymore which line is being executed in parallel with yours.
이 얘기는 ‘Go To Considered Harmful‘에서 Dijkstra가 주장하고 있는 프로세스의 진행 정도를 표현할 수 있는 의미있는 좌표가 존재해야한다는 아이디어를 thread에 적용한 것이다. 하지만, 적어도 구조적인(structured) 프로그램을 수행하는 여러 thread의 진행 정도를 표현하는 의미있는 좌표는 존재한다. ‘Go To Considered Harmful’ 논문에 대한 리뷰 글에서 설명한 방식을 빌려오자면, 우리가 여러 쓰레드를 사용하는 프로그램을 디버깅하기 위해 멈췄을 때, 우리는 모든 쓰레드의 진행 정도를 각 쓰레드의 stack trace를 통해서 알 수 있다. 글의 저자는 문제를 약간 잘못 설명하고 있다. 진짜 문제는, 서로 간에 side-effect를 미칠 수 있는 쓰레드들이 어떤 식으로 수행되느냐에 따라 결과가 달라지기 때문에, 프로세스의 진행 정도를 알 수 있다고 해서 디버깅을 할 수 없게된 것이다. Dijkstra가 위의 논문 처음에서 내세웠던, 정적인 프로그램과 동적인 프로세스 간의 상응성을 단순하게 만들기 위해서는 프로세스의 진행 정도만으로는부족한것이다.
예를 들어, a-b-c-d라는 프로그램을 수행하는 두 thread가 있어서 어떤 시점의 프로세스들의 진행 정도가 둘다 d라는 걸 알더라도, 실제로 수행된 순서가 ababcc인지 aabbcc인지를 알지 못하면 디버깅을 불가능한 것이다.
여담으로, 쓰레드를 하나의 프로세스로 보지 않고,모든 thread를 각 쓰레드가 수행하는 프로그램이 랜덤하게 수행되는 하나의 프로세스로 본다면, 프로세스의 진행 정도를 표현할 수 있는 좌표 자체가 존재하지 않는다고 볼 수도있다. 이 문제에 여러 프로세서(SMP)의 문제까지 끼어들면 더욱 복잡한 모델이 된다.
한편, 다음 문장이 여러 쓰레드를 사용하는 프로그램의 디버깅이 힘든 이유를 한마디로 표현하고 있다.
The chief problem with threads are race conditions and dead locks. Both tend to occur randomly.
친절하게도 자세한 예를 들어 synchronization의 어려움과 dead lock을 방지하는 것의 어려움을 강조하고 있고, dead lock을 방지하기 위해 lock들의 순서를 지키는 것이 부담스러워서 coarse-grained lock을 쓴다면 performance 저하의 문제가 있음을 지적한다.
저자가 쓰레드를 사용하는 것에 어떠한 이익도 없다고 결론 내리고 대안으로 내세운 것은 Multiple processes, Event-based programming, Co-operative threads (co-routines)가 있다.
My Opinion
- 쓰레드의 필요 뿐만 아니라 쓰레드의 대안들에서 오는 복잡성은 모두 concurrently shared state (또는 resource)의 필요에서 발생한다. concurrently shared state가 전혀 없다면 물론 쓰레드를 사용할 필요는 없다. 글의 저자가 주장한 것과 같이 하나 또는 그 이상의 프로세스를 사용해도 되고, 설령 쓰레드를 사용한다고 하더라도 글의 저자가 우려하는 것과 같은 사태는 절대 발생하지 않는다. 하지만, 많은 프로그램은 어느 정도의 concurrently shared state를 필요로 한다. 그것이 메모리 조각이든, 객체든, 파일과 같은 커널 리소스든, 그것이 공유되기 시작하면, 여러 프로세스에서도 똑같은 문제가 발생하기 시작한다. 복잡성의 원인은 해결법(쓰레드)에 있는 것이 아니라 궁극적으로 우리가 풀어야하는 문제 자체에 내재하고 있는 것이다.
- shared state vs. messaging 문제. shared state와 messaging 사이에 어느 것을 선택할 것이냐 하는 것은 컴퓨팅의 역사에서 항상 반복되어온 문제다. OS의 디자인이나 구현을 보다보면, 어떤 resource (또는 state)에 대한 접근을 strict하게 제한할 것이냐, 아니면 시스템이 fragile하지 않는 한 느슨하게 허용할 것이냐의 trade-off 문제가 상당히 자주 등장한다. 여기에는 성능이나 디자인 등의 문제가 복잡하게 게재된다. 쓰레드를 쓸 것이냐의 문제도 trade-off 문제에 불과하다고 생각한다.
- 적절한 대안의 부재: 프로그래밍은 계속 더욱 높은 레벨의 abstraction을 지향해서 언젠가는 이런 고민을 하지 않아도 될 날이 올 것이다. 하지만, 현재로서는 만족할만한 대안이 없는 것이 사실이다. 프로세스는 concurrently shared state가 없다는 가정하에서 쓰레드와 다를 바는 없고, 이벤트 기반 프로그래밍은 쓰레드 만큼의 어쩌면 더 많은 복잡성을 프로그램 내에 도입할 뿐만 아니라, 쓰레드 지원이 허름하던 시절의 성능 이점을 가져다 주지도 않는다. (쓰레드 프로그래밍과 이벤트 기반 프로그래밍의 비교에 대해서는 다음 기회에 글을 써보도록 하겠다.) Parallel programming 문맥에서의 Co-routine은 go to를 대체하는 loop와 같은 programming constructs의 역할과 비슷한다고 생각되는데, 주요 프로그래밍 언어에서 구현되어 있지 않을 뿐만 아니라, (그 때문에) 보편적으로 practice가 확립되어있지 않다. (학교에서 가르치지 않는다.)
Conclusion
concurrently shared state라는 문제 자체의 복잡성이 쓰레드 프로그래밍이나 이벤트 기반 프로그래밍의 복잡성을 야기하고, 어느 해결책을 선택하느냐는 trade-off의 문제이나, 현재로서는 쓰레드가 더 나은 해결책으로 보인다. go to를 대체하는 loop constructs와 같이 쓰레드를 대체하는 프로그래밍 언어 레벨의 해결책이 필요하다는 것에는 크게 공감한다.