예를 들어, 웹 서버는 클라이언트로부터 웹 페이지나 이미지, 소리 등에 대한 요청을 받는다.
하나의 웹 서버는 여러 개의 클라이언트들의 병행하게 접근할 수 있다. 만약 웹 서버가 단일 스레드 프로세스로 작동한다면, 자신의 단일 프로세스로 한 번에 하나의 클라이언트만 서비스할 수 있게 되어 클라이언트는 자신이 요청이 서비스되기까지 매우 긴 시간을 기다려야 할 것이다.
하지만 멀티 스레드 환경에서는 아래와 같은 구조로 요청-응답구조가 바뀐다.
요청이 들어오면 새로운 프로세스를 생성하지 않고, 요청을 서비스 할 스레드를 생성하고 추가적인 요청을 받기위한 작업을 재개한다.
응답성 실시간 애플리케이션을 멀티 스레드화 하면 애플리케이션의 일부분이 동기식 작업에 의해 멈추거나, 긴 작업을 수행하더라도 프로그램의 수행이 계속되는 것을 허용함으로써, 사용자에 대한 응답성을 증가시킨다.
자원 공유 프로세스는 공유 메모리와 메시지 전달 기법을 통해서만 자원을 공유할 수 있다. 이러한 기법은 명시적으로 개발자에 의해 처리되어야 한다. 그러나 스레드는 자동으로 그들이 속한 프로세스의 자원들과 메모리를 공유한다.
경제성 프로세스 생성에는 메모리와 자원의 할당이 필요하기 때문에 비용이 많이 든다. 하지만 스레드는 자신이 속한 프로세스의 자원을 공유하기 때문에 스레드에서의 context switch가 훨씬 경제적이다. 오버헤드의 차이를 경험적으로 측정하기는 어려울 수 있지만, 일반적으로 스레드 생성은 프로세스 생성보다 메모리를 덜 소비한다.
규모 적응성(확장성) 멀티 스레드의 이점은 멀티 코어 구조에서 더욱 증가한다. 멀티 코어 구조에서는 각각의 스레드가 다른 프로세서에서 병렬로 수행될 수 있기 때문이다. 단일 스레드 프로세스는 코어가 아무리 많아도 오직 하나의 코어에서만 실행된다.
태스크 인식 애플리케이션을 분석하여 독립된 병행 가능 태스크로 나눌 수 있는 영역을 찾는 작업이 필요하다. 태스크는 서로 독립적이므로, 개별 코어에서 병렬 실행될 수 있어야 한다.
균형 병렬로 실행될 수 있는 태스크를 찾아내는 것도 중요하지만 찾아진 부분들이 전체 작업에 균등한 기여도를 가지도록 태스크로 나누는 것도 매우 중요하다. 임의의 태스크를 나누고, 나눠진 태스크 일부의 기여도가 매우 적어진다면 별도의 코어를 사용하는 것은 그만한 가치가 없다.
데이터 분리 태스크가 나눠지면 태스크에서 접근하고 조작하는 데이터 또한 개별 코어에서 사용할 수 있도록 나누어져야 한다.
데이터 종속성 태스크가 접근하는 데이터는 둘 이상의 태스크 사이에 종속성이 없는지 검토되어야 한다. (자원 동시 접속 문제)
시험 및 디버깅 프로그램이 멀티 코어에서 실행될 때, 다양한 실행 경로가 존재할 수 있다. 따라서 프로그램의 시험 및 디버깅이 매우 어려워진다.
암달의 법칙(Amdahl's Law)
N개의 처리 코어를 가진 시스템에서 실행되는 애플리케이션 중에서 실제로 순차적으로 실행되어야만 하는 구성요소의 %를 S라고 하면 공식은 다음과 같이 표현된다.
예를 들어 75%의 병렬 실행 구성요소와 25% 순차실행 구성요소를 구진 애플리케이션이 있다. 이 애플리케이션을 2코어 시스템에서 실행시킬 경우, 약 1.6배의 속도향상을 얻을 수 있다.
모든 스레드의 동작을 관리하는 프로그램을 만드는 것은 어려운 일이다. 이러한 어려움을 극복하고 병행 및 병렬 프로그램의 설게를 도와주는 한 가지 방법은 스레딩의 생성과 관리 책임을 개발자로부터 컴파일러와 런타임 라이브러리에게 넘겨주는 것이다. 이를 암묵적 스레딩이라고 부르고 이 전략은 점점 널리 사용되고 있다.
웹 서버는 요청을 받을 때마다 그 요청을 위해 새로운 스레드를 만들어 준다. 새로운 스레드를 매 요청마다 만들어 주는 것은, 그때마다 새로운 프로세스를 만들어주는 것보다는 확실히 더 진보된 방법이지만, 다중 스레드 서버는 아직도 여러 문제를 가지고 있다.
첫 번째 문제는 서비스할 때마다 스레드를 생성하는 데 소요되는 시간이다. 특히 만들어진 스레드가 사용되는 시간이 짧을수록 비효율적일 것이다.
두 번째 문제는 동시에 실행할 수 있는 최대 스레드 수가 몇 개인지 한계를 정해야 한다.
이러한 문제들을 해결할 수 있는 방법 중 하나가 스레드 풀 방법이다.
기본 아이디어
프로세스를 시작할 때 아예 일정한 수의 스레드들을 미리 풀로 만들어두는 것이다. 이 스레드들은 평소에는 하는 일 없이 일감을 기다린다. 서버는 추가적인 스레드를 생성하지 않고 요청을 받으면 대신 스레드 풀에 제출하고 추가 요청 대기를 재개한다. 풀에 사용 가능한 스레드가 있으면 깨어나고 요청이 즉시 서비스된다. 풀에 사용 가능한 스레드가 없으면 사용 가능한 스레드가 생길 때까지 작업이 대기된다. 스레드가 서비스를 완료하면 풀로 돌아가서 더 많은 작업을 기다린다.
장점
새 스레드를 만들어 주기보다 기존 스레드로 서비스해 주는 것이 종종 더 빠르다.
스레드 풀은 임의 시각에 존재할 스레드 개수에 제한을 둔다. 이러한 제한은 많은 수의 스레드를 병렬 처리할 수 없는 시스템에 도움이 된다.
태스크를 생성하는 방법을 태스크로부터 분리하면 태스크를 실행을 다르게 할 수 있다. 예를 들어 태스크를 일정 시간 후에 실행되도록 스케줄 하더나 혹은 주기적으로 실행시킬 수 있다.
ETHREAD - 실행 스레드 블록 스레드가 속한 프로세스를 가리키는 포인터와 그 스레드가 실행을 시작해야 할 루틴의 주소등을 가지고 있다. 그리고 KTHREAD에 대한 포인터도 가지고 있다.
KTHREAD - 커널 스레드 블록 스레드의 스케줄링 및 동기화 정보를 가지고 있다. 그리고 스레드가 커널 모드에서 실행될 때 사용되는 커널 스택과 TEB에 대한 포인터를 가지고 있다.
TEB - 스레드 환경 블록 ETHREAD와 KTHREAD는 모두 커널 안에 존재한다. 이는 커널만이 읻르을 접근할 수 있다는 것을 의미한다. TEB는 사용자 모드에서 실행될 때 접근되는 사용자 공간 자료구조이다. TEB는 스레드 식별자, 사용자 모드 스택 및 스레드 국지 저장소(내부 로컬 저장소)를 저장하기 위한 배열을 가지고 있다.
부모와 자식 태스크가 자료구조를 얼마나 공유할지 결정하는 플래그의 집합이 전달된다. (위 그림에 있는 flag를 모두 전달받았다고 가정하자)
부모 태스크와 자식 태스크는 같은 파일 시스템 정보, 같은 메모리 공간, 같은 신호처리기, 같은 열린 파일의 집합을 공유하게 된다. - 이렇게 부모 태스크와 자식 태스크가 거의 모든 자원을 공유하기 때문에 이 챕터에서 본 스레드를 생성하는 것과 같은 결과가 된다.
만약 아무 flag 없이 clone() 이 호출되면 정보 공유는 일어나지 않게되고, fork() 시스템 콜이 제공하는 기능과 유사간 기능을 제공한다.
Linux 스레드의 특징
fork()를 호출하면 부모 프로세스의 관련 자료구조를 복사함으로써 새로운 태스크를 생성한다.(공유 X)
clone()은 전달되는 flag에 따라 부모 태스크의 자료구조를 가리킨다. clone() 시스템 콜의 융통성은 컨테이너 개념으로 확장될 수 있다. clone()을 통해 서로 격리된 환경인 컨테이너 환경을 생성하는 것을 가능하게 한다. 이후 Linux 컨테이너 관련 부분에서 자세히 다루겠다.
스레드는 CPU 사용의 기본 단위를 다나태며 동일한 프로세스에 속하는 스레드는 코드 및 데이터를 포함하여 많은 프로세스 자원을 공유한다.
다중 스레드 애플리케이션에는 1.응답성, 2.자원 공유, 3.경제성, 4.확장성 이라는 4가지 주요 이점이 있다.
여러 스레드가 진행 중인 경우 병행성이 존재하는 반면에, 여러 스레드가 동시에 진행 중인 경우 병렬성이 존재한다. 단일 CPU가 있는 시스템에서는 오로지 병행성만 가능하고, 병렬성은 여러 CPU를 제공하는 다중 코어 시스템이 필요하다.
다중 스레드 애플리케이션의 도전과제. 1.작업 분할 및 균형 조정 2.서로 다른 스레드 간에 데이터 분할 및 데이터 종속성 식별 3.테스트 및 디버깅의 어려움
데이터 병렬 처리는 동일한 데이터의 부분 집합을 다른 컴퓨팅 코어에 분산시키고, 각 코어에서 동일한 연산을 수행한다. 작업 병렬 처리는 여러 코어에 데이터가 아니라 작업을 분산시킨다. 각 작업은 고유한 연산을 실행한다.
사용자 응용 프로그램은 사용자 수준 스레드를 생성하며, 이 스레드는 궁극적으로 CPU에서 실행되도록 커널 스레드에 매핑되어야 한다. 다대일 모델은 많은 사용자 수준 스레드를 하나의 커널 스레드에 매핑한다.(전체 스레드 봉쇄의 위험성)
암묵적 스레딩에는 스레드가 아니라 작업을 식별하고 언어 또는 API 프레임워크가 스레드를 만들고 관리할 수 있게 한다. 스레드 풀, fork join 프레임워크 및 GCD(Grand Central Dispatch)를 포함하여 암묵적 스레딩에 대한 몇 가지 접근 방식이 있다. 암묵적 스레딩은 병행 및 병렬 프로그램을 개발할 때 점점 보편적인 기술이 되고 있다.
스레드는 비동기 또는 지연 취소를 사용하여 종료될 수 있다. 비동기 취소는 스레드가 업데이트를 수행하는 중이라도 스레드를 중지한다. 지연 취소는 스레드에 종료를 통지하지만 스레드는 질서있게 종료된다. 주로 지연 취소를 선호한다.
Linux는 프로세스와 스레드를 구분하지 않는 대신 각각을 태스크라고 한다. Linux clone() 시스템 콜을 사용하여 프로세스와 더 비슷하거나 스레드와 더 비슷한 태스크를 만들 수 있다.