개요
- 운영체제 목적
- 운영체제 없이 프로그램 실행은 가능
- 자동 적재 및 멀티 태스킹 지원
- 프로세스 목적
- 프로그램 동시 실행하기 위해
- 프로세스는 상황정보 저장된 구조체임
- 운영체제는 이 상황정보를 통해 스레드를 효율적으로 할당
- 스레드 목적
- 가용 cpu를 최대한 이용하고자 프로세스 진입함수를 추가로 생성하여 스레드 활용
- 운영체제가 스레드 할당하며 프로세스 일시 중지 및 재시작 가능
- 코루틴 목적
- 동기 프로그래밍으로 비동기 가능하도록
- 코루틴 정의로 함수의 실행 일시 중지 및 재시작 가능
CPU
- cpu는 메모리에서 명령어를 가져오고 명령어를 실행하는 작업만 할 수 있음
운영체제
- 프로그램을 자동으로 적재, 멀티태스킹을 실현 해주는 역할을 함
- 운영체제가 없어도 cpu가 프로그램을 실행하도록 할 수 있지만 적절한 메모리 영역 찾기, pc 레지스터 설정 등등 매우 불편하다
- 내가 지금 사용하는 프로그램이 cpu와 표준 크기 메모리를 독점하고 있다고 생각할 수 있게 해줌 -> 가상 메모리
프로세스
- cpu가 어떤 기계 명령어를 실행했는지와 cpu내부 기타 레지스터 값 등 상태값을 저장해둔 구조체를 프로세스라고 한다.
- 이 정보로 프로그램을 일시 저장했다가도 프로그램 실행을 재개할 수 있어서 멀티 태스킹이 가능하다.
- 프로세스 주소 공간
- 운영체제의 가상 메모리는 각 프로세스가 표준적인 메모리 크기를 독점적으로 사용하는 것처럼 보이게 함
- 코드 영역
- 데이터 영역
- 힙 영역
- 스택 영역
- 다중 프로세스 프로그래밍 단점
- ex) afunc이 bfunc의 결과를 필요로 할 때
- 프로세스 생성 시 오버헤드
- 프로세스 마다의 자제척 주소 공간으로 프로세스 간 통신은 더 복잡함
- 프로세스 단점
- 진입 함수가 main 하나 밖에 없어서 프로세스의 기계명령어를 한 번에 하나의 cpu에서만 실행 가능
- cpu 여러대가 동일한 프로세스의 기계명령어를 실행하게 할 방법은??
- pc레지스터가 진입 함수로 main을 지정한 것과 동일한 방식으로 pc레지스터가 다른 함수를 지정하고 이를 통해 새로운 실행 흐름을 형성할 수 있음
- -> 스레드
- 이런 실행 흐름은 동일한 프로세스 주소 공간을 공유하므로 프로세스 간 통신이 불필요
스레드
- pc레지스터를 통해 하나의 프로세스에 두개 이상의 진입 함수를 설정하여 cpu 여러개가 동시에 프로세스의 기계 명령어를 실행할 수 있다
- == 공유 프로세스 주소 공간에서 동일한 프로세스에 속한 기계 명령어를 동시에 실행 가능
- == 하나의 프로세스 안에 여러 개의 실행 흐름 존재 가능
- 하나의 프로세스 내에서 이루어 지므로 프로세스 간 통신이 필요없음. 프로세스 내의 스레드들은 변수들을 공유 가능. 자신이 속해 있는 프로세스 주소 공간을 공유한다는 의미
- 스레드 덕분에 프로세스를 시작하고 스레드를 여러개 생성하여 모든 cpu를 최대한 사용하여 다중 코어를 충분히 이용할 수 있게 된다. -> 동시성 기초
다중 스레드
- 스레드는 운영체제 계층에 구현되어 코어 개수와 무관. cpu가 기계명령어를 실행할 때도 어느 스레드에 속하는지 인식 못함
- 주의점
- cpu가 명령어를 실행할 때 스레드 고려 않기 때문에 공유 리소스 접근 시 버그 발생 가능성이 있음
- 상호배제와 동기화 이용하여 개발자가 명시적으로 해결해야함
- 다중 스레드 메모리 구조
- cpu와 스레드는 진입함수 주소로 관련을 갖는것을 위에서 봄 -> 스레드와 메모리는??
- 스택 프레임
- 함수 실행 시 필요정보
- 매개변수, 지역변수, 반환주소 등
- 위 정보가 스택 프레임에 저장됨
- 스택 프레임의 증감이 프로세스 주소 공간에서 스택 영역을 형성함
- 각 실행 흐름(스레드)이 실행될 때 해당 정보를 저장하기 위해 스택영역이 여러개 필요
- 각 스레드는 프로세스 주소 공간에 자신만을 위한 스택영역을 갖고 있으며 이를 인지하고 있음
- 따라서 스레드를 생성하면 프로세스의 메모리 공간이 추가적으로 필요함 주의
- 수명 주기 관점에서의 긴 작업과 짧은 작업
- 긴 작업
- 워드 문서 편집
- 짧은 작업
- 웹 서버, 디비 서버, 파일 서버, 디비 서버 등
- 두가지 특징
- 작업 처리 필요 시간 짧음
- 작업 수가 매우 많음
- 요청 당 스레드 방식
- 서버가 하나의 요청을 받으면 해당 작업 처리하는 스레드 생성, 처리 완료되면 스레드 종료
- 짧은 작업엔 부적합
- 스레드 생성 종료 오버헤드
- 스레드마다 독립적인 스택영역이 필요하므로 메모리와 기타 시스템 리소스 많이 소비
- 스레드 전환 오버헤드
- 요청 당 스레드 방식 단점 해결 위해 스레드 풀이 탄생됨
스레드풀
- 스레드 재사용이 핵심
- 스레드 여러개를 미리 생성해두고 작업 생기면 해당 스레드에게 요청
- 스레드 생성 및 종료가 빈번하지 않으며 스레드 개수 관리되므로 메모리 안정적
- 스레드풀 내에 있는 스레드에게 작업 전달하기
- 큐 자료구조 (생산자 소비자 패턴)
- 스레드풀에 전달되는 작업은 구조체로서 데이터와 함수 부분으로 구성됨
- 생산자가 작업 대기열에 데이터를 기록하면 스레드가 깨어나고 작업 구조체를 가져와 구조체의 처리함수를 실행
- 작업 대기열에서 동기화 시 상호문제 처리 필요
- 스레드풀에서 적당한 스레드 개수
- cpu 집약 작업 vs i/o 집약 작업
- cpu 집약 작업은 외부 입출력 필요 없으므로 스레드 수와 cpu코어 수 같다면 cpu리소스를 충분히 활용 가능
- i/o 집약작업은 성능 테스트 도구로 입출력 대기시간과 cpu연산 시간 평가 필요 -> 실상황으로 테스트하여 대응
- 스레드간 공유되는 프로세스 리소스
- 프로세스와 스레드
- 운영체제가 제공하는 두가지 추상화 개념
- 프로세스는 운영체제가 리소스를 할당하는 기본 단위
- 스레드는 스케줄링 기본단위, 프로세스 리소스는 스레드간 공유됨
스레드 개별 전용 리소스 (스택영역)
- 상태변화 관점에서 스레드는 사실 함수 실행
- 함수 실행에는 항상 하나의 시작점 존재
- 이 시작점이 진입함수
- cpu는 진입 함수에서 실행 시작하여 실행 흐름 생성
- 이 진입 함수에서 시작된 실행흐름이 스레드
- 함수 실행에 필요한 정보는?
- 함수의 런타임 정보는 개별 스택 영역을 구성하는 스택 프레임에 각각 저장됨
- 반환값
- 매개변수
- 지역변수
- 레지스터 정보
- pc 레지스터
- 스택 포인터 등
- 스레드 여러개 있을 때 각 스레드가 독점하는 스택 영역들이 존재하게 됨
- 스레드 상황정보(컨텍스트)
- 스레드는 프로세스 주소 공간에서 위 스택영역을 제외한 나머지 영역 공유함
- 스레드 공유 리소스 vs 전용 리소스
- 위의 스택 영역을 제외하고 스레드 간 리소스를 공유한다
- 코드 영역, 데이터 영역, 힙 영역
- 스택 영역은 스레드 별 전용 리소스 공간
- 코드영역 : 모든 함수는 스레드에 배치될 수 있음
- 프로세스 주소 공간 중 코드영역에는 코드, 즉 컴파일한 후 생성된 실행가능한 기계명령어가 저장됨
- 실행 파일에 저장되어 있음
- 프로그램이 시작될 때 프로세스 주소 공간에 적재됨
- 스레드 모두가 공유
- 어떤 함수든 모두 스레드에 적재하여 실행될 수 있음
- 특정 함수를 특정 스레드에서만 실행되도록 하는 것은 불가
- 읽기전용
- 어떤 스레드도 코드 영역의 내용을 변경할 수 없음
- 따라서 스레드 안전 문제가 발생하지 않음
- 전역 변수가 저장되는 곳
- 프로세스 주소 공간 중 데이터 영역에 저장됨
- 전역 변수에 모든 스레드가 접근 가능
- 프로그램이 실행되는 동안 데이터 영역 내의 전역 변수의 인스턴스는 하나만 있기 때문
- malloc 함수나 new로 요청되는 메모리가 이 영역에 할당됨
- 모든 스레드는 해당 변수 주소, 즉 포인터를 얻을 수 있다면 데이터에 접근 가능
- 따라서 힙 영역은 스레드간 공유 리소스가 되는 것임
- 데이터 영역: 모든 스레드가 접근 가능
스택영역 : 공유 공간 내 전용 데이터
- 스레드의 추상화 측면에서 바라보면 스택 영역은 스레드 전용 공간
- 하지만 실제 구현 측면에서 엄밀하게 격리된 스레드 전용 공간은 아니다
- 서로 다른 프로세스의 주소 공간은 서로 격리되어 있음
- 가상 메모리 시스템은 특수힌 경우 제외하고 다른 프로세스의 주소 공간에 접근 하지 못하도록 보장
- 하지만 하나의 프로세스 내에서는 접근 가능
- 스레드 간 스택 영역에 접근 하지 못하도록 하는 보장은 없다.
- 다른 스레드의 스택 프레임의 포인터를 가져올 수 있다면 데이터에 접근 및 수정 가능
- 버그로 이어질 가능성 존재
- 다른 스레드의 전용 데이터를 수정하는 경우라면 버그의 원인을 찾기 굉장히 어려움
동적 링크 라이브러리와 파일
- 링크는 컴파일 후 최종적으로 실행 파일을 생성하는 핵심적인 단계
- 정적링크와 동적링크로 구분
- 정적링크는 종속된 모든 라이브러리가 실행 파일에 포함되는 것을 의미
- 동적링크는 포함되어 있지 않아 시작 또는 실행 중에 코드와 데이터를 찾아 프로세스 주소 공간에 넣는 링크 과정이 필요
- 동적 링크 중 코드와 데이터는 프로세스 주소 공간 어디에 놓여야 할까?
- 스택영역과 힙영역의 중간에 있는 여유 공간에 배치됨
- 모든 스레드가 공유 가능
- 프로그램이 동작 중에 특정 파일을 열면 프로세스 주소 공간에 열린 파일 정보도 저장됨
- 스레드간 공유 가능
스레드 전용 저장소(thread local storage)
- 이 영역에 저장된 변수는 모든 스레드에서 접근 가능
- 하지만 사실 변수의 인스턴스는 각각의 스레드에 속함
- 하나의 스레드에서 값을 변경해도 다른 스레드에서 적용되지 않음
__thread int a = 1;
- 컴파일러가 전역변수 a를 스레드 전용 저장소에 넣도록 지시하는 것
스레드 안전
- 스레드 안전을 이해하지 못하면 다중 스레드 프로그래밍을 다루기 어려움
- 전용 리소스만 사용하는 스레드는 스레드 안전을 달성할 수 있음
- 공유 리소스 사용하는 스레드는 대기 제약 조건에 맞게 리소스 사용하면 스레드 안전 달성
- 스레드 안전이란?
- 어떤 코드가 주어졌을 때 그 코드가 스레드 몇 개에서 호출되든 어떤 순서로 호출되든 올바른 결과가 나온다면 스레드 안전
- 스레드 안전 코드 작성하는 방법?
- 어떤 것이 스레드 전용 리소스고 어떤 것이 공유 리소스인지 구분할 수 있어야함
스레드 전용 리소스와 공유 리소스
- 스레드 전용 리소스
- 함수의 지역 변수
- 스레드의 스택 영역
- 스레드 전용 저장소
- 스레드 공유 리소스 : 순서가 중요
- 위의 리소스 외의 영역
- 힙 영역
- 데이터 영역
- 코드 영역
- 코드 영역은 읽기 전용이기 때문에 스레드 안전에서 고려하지 않아도 무방
- 스레드 안전을 위해 공유 리소스 사용 스레드는 순서를 따라야함
- 순서의 핵심은 공유 리소스를 사용하는 작업이 다른 스레드를 방해할 수 없다는 것
- 이를 위해 락이나 세마포어 같은 장치를 사용하여 공유 리소스 순서를 지킬 수 있음
- 무상태 함수
- 스레드 전용 리소스인 지역 변수만 사용하는 함수를 무상태 함수라 함
- 당연하게 스레드 안전
- 함수에 매개변수를 전달해야한다면??
- 함수 매개변수
- 매개변수를 값으로 전달하는 경우에는 스레드 안전
- 값으로 전달된 매개변수는 스택영역에 있기 때문
- 매개변수를 포인터로 전달하는 경우에는 스레드 안전하지 않을 수 있음
- 포인터로 전달된 변수가 지역 변수라면 스레드 안전
- 그 외는 락과 같은 순서가 반드시 부여되어야함
- 개선방법
- 스레드간 공유 리소스 사용하지 못하도록 함수 호출 시 해당 스레드에 속하는 리소스 주소를 전달하기
- 스레드가 공유 리소스 사용해야하는 상황이라면??
- 전역 변수 사용
- 전역 변수를 초기화 후 읽기 전용으로만 사용한다면 공유 리소스 사용하면서 스레드 안전
- 수정이 필요한 경우 스레드 전용 저장소 변수로 선언하면 스레드 안전
- 함수 반환값
- 값를 반환하는 경우
- 스레드 안전
- 포인터를 반환하는 경우
- 스레드 안전하지 않음
- funcA가 스레드 안전이 아닌 함수를 호출
- funcA도 기본적으로 스레드 안전하지 않지만
- 스레드 안전하지 않은 함수 호출 전후에 락으로 보호하면 funcA는 스레드 안전
- 스레드 안전 코드 구현 정리
- 스레드 간 어떤 리소스 공유하는지 고려하기
- 리소스 공유 필요한 경우라면
- 스레드 전용 저장소 활용
- 읽기 전용으로 사용
- 원자성 연산 방식으로 사용
- 동기화 시 상호 배제
- 순서 프로그래머가 유지
- 뮤텍스, 스핀락, 세마포어 등을 활용하서 순서 보장
- 커널 스레드 vs 코루틴
- 이번 장에서 언급한 스레드는 커널 스레드
- 스레드 생성, 스케줄링, 종료를 운영체제가 수행
- 개발자는 스레드에 관여 불가
- 운영체제에 의존하지 않고 직접 스레드 구현 가능??
- 코루틴
- 스레드보다 더 가벼운 실행 흐름
코루틴
- 일반함수는 return 명령어를 만나거나 코드의 마지막 줄까지 실행되어야 반환이 가능
- 일반 함수는 반환된 후 스택 영역에 정보 저장 하지 않음
- 코루틴은 스레드와 매우 유사한 기능인 일시 중지와 재개 기능이 존재
- 코루틴은 자신의 실행 상태 저장 가능
- 코루틴 반환된 후에도 이어서 계속 호출이 가능하며 마지막으로 일시중지된 지점에서 이어서 실행됨
- 반환된 후에도 함수 실행시의 정보 저장 필요
- 다시 실행될 때 해당 정보가 필요함
- 코루틴은 자신이 마지막으로 실행된 위치를 알 수 있다는 점에서 일반 함수와 다르다
- 전용 리소스만 사용하는 스레드는 스레드 안전을 달성할 수 있음
- 공유 리소스 사용하는 스레드는 대기 제약 조건에 맞게 리소스 사용하면 스레드 안전 달성
- 스레드 안전이란?
- 어떤 코드가 주어졌을 때 그 코드가 스레드 몇 개에서 호출되든 어떤 순서로 호출되든 올바른 결과가 나온다면 스레드 안전
- 스레드 안전 코드 작성하는 방법?
- 어떤 것이 스레드 전용 리소스고 어떤 것이 공유 리소스인지 구분할 수 있어야함
- 운영체제가 프로세스를 스케줄링 하는 것과 똑같은 구조
- 컴퓨터 시스템은 주기적으로 타이머 인터럽트를 생성하고 인터럽트가 처리될 때마다 운영체제는 현재 스레드의 일시 중지 여부를 결정
- 운영체제 덕분에 개발자는 스레드 일시중지나 재시작을 명시할 필요가 없음
- 진입 함수가 존재하면 운영체제가 자동적으로 여유 cpu를 가지고 스레드를 할당
- 하지만 이와 달리 코루틴은 개발자가 명시해줘야함!
- 운영체제는 코루틴 정보에 대해 알 수 없음
- 코루틴 vs 일반함수
- 일반 함수는 연결 시작 지점이 존재하지 않는 코루틴에 불과하다
- 코루틴은 어떻게 구현될까
- 스레드 구현과 동일한 구조
- 일시 중지되고 다시 시작할 수 있어야함
- 일시중지 될 때의 상태정보를 반드시 기록해야함
- 런타임 스택프레임 정보
- cpu 레지스터 정보
- 함수 실행시 상태 정보
- 스레드는 이를 프로세스 주소 공간의 스택영역에 저장함
- 코루틴은 런타임 스택 프레임 정보를 힙영역에 저장
- 메모리가 충분하다면 코루틴 개수에 제한 없으며 코루틴 간 전환도 가벼워 효율 좋음
- 코루틴 사용 목적
- 개발자가 동기 방식으로 비동기 프로그래밍을가능하게 함
'컴퓨터 과학 > [책] 컴퓨터 밑바닥의 비밀' 카테고리의 다른 글
컴퓨터 맨 밑바닥의 비밀 - 2.1 콜백함수, 동기화, 비동기화, 블로킹, 논블로킹 (0) | 2024.11.08 |
---|