현대 컴퓨터 시스템에서는 여러 프로세스(혹은 스레드)가 동시에 실행되며 서로 영향을 주고받습니다. 이때 자원의 일관성을 보장하기 위해 프로세스의 실행 시기를 조절하는 것을 동기화라고 합니다. 이번 글에서는 동기화의 필요성과 다양한 동기화 기법들에 대해 자세히 알아보겠습니다.
동기화란 무엇인가?
동기화란 동시에 실행되는 프로세스들이 자원을 공유하는 과정에서, 자원의 일관성을 보장하기 위해 프로세스들의 수행 순서를 조정하는 것입니다. 예를 들어 여러 프로세스가 동시에 하나의 파일에 접근하려고 한다면, 동기화가 제대로 이루어지지 않을 경우 데이터의 무결성이 깨질 수 있습니다.
실행 순서 제어의 중요성
동기화는 프로세스의 올바른 실행 순서를 보장하는 데 필수적입니다. 예를 들어, 하나의 파일에 데이터를 쓰는 프로세스(Writer)와 그 데이터를 읽는 프로세스(Reader)가 있을 때, Writer가 데이터를 완전히 쓰기 전에 Reader가 이를 읽으려고 한다면 문제가 발생할 수 있습니다. 따라서 Reader와 Writer의 실행 순서를 조절하는 것이 중요합니다.
- Writer : .txt 파일에 값을 저장하는 프로세스
- Reader : .txt 파일에 저장된 값을 읽어들이는 프로세스
공유 자원과 임계 구역
공유 자원
공유 자원은 여러 프로세스 혹은 스레드가 동시에 접근할 수 있는 자원입니다. 전역 변수, 파일, 입출력 장치 등이 그 예시입니다. 이러한 공유 자원에 동시에 접근하면 예기치 않은 결과가 발생할 수 있기 때문에 동기화가 필요합니다.
임계 구역
임계 구역은 동시에 실행되면 문제가 발생할 수 있는 코드 영역입니다. 예를 들어, 은행 계좌에 접근하는 두 개의 프로세스가 있다고 가정해 봅시다. 하나는 잔액에 2만 원을 추가하고, 다른 하나는 5만 원을 추가하는 작업을 합니다. 만약 이 두 프로세스가 동시에 실행된다면 잔액이 제대로 반영되지 않는 경쟁 상태(Race Condition)이 발생할 수 있습니다. 따라서 임계 구역에 접근하는 프로세스는 반드시 상호 배제 원칙을 따라야 합니다.
임계 구역 문제 해결의 세 가지 원칙
- 상호 배제: 한 프로세스가 임계 구역에 진입했다면 다른 프로세스는 진입할 수 없습니다.
- 진행: 임계 구역에 어떤 프로세스도 진입하지 않았다면, 진입하고자 하는 프로세스는 들어올 수 있어야 합니다.
- 한정 대기: 프로세스는 임계 구역에 들어가기 위해 영원히 기다릴 필요가 없습니다. 다른 프로세스가 앞서 진행할 수 있는 횟수에는 제한이 있습니다.
동기화 도구
뮤텍스 락
뮤텍스(Mutex) 락은 상호 배제를 위해 사용되는 동기화 도구로, 임계 구역을 보호하기 위해 자물쇠 역할을 합니다. 한 프로세스가 임계 구역에 진입할 때 acquire() 함수를 호출하여 락을 걸고, 작업이 끝나면 release() 함수를 호출하여 락을 해제합니다.
이러한 방식으로 한 번에 하나의 프로세스만 임계 구역에 진입할 수 있도록 보장합니다. 뮤텍스를 사용할 때 발생할 수 있는 문제점 중 하나는 바쁜 대기(busy waiting)입니다. 임계 구역이 열릴 때까지 반복적으로 확인하는 방식은 CPU 자원을 낭비하기 때문에, 효율적인 동기화 방법이 아닙니다.
뮤텍스 Python Code
import threading
import time
# 공유 자원
counter = 0
# 뮤텍스 락 생성
lock = threading.Lock()
def increment_counter(thread_name):
global counter
for i in range(5):
# 락 획득 전 대기
print(f"{thread_name}: 증가 시도 중...")
# 락 획득
lock.acquire()
try:
current_value = counter
print(f"{thread_name}: 락 획득. 현재 counter: {current_value}")
# 의도적인 지연 추가
time.sleep(1)
counter += 1
print(f"{thread_name}: counter 증가. 새로운 값: {counter}")
finally:
# 락 해제
lock.release()
print(f"{thread_name}: 락 해제됨")
# 락 해제 후 잠시 대기
time.sleep(1)
def decrement_counter(thread_name):
global counter
for i in range(5):
# 락 획득 전 대기
print(f"{thread_name}: 감소 시도 중...")
# with 문을 사용한 락 관리
with lock:
current_value = counter
print(f"{thread_name}: 락 획득. 현재 counter: {current_value}")
# 의도적인 지연 추가
time.sleep(1)
counter -= 1
print(f"{thread_name}: counter 감소. 새로운 값: {counter}")
# 락 해제 후 잠시 대기
time.sleep(1)
def main():
# 스레드 생성
thread1 = threading.Thread(target=increment_counter, args=("증가 스레드",))
thread2 = threading.Thread(target=decrement_counter, args=("감소 스레드",))
# 스레드 시작
thread1.start()
thread2.start()
# 스레드 종료 대기
thread1.join()
thread2.join()
print(f"최종 counter 값: {counter}")
if __name__ == "__main__":
main()
세마포어
세마포어(Semaphore)는 뮤텍스를 일반화한 동기화 도구로, 여러 프로세스가 동시에 자원에 접근할 수 있는 경우에도 사용할 수 있습니다. 세마포어는 임계 구역 앞에서 멈춤 신호를 받고 기다리거나, 가도 좋다는 신호를 받고 진입할 수 있는 구조로 되어 있습니다. wait() 함수와 signal() 함수로 임계 구역의 접근을 제어하며, 이를 통해 효율적인 동기화를 구현할 수 있습니다.
세마포어의 경우에도 바쁜 대기 문제가 발생할 수 있지만, 이를 해결하기 위해 사용할 수 없는 자원이 있을 때 해당 프로세스를 대기 상태로 만들고, 자원이 생겼을 때 다시 준비 상태로 전환하는 방식으로 CPU 자원을 절약할 수 있습니다.
세마포어 Python Code
import threading
import time
# 제한된 자원 풀 (예: 최대 2개의 동시 접근 허용)
semaphore = threading.Semaphore(2)
def access_resource(thread_name):
print(f"{thread_name}: 자원 접근 시도")
# 세마포어 획득 시도
semaphore.acquire()
try:
print(f"{thread_name}: 자원 접근 성공")
# 자원 사용 시뮬레이션
time.sleep(1)
finally:
# 세마포어 해제
semaphore.release()
print(f"{thread_name}: 자원 해제")
def main():
# 여러 스레드 생성
threads = []
for i in range(5):
thread = threading.Thread(target=access_resource, args=(f"스레드-{i+1}",))
threads.append(thread)
thread.start()
# 모든 스레드 종료 대기
for thread in threads:
thread.join()
print("모든 작업 완료")
if __name__ == "__main__":
main()
'운영체제' 카테고리의 다른 글
CPU 스케줄링이란? (0) | 2024.11.10 |
---|---|
스레드와 멀티 프로세스(Multi Process), 멀티 스레드(Multi Thread) (10) | 2024.11.06 |
프로세스(Process)란? (0) | 2024.11.03 |
[OS] 운영체제란 무엇인가? (0) | 2024.06.09 |