들어가기 전에
학교 수업 때 배운걸 다시 정리해서 쓴 글입니다.
오타 및 잘못된 부분 지적은 언제나 환영입니다!
프로세스도 포함되는 내용들이 있지만, 이 글에서는 스레드를 기준으로 작성했습니다.
원자적 : 실패 또는 성공만 있고 일부만 성공은 허용하지 않음
스레드란?
프로세스 내부 상태를 가지고 독립적으로 실행 가능한 작업 단위
독립적으로 실행되며, 여러 루틴들을 호출할 수 있기 때문에 각 스레드마다 스택 주소 공간을 가지고있다.
이 부분에 대해선 사람마다 정의가 다르고 표현도 모호하기 때문에 여러 글을 참고하는걸 추천한다.
스레드와 프로세스의 차이?
cs 대비할 때 꼭 나오는 질문이다.
쉽게 말하면 스레드끼리는 힙을 공유하고, 프로세스끼리는 공유하지 않는다.
c 같은 경우 전역 변수나 이런 걸로도 스레드 간 공유가 되긴 하는데..
자바의 경우 모든 메모리가 힙으로 관리되니 모든 상황을 본다면 힙을 공유하는 게 더 정확하다고 생각한다.
스레드는 프로세스 내부에서 파생되기 때문에 프로세스의 자원을 공유하지만,
프로세스가 다른 프로세스의 자원을 공유하려 하면 os 단에서 막아버린다.
스레드가 필요한 상황
어떤 작업을 수행하는 과정에서 다른 작업들도 수행해야 할 때 쓰인다.
non-blocking 비동기 방식의 경우, 서버에서 사용자 유입이나 이탈이 발생할 때 백그라운드로 커널에 I/O 요청을 보내고 완료되는 동안, 원래 작업을 수행할 수 있게 한다.
즉, 백그라운드로 다른 작업들을 수행하고 싶을 때 쓰인다.
스레드의 흐름 제어
운영체제는 타이머 인터럽트나, 프로세스 우선순위 등으로 인해 프로세스의 실행 상태를 바꿀 수 있다.
이 과정에서 스레드가 작업을 다 마치지 못한 상태에서 다른 스레드가 작업을 시작하게 될 수 있다.
여러 스레드가 공유 자원에 접근하는 과정에서 결과값이 바뀌는 상황을 경쟁 상태(race condition)라 한다.
임계 구역(critical section)
이 경쟁상태를 유발하게 만드는 코드영역 다음 3가지를 통해 해결할 수 있다.
1. 상호 배제(Mutual exclusion)
한 프로세스가 임계 구역에 접근한 경우, 다른 프로세스는 여기에 접근할 수 없다.
2. 진행(Progress)
임게 구역에 존재하는 프로세스가 없을 때, 여기에 진입하려는 프로세스는 기다릴 필요 없이 진입한다.
3. 유한 대기(Bounded waiting)
프로세스가 임계 구역에 접근할 수 있는 횟수를 제한을 해, 임계 구역에 접근하려는
모든 프로세스들이 무한히 대기하지 않고 임계 구역에 접근할 수 있게 보장
즉, 다른 프로세스 때문에 계속 대기하는 상황이 있어선 안 된다.
락
임계 영억을 하나의 원자 단위 명령어로 묶음
프로세스는 락을 얻어야 임계 영역에 접근할 수 있다.
연산 과정의 원자성이 중요한 경우에 사용한다.
스핀 락
원시적인 형태의 락. while같은 기능을 통해 CPU의 사이클을 소모하며 락을 얻음.
유저 모드 단에서의 스핀 락은 오버헤드가 크기 때문에 하드웨어에서 명령어를 지원한다.
ex) intel xchg 명령어
상호 배제는 이루어지지만, while문을 회전 중인 스레드 중 계속 경쟁에 밀리는 상황 발생.
단일 CPU라면 성능 오버헤드가 굉장히 크기 때문에, CPU를 여러 개 두고 분산시켜야 한다.
컨디션 변수
공유 변수를 가지고 스핀 락 방식은 비효율적.
큐 자료 구조를 통해 조건을 만족하지 못하면 스레드는 대기 큐에 들어는 식으로 개선.
pthread의 join이 이 기능이라 보면 된다.
여러 스레드 간 작업 순서가 중요한 경우에 사용한다.
하지만, 부모 스레드는 조건 확인을 스핀 락으로 한다.
자식 스레드를 만들고 대기 상태로 가야 하는데, 자식 스레드가 바로 실행되고 종료된다 하자.
자식 스레드는 깨울 스레드가 없어 그대로 종료하고
부모 스레드는 wait를 호출하고 계속 대기상태가 되버린다.
pthread의 signal과 wait이 있는데
이는 각각 조건이 참이 되면 시그널을 보내 대기 큐에 있는 스레드를 깨우고
스레드를 스스로 잠재우기 위해 호출한다.
락을 가진 스레드는, signal을 보낸 뒤에야 락을 해제한다.
signal 호출하기 전, 인터럽트가 걸린 상태에서 다른 스레드가 락을 획득해버리면
대기 큐의 스레드는 다시 대기 상태로 돌아가기 때문
wait를 종료하기 전 mutext락을 획득해야 한다. 여기서 획득 못한 스레드는 다시 대기 큐에 간다.
종료 전 락을 획득해야 하는 이유는 락을 획득하는 과정도 하나의 레이스 컨디션이기 때문에,
인터럽트로 인해 다른 스레드가 진입하는 상황이 일어나면 안 된다.
컨디션 변수를 통해 기존 스핀 락에서 발생하는 과도한 오버헤드를 해결했지만, 시그널을 통한 대기 큐를 깨우는 과정에서 정책이 잘못됐다면, 공정성이 성립하지 않을 수 있다.
생산자 소비자 문제
컨디션 변수의 동기화 문제를 잘 보여준 예시
컨디션 변수만으로 해결하기 위해선, 변수를 더 추가하고, 작업이 끝날 때 어떤 대기 큐를 깨울 것인지 정해야 한다.
하지만, 단순히 변수를 추가한다는 식은 좋은 해결이 아니기 때문에, 새로운 해결책이 등장했다.
세마포어
세마포어 값을 통해 락과 컨디션 변수 모두 사용할 수 있는 기법
리눅스에서 세마포어와 관련된 라이브러리를 지원하며, wait와 post 함수를 통해 조절한다.
이거를 P(wait)와 V(post)라고 표현한다. 이 두 함수는 원자적이다.
init 함수의 3번째 세마포어 초기값을 정한다. (락의 개수라 보면 된다.)
wait: 세마포어 값을 1 감소시킨다. 값이 1 이상이면 임계 구역에 즉시 진입하고 리턴된다.
만약, 음수라면 대기 상태가 된다.
즉, 값이 음수면 그 값만큼 스레드가 대기하고, 양수면 임계 구역에 더 진입할 수 있단 뜻이다.
post: 값을 1 증가시킨다. 만약 현재 값이 음수라면 대기 중인 스레드를 깨운다.
원자성을 보장하기 때문에 임계 구역에 나가고 다른 스레드가 임계 구역에 들어간 뒤, 리턴된다.
이진 세마포어 : 초기값이 1, 세마포어를 락처럼 쓸 수 있다.
계수 세마포어 : 초기값만큼 임계 구역 허용 스레드 설정. 세마포어를 컨디션 변수로 쓸 수 있다.
read-write 락
다수의 스레드가 동시에 진입 가능 단, 이렇게 진입이 가능한 스레드는 읽기만 해야 한다.
read 스레드의 경우, 임계 구역 진입시 write 락도 얻음
-> 읽는 도중에 수정되면 안 되니까
마지막으로 진입한 read 스레드가 대기 중인 write 스레드들을 깨운다.
-> read 스레드가 수시로 들어오는 경우, wirte 스레드는 락을 획득할 수 없음
식사하는 철학자 문제
여러 스레드가 돌아갈 때 교착상태가 나는 과정을 보여줌
해결책: 각자 포크를 집는 방향을 동일하게 한다. 단, 한 철학자는 잡는 방향을 반대로 한다.
밑에서도 설명하겠지만, 해결법은 포크 획득 순서가 순환이되면 안 된다.
교착 상태(Dead lock)
두 개 이상의 스레드가 서로의 작업이 끝나기만을 기다리고 있는 상태
교착 상태의 발생 조건
1. 순환 대기 : 락의 요청 순서가 순환 구조를 가진다.
해결 : 전체 또는 부분적인 락 획득 순서를 정하는 식으로 순환을 깬다.
2. 점유 및 대기 : 락을 가지고 있는 채로 다른 스레드의 락 요구
해결 : 처음부터 모든 락을 원자적으로 획득 가능하게 함, 이렇게 할 경우 병행성이 저하됨
3. 비선점 : 작업을 끝낼 때 까지 락을 가져올 수 없다.
해결 : 커널 단에서 강제적으로 락을 가져오게 한다.
4. 상호 배제 : 프로세스들은 필요로 하는 자원에 자신만이 접근해야 함
해결 : 상호 배제가 되는 상황을 제거한다. 이렇게 되면 동시성 문제가 생겨버려
현실적으로 불가능한 해결
교착 상태 회피
스케줄링
어떤 스레드가 어떤 락을 획득하게 될 것인지에 대해 파악하고 있어야 함
데드락이 발생하지 않는 스레드끼리 묶어 개별 프로세서에서 실행
전체 작업 시간이 오래 걸리는 경우, 다른 프로세서들은 끝나기 까지 대기를 해야 해
성능이 제한된다.
무시
보통 락 순서에 대한 설계를 하고 개발을 진행하기 때문에 실제 데드락이 발생하는건 드묾
또한, 데드락 해결 비용이 무척 비싼 편이기에, 해당 프로세스를 죽이거나 재부팅으로 해결한다.
'학교 > os' 카테고리의 다른 글
파일 시스템 (0) | 2022.07.10 |
---|---|
주소 변환 (0) | 2022.07.10 |
문맥 교환(context switching) (0) | 2022.06.27 |
프로세스의 생성과 통신 (0) | 2022.06.26 |
프로세스 구조 (0) | 2022.06.26 |