강민철님의 "혼자 공부하는 컴퓨터 구조+운영체제"을 기반으로 학습한 게시물입니다.
📕 목차
1. Synchronization
2. Technique
1. Synchronization
📌 Concept
- Synchronization은 Multi-thread 환경에서 실행 순서와 자원의 일관성을 보장하기 위해 필요하다.
- 즉, Process(혹은 Thread) 사이의 작업 수행 시기를 맞추는 것을 말한다.
- 실행 순서 제어 : Process를 올바를 순서대로 실행한다.
- 상호 배제(mutual exclusion) : 동시에 접근하면 안 되는 자원에 하나의 Process만 접근하게 한다.
🟡 실행 순서 제어
- Writer와 Reader Process가 하나의 text 파일을 공유하는 경우, Write가 먼저 값을 저장해야 한다.
- Reader Process에게 "txt 안에 값이 존재한다"라는 사전 조건이 만족되어야 실행하도록 조건을 걸어야 한다.
🟡 상호 배제(Mutual Exclusion)
- 공유가 불가능한 자원의 동시 사용을 피하기 위한 알고리즘이다.
- 동기화를 하지 않으면 하나의 Process가 공유 자원을 사용하고 있을 때, 다른 Process가 자원의 상태를 바꾸어 심각한 오류를 야기할 수 있다.
📌 생산자 - 소비자 문제
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUMBER 10000
void *thread_increment(void *arg);
int num = 0;
int main(int argc, char **argv) {
pthread_t thread_id[10];
void *t_return;
for (int i=0; i<10; i++)
pthread_create(&thread_id[i], NULL, thread_increment, NULL);
for (int i=0; i<10; i++)
pthread_join(thread_id[i], &t_return);
printf("main 함수 종료, num = %d\n", num);
return 0;
}
void *thread_increment(void *arg) {
int i;
for (i=0; i<NUMBER; i++)
num++;
}
- 생산자(Producer)와 소비자(Consumer)
- 생산자 : 공유 자원에 값을 더하거나, 새로운 값을 추가한다.
- 소비자 : 공유 자원에 값을 빼거나, 기존 값을 제거한다.
- 동기화하지 않고 공유 자원을 수정하면 원하는 결과를 얻을 것이라 보장할 수 없다.
📌 공유 자원 & 임계 구역
- 공유 자원(Shared Resouce)
- 전역 변수, 파일, I/O Device, 보조 기억 장치 등
- 여러 Process가 사용할 수 있는 Resource를 말하낟.
- 임계 구역(Critical Section)
- 동시에 사용하면 문제가 발생하는 Resource에 접근하는 코드 영역
- 두 개 이상의 Process가 Critical Section에 접근하면, 둘 중 하나는 반드시 대기해야 한다.
⚔️ 경쟁 조건 (Race Condition)
- 두 개 이상의 Process가 Shared Resource를 병행적으로 읽거나 쓰는 경우
- Shared Resource 접근 순서에 따라 실행 결과가 달라질 수 있다.
- 저급 언어로 변환된 고급 언어 한 줄을 실행하는 과정에서 Context Switching이 발생하면 문제가 생긴다.
🛡️ 상호 배제를 위한 3원칙
- 상호 배제(mutual exclusion)
- 한 Process가 Critical Section에 진입했다면, 다른 Process는 Critical Section에 접근할 수 없다.
- 진행(progress)
- Critical Section에 어떤 Process도 진입하지 않았다면, Critical Section에 진입하고자 하는 Process는 들어갈 수 있어야 한다.
- 유한 대기(bounded waiting)
- 한 Process가 Critical Section에 진입하고 싶다면, 해당 Process는 언젠가는 들어갈 수 있어야 한다.
- 무한 대기 상태에 빠져서는 안 된다.
2. Technique
📌 뮤텍스 락(Mutex Lock)
- 자물쇠 관리
- 상호 배제를 통해 동시성 문제 해결 (순서를 보장하지는 않는다.)
- 기본 원칙
- Lock : Critical Section에 들어갈 때 Mutex를 잠근다.
- Unlock : Critical Section에서 나올 때 잠금을 해제한다.
- 구성 (하나의 전역 변수 + 두 개의 함수)
- 자물쇠 : Process들이 공유하는 전역 변수 lock
- Ciritical Section을 잠그는 역할
- acquire 함수
- Critical Section이 잠겨있다면 열릴 때까지 반복적으로 확인
- 열려 있다면 Critical Section에 진입하고 Lock을 실행
- Critical Section의 잠금을 해제하는 역할
- release 함수
- Critical Section에서 작업이 끝나고 Unlock을 실행
...
#include <pthread.h>
void *thread increment(void *arg);
char thread1[] = "A Thread";
char thread2[] = "B Thread";
pthread_mutext_t mutx;
int number = 0;
int main(int argc, char **argv) {
pthread_t t1, t2;
void *thread_result;
int state;
state = pthread_mutex_init(&mutx, NULL);
if (state) {
puts("뮤텍스 초기화 실패");
exit(1);
}
pthread_create(&1, NULL, thread_increment, &thread1);
pthread_create(&2, NULL, thread_increment, &thread2);
pthread_join(t1, &thread_result);
pthread_join(t2, &thread_result);
printf("최종 number : %d\n", number);
pthread_mutex_destroy(&mutx);
return 0;
}
void *thread_increment(void *arg) {
for (int i=0; i<5; i++) {
pthread_mutex_lock(&mutx);
sleep(1);
number++;
print("실행: %s, number: %d\n", (char*)arg, number);
pthread_mutex_unlock(&mutx);
}
}
🏃♂️ 바쁜 대기(busy wait)
while (lock == true) /* 임계 구역이 잠겨 있다면 */
; /* 임계 구역이 잠겨 있는지를 반복적으로 확인 */
가끔 이런 형태로 Unlock 상태를 쉴 새 없이 반복하며 확인해보는 코드가 있는데 이를 바쁜 대기라고 한다.
물론 CPU 자원을 계속 소모하므로 굉장히 좋지 않은 방법이다.
📌 세마포어(Semaphore)
- 자물쇠 pool을 관리
- 여러 Shared Resource 접근 권한 관리 (하나의 Shared Resource에는 하나의 Process만 들어갈 수 있을지라도)
- 이진 세마포어(binary semaphore) : 사실상 Mutex Lock과 비슷한 개념.
- 카운팅 세마포어(counting semaphore) : 여러 공유 자원의 개수만큼 pool 생성
- 기본 원칙
- 정수 값을 가진다.
- 0일 때 실행 불가, 1일 때 실행 가능
- 절대 음수값을 가질 수 없다.
- 구성 (하나의 전역 변수 + 두 개의 함수)
- 전역 변수 S : Critical Section에 접근 가능한 Process 개수 (사용 가능한 Shared Resource 개수)
- wait 함수 : Critical Section에 접근해도 괜찮은지, 아니면 기다려야 할지를 알려주는 함수
- signal 함수 : Critical Section 바로 앞에서 기다리는 Process에 '접근 허가' 신호 전달하는 함
(코드 다 치려고 했는데, 전에 공부한다고 옆에 써놓은 내용이 보기에 더 도움을 주길래 그냥 복붙했다.)
✒️ 바쁜 대기 → Ready Queue
- while을 사용한 바쁜 대기 방식은 CPU가 더 효율적인 일을 수행하는 것을 방해한다.
- Ready Queue를 사용해서 처리할 수 있다.
- 사용 가능한 Resource가 없을 경우 wait 함수가 실행되어 Process의 PCB를 Ready Queue에 넣는다.
- Shared Resource를 모두 사용한 Process가 signal 함수를 호출하면 대기 중인 Process를 깨운다.
- Semaphore를 이용해서도 관리할 수 있으며, Process 간의 실행 순서까지도 제어할 수 있다.
📌 모니터(Monitor)
- Semaphore만으로 Multi-Process와 Mutli-Thread 환경을 관리하기엔 휴먼 에러의 위험성이 너무 크다.
- Semaphore를 누락한 경우
- wait과 signal 순서를 헷갈린 경우
- wait과 signal을 중복 사용한 경우 등
- Monitor는 고급 언어 설계 구조물로서, 개발자의 코드를 Mutual Exclusion하도록 만든 추상화된 데이터 형태다.
- 즉, Shared Resource에 접근하기 위한 Interface를 묶어서 관리한다.
- Process는 반드시 해당 Interface를 통해서만 Shared Resource에 접근할 수 있다.
- Monitor 안에는 항상 하나의 Process만이 접근할 수 있도록 mutual exclusion을 위한 Synchronization을 제공한다.
- Shared Resource에 접근하기 위한 key 획득과 unlock 과정을 모두 알아서 처리한다.
- Java에서는 synchronized 메서드가 선언된 객체, synchronized 블럭에 의해 동기화 되는 객체에 고유한 monitor가 결합된다.
- 구성
- Thread 단위로 Monitor Lock 획득(acquire lock)하거나 반환(release lock)한다.
- 동기화 코드를 수행할 때는 synchronized instance와 결합된 monitor lock을 획득해야 진입 가능
🌱 조건 변수(Condition Variable)
- 실행 순서 제어를 위한 Synchronization
- wait과 signal 연산을 수행하여 임시로 Conditional variable queue에 Process를 삽입/제거한다.
- 상호 배제를 위한 큐 : Monitor에 진입하기 위해 삽입되는 큐 (Monitor 접근 전)
- 조건 변수에 대한 큐 : wait이 호출되어 실행이 중단된 Process가 삽입되는 큐 (Monitor 접근 후)
- 기본 원칙
- 특정 Process가 아직 실행 조건을 충적하지 못했다면 wait을 통해 중단한다.
- 특정 Process가 실행 조건을 충족했다면 signal을 통해 재개한다.
- 실행 중이던 Process가 특정 조건 변수에 대한 wait을 호출하면, 다른 Process가 signal을 보내줄 때까지 대기 상태에 들어간다.
- 상호 배제에 의해 Signal을 보낸 Process가 종료되거나, 해당 Process를 중단하고 수행을 재개해야 한다.