Ch4. Threads and Concurrency
목표
- 스레드의 기본 구성, 프로세스와의 비교
- 멀티스레드 어플리케이션 설계의 이점과 첼린징
- 스레드 풀, fork-join, Grand Central Dispatch를 포함한 암시적 스레딩(implicit threading)의 다양한 접근 방식
- 리눅스 운영체제에서 스레드를 어떻게 사용하는지
왜 스레드를 사용하는가?
최근의 어플리케이션은 대부분이 멀티스레드로 동작한다. 스레드는 어플리케이션 안에서 동작한다. 멀티테스크는 어플리케이션에서 다음 스레드들을 이용해서 구현되었다.
- 화면 갱신
- 데이터 fetch
- 맞춤법 확인
- 네트워크 요청 확인 등 프로세스는 무거운 방면에 스레드는 비교적 가볍게 동작한다. 코드가 간단해지고, 효율성이 좋아진다. 커널은 일반적으로 멀티스레드이다.
그럼 스레드는 어떻게 생겼나?
여러 스레드는 같은 프로세스 안에서 실행된다. 이때, 각 스레드는 공뷰하는 부분과 독립적으로 가지고 있는 부분으로 나뉜다.
-
공유 자원
- code : 모든 스레드는 같은 코드를 실행함
- data : 같은 전역 변수나 static 변수
- heap : 동적할당
- files : 열려 있는 파일 디스크립터 등
- 독립 자원
- registers : 각 스레드 CPU 사용 시 자기만의 레지스터가 필요
- stack : 함수 호출 기록이 다름 (지역변수 등)
- PC : 각 스레드는 다른 위치에서 실행 가능함
멀티스레드 서버 아키텍처
장점 1. Responsiveness (응답성) : 한 부분이 block되어도 다른 스레드는 계속 실행할 수 있어서 사용자 인터페이에 우리 2. Resource Sharing (자원 공유) : 스레드는 같은 프로세스 내의 메모리와 자원을 공유하므로, 통신이 빠르고 효율적 3. Economy (경제성) : 스레드 생성은 프로세스 생성보다 비용이 훨씬 적음 4. Scalability (확장성) : 멀티코어 시스템에서 여러 스레드가 병렬로 동작해 성능 향상 가능
멀티코어 프로그래밍
- 멀티코어, 멀티프로세서 시스템 프로그래머한테는 여러 도전과제가 있음
- 작업 분할
- 균형
- 데이터 분할
- 데이터 의존
- 테스트와 디버깅
- 병렬성은 시스템이 동시에 여러 작업을 수행할 수 있음을 의미한다.
- Data parallelism : 각 코어가 같은 데이터의 일부를 나눠서 같은 연산한다.
- Task parallelism : 스레드를 여러 코어에 분산시키고, 각 스레드가 고유한 작업을 수행한다.
- 동시성은 여러 작업이 진행 중일 수 있도록 지원한다.
- 단일 프로세서/코어에서도, 스케줄러가 동시성을 제공할 수 있다
Amdahl’s Law
어플리케이션에서 병렬화되지 않는 부분이 전체 성능 향상에 큰 제약을 건다.
S : 전체 작업 중 직렬 부분 비율 N : 사용하는 코어 개수
한계점
코어 수 N이 무한대로 많아져도 속도는 1 / S 이상 올라갈 수 없음
User Threads 와 Kernel Threads
- User Threads : user level에서 관리됨 (운영체제가 아닌 사용자 코드 or 라이브러리에서)
- POSIX Phreads
- windows threads
- Java threads
- Kernel Threads : 운영체제 커널에서 직접 지원 및 관리
- 모든 주요 OS가 커널 스레드 사용함
- Windows
- Linux
- macOS
- iOS
- Android
- 모든 주요 OS가 커널 스레드 사용함
Multithreading Models
- Many-to-One
- 많은 유저레벨의 스레드들이 하나의 커널 스레드로 맵핑된다.
- 장점 : 원하는 만큼 사용자 스레드를 생성할 수 있음
- 단점
- 하나의 스레드가 블로킹되면, 전체 사용자 스레드가 멈춤
- 멀티코어 시스템이라도, 커널 스레드가 하나뿐이라 병렬 실행이 안됨
- 단점이 너무 강하기 때문에 요새는 안
- One-to-One
- 각 유저레베의 스레드가 1:1로 커널 스레드에 맵핑된다.
- 장점 :
- 더 병행적이다.
- 스레드가 블로킹되어도 다른 스레드를 실행할 수 있음.
- 여러개의 스레드를 다중 코어에 매핑할 수 있음
- 단점 :
- 사용자 스레드와 커널 스레드가 1:1이기 때문에 사용자 스레드를 무한정 생성 할 수 없음
- 현재 대부분의 운영체제가 사용하는 방심임
- Many-to-Many
- 사용자 스레드가 여러개의 커널 스레드에 매핑 가능
- 장점:
- 원하는 만큼 사용자 스레드 생성 가능
- 멀티코어에서 병령 실행 가능
- 유연한 매핑 구조 덕분에 효율적인 스케줄링 가능
- 단점:
- 구현이 복잡하고 어렵다
- 멀티플렉싱하는 부분에서 병목현상이 있을 수 있음
- 잘 사용하지 않음
- Two-level
- M:M과 1:1 방식을 섞어 놓은 형태
- 대부분의 사용자 스레드는 커널 스레드와 다대다(M:N) 관계로 multiplexing됨
- 그런데 특정 사용자 스레드 일부는 커널 스레드에 1:1로 고정해서 실행할 수도 있음
- 즉, M:N과 1:1 모델이 혼합된 구조
- 근데 구현이 복잡해서 잘 안씀
Thread Libraries
프로그래머가 스레드를 만들고 관리할 수 있게 해주는 API를 제공하는 도구 구현방식
- 유저 공간에서만 동작하는 라이브러리
- 커널 개입 X
- 성능 빠름
- 커널이 직접 지원하는 커널 레벨 라이브러리
- 스레드 생성/종료 시 시스켐 콜 발생 -> 커널에 알려야 함
- OS가 직접 스케줄리함 -> 병렬 실행 가능 동기 / 비동기 실행 차이 - Asynchronous threading : 부모와 자식 스레드가 독립적으로 실행 (각자 자기 일함) - Synchronous threading : 부모 스레드는 모든 자식 스레드가 끝날 때가지 기다려
라이브러리 | 설명 |
---|---|
POSIX Pthreads | 유저레벨 혹은 커널 레벨 (OS 따라) |
Windows threads | 커널 레벨 스레드 사용 |
Java threads | OS에 따라 다름 (window에서는 커널) |
Pthreads
사용자 레벨과 커널 레벨을 제공 스레드를 생성하고 비동기화하는 POSIX 표준 API 이다.
- pthread_create : 스레드 생성함수
- pthread_join : 스레드 종료 대기 함수
- pthread_cancle
- pthread_exit
- pthread_kill : 시그널 생성 함수
Implicit Threading
쓰레드를 직접 생성해서 관리하는거는 굉장히 번거롭다. 개발자는 병렬로 수행할 수 있는 task를 식별하는데 주력하고, 스레드는 컴파일러나 런타임 라이브러리가 담당한다. 5가지 방법이 있다.
- Thread Pools
- 할 일을 대기하는 pool에 스레드를 생성한다.
- 장점
- 새롭게 스레드를 생성하는거보다 이미 존재하는 스레드에 요청하기 때문에 빠르다.
- pool의 크기로 어플리케이션이 사용할 스레드의 수를 정할 수 있다.
- task 실행과 생성을 분리하여 사용한다.
- Fork-Join (OpenMp 에서 사용함)
- 작업은 나누고 다시 모으는 구조가 명확해서 관리가 쉬움
- 재귀적 문제에 많이 사용
- OpenMP
- 컴파일러 지시문을 사용
- #pragma omp parallel : 코어 수 만큼 fork
- #pragma omp parallel for : 코어의 수 만큼 스레드 생성하고 N개의 iteration을 스레드에 분할 매핑
- Grand Central Dispatch (Apple’s thread pool)
- Thread Pool의 크기를 자동 조절 하부는 POSIX pthread로 구현
- 코드의 어느 부분을 병렬로 실행시킬 수 있는지 개발자가 지정할 수 있다.
- GCD는 스레딩의 세세한 것을 관리
- Intel Threading Building Blocks (C++ template library)
질문??
- fork() 할 떄, threads 누글 복제할까?
- 현재 호출한 스레드만 복제할까?
- 프로세스 안에 있는 모든 스레드를 다 복제할까? -> 어떤 UNIX 시스템은 fork()의 두가지 버전을 제공하기도 한다. 근데 리눅스는 기본적으로 현재 호출한 스레드만 복제하는 방식 -> 바로 exec()를 해야 한전하게 프로그램을 교체할 수 있다. -> 근데 멀티 스레딩할때는 가급적이면 fork()를 사용하지 마라
Signal Handling
Signal은 Unix system에서 프로세스에 특정 이벤트가 발생해싿고 알려줄때 사용된다. Signal 처리하는 과정
- 특정 이벤트로 발생하여 생성된 signal
- 프로세스로에 전달된 signal
- Signal handling
- Default handler
- User-defined handler
Thread Cancellation
thread가 완전히 끝나기도 전에 강제로 종료시키는 것 - Asynchronous canellation -> 즉시 강제 종료해버림, 근데 자원 모두 해제 하지 못하고 메모리 누수 문제가 생길 수 있음 - Deferred cancellation -> 스레드가 주기적으로 자신이 취소인지 확 -> Cancellation point에서 스레드를 캔슬함. -> 일반적으로 read()와 같은 blocking system call이 해당 point가
Thread-Local Stroage (TLS)
각 스레드가 자기만의 복사본을 가지게 해주는 저장 공간
- 언제 유용함?
- 내가 스레드 생성 자체를 직접 제어할 수 없을 때
- 스레드 풀을 사용할 떄 유용
- TLS는 일반 지역 변수와 다름
- TLS는 지역 변수와 다르게 함수가 끝나도 유지됨
- 스레드별로 따로 존재함. 지역변수는 호출 스택에 따라 다