(전체가 아니라 C#과 차이가 있는 부분을 중심으로 요약 정리)

멀티스레드 프로그래밍(multithreaded programming)은 프로세서 유닛이 여러 개 장착된 컴퓨터 시스템에서 중요한 기법이다. 멀티스레드 프로그래밍을 이용하면 시스템에 있는 여러 프로세서 유닛을 병렬로 사용하는 프로그램을 작성할 수 있다.

독립적인 CPU를 담은 프로세서 칩이 여러 개 달려 있을 수 있고, 한 프로세스 칩 안에 코어라 부르는 독립적인 CPU가 여러 개 있을 수 있고, 또 어떤 시스템은 두 가지 방식을 혼합하기도 한다. 이렇게 프로세스 유닛이 여러 개 달린 프로세서를 흔히 멀티코어 프로세서라 부른다.

요즘 판매되는 CPU는 모두 멀티코어 프로세서다. 멀티코어 프로세서가 보편화 됐기 때문에 멀티스레드 애플리케이션을 작성할 줄 아는 것은 중요하다. 전문 C++ 프로그래머라면 프로세서의 기능을 최대한 활용할 수 있도록 멀티스레드 코드를 정확하게 작성할 줄 알아야 한다.

멀티스레드 애플리케이션은 플랫폼이나 OS에서 제공하는 API에 상당히 의존한다. 그래서 멀티스레드 코드를 플랫폼 독립적으로 작성하기는 힘들다. C++ 11부터 제공되는 표준 스레딩 라이브러리를 활용하면 이 문제를 어느 정도 해결할 수 있다.

멀티스레드 프로그래밍 개념

멀티스레드 프로그래밍을 사용하면 여러 연산을 병렬로 처리할 수 있다. 그래서 현재는 거의 모든 시스템에 장착된 멀티 프로세서를 최대한 활용할 수 있다. 20년 전만 해도 프로세서 제조사는 속도 경쟁에 열을 올렸지만 2005년 즈음 전력 소모량과 발열 문제가 발생하면서 속도 경쟁의 한계에 부딪혔다. 그래서 듀얼 코어, 쿼드 코어 프로세스가 보편화됐고, 12, 16, 18 코어나 심지어 그보다 더 많은 코어를 장착한 프로세서가 등장하게 됐다.

CPU 뿐만 아니라 GPU라 부르는 그래픽 카드용 프로세서도 자세히 들여다보면 상당히 병렬화돼 있다. 요즘 나오는 고성능 그래픽 카드는 코어를 무려 4,000개 이상 장착하고 있으며 그 수는 계속 증가하고 있다. 이렇게 제작된 그래픽 카드는 단순히 게임용으로만 사용하지 않고, 수학 연산의 비중이 높은 작업을 처리하는데도 활용된다. 예컨대 이미지나 비디오 처리, 단백질 분석, 외계지적생명체 탐사 프로젝트에서 신호를 처리하는 작업 등에 활용된다.

C++ 98/03 버전은 멀티스레드 프로그래밍을 지원하지 않아서 서드파티 라이브러리나 타깃 시스템의 OS에서 제공하는 멀티스레드 API를 활용하는 수밖에 없었다. C++ 11부터 표준 멀티스레드 라이브러리가 추가되면서 크로스 플랫폼 멀티스레드 프로그램을 작성하기 한결 쉬워졌다. 현재 C++ 표준은 GPU를 제외한 CPU만을 대상으로 API를 정의하고 있지만, 향후 GPU도 지원하도록 개선될 가능성이 있다.

멀티스레드 프로그래밍이 필요한 이유는 크게 두 가지다. 첫쨰, 주어진 연산 작업을 작은 문제들로 나눠서 각각을 멀티프로세서 시스템에서 병렬로 실행하면 전반적인 성능을 크게 높일 수 있다.

둘째, 연산을 다른 관점에서 모듈화 할 수 있다. 예컨대 연산을 UI 스레드에 종속적이지 않은 독립 스레드로 분리해서 구현하면 처리 시간이 긴 연산을 백그라운드로 실행시키는 방식으로 UI의 응답 속도를 높일 수 있다.

아래 그림은 병렬 처리가 절대적으로 유리한 상황을 보여준다. 예컨대 이미지의 픽셀을 처리할 때 주변 픽셀 정보를 참조하지 않는 방식으로 구현한다고 하자. 그러면 이미지를 크게 네 부분으로 나눠서 처리하도록 알고리즘을 구성할 수 있다. 이러면 성능이 코어 수에 정비례 하게 된다.

https://drive.google.com/uc?id=14COJBKv_dgDqs6Iig_DHm3kcKwBdaQkt

항상 독립 작업으로 나눠서 병렬화 할 수 있는 것은 아니지만 최소한 일부분만이라도 병렬화 할 수 있다면 조금이라도 성능을 높일 수 있다. 멀티스레드 프로그래밍을 하는데 어려운 부분은 병렬 알고리즘을 고안하는 것이다. 처리할 작업의 성격에 따라 구현 방식이 크게 달라지기 때문이다.

또한 경쟁 상태, 교착 상태(데드락), 테어링(tearing), 잘못된 공유(false-sharing) 등과 같은 문제가 발생하지 않게 만드는 것도 쉽지 않다. 이런 문제는 주로 아토믹과 명싲거인 동기화 메커니즘으로 해결하며 구체적인 방법은 뒤에 소개하겠다.

Note) 멀티스레드 관련 문제를 방지하려면 여러 스레드가 공유 메모리를 동시에 읽거나 쓰지 않도록 디자인해야 한다. 아니면 동기화 기법이나 아토믹 연산을 적용한다.

경쟁 상태

여러 스레드가 공유 리소스를 동시에 접근할 때 경쟁 상태가 발생할 수 있다. 그중에서도 공유 메모리에 대한 경쟁 상태를 흔히 데이터 경쟁이라 부른다. 데이터 경쟁은 여러 스레드가 공유 메모리에 동시에 접근할 수 있는 상태에서 최소 하나의 스레드가 그 메모리에 데이터를 쓸 때 발생한다.

예컨대 공유 변수가 하나 있는데 어떤 스레드는 이 값을 증가시키고, 또 어떤 스레드는 이 값을 감소시키는 경우를 생각해보자. 값을 증가하거나 감소하려면 현재 값을 메모리에서 읽어서 증가나 감소 연산을 수행해야 한다.

PDP-11이나 VAX와 같은 예전 아키텍쳐는 아토믹하게 실행되는(주어진 시점에 혼자만 실행되는) INC와 같은 인스트럭션을 제공했다. 하지만 최신 x86 프로세서에서 제공하는 INC는 더는 아토믹하지 않다. 다시 말해 INC를 처리하는 도중에 다른 인스트럭션이 실행될 수 있기 때문에 결과가 얼마든지 달라질 수 있다.