안녕하딤니카?
이번 글에서는 프로세스의 동기화에 대해 정리해 보고, 동기화 기법에는 어떤 것이 있는지 함께 쭉 정리해 보겠습니다.
날씨가 많이많이 춥네요 모두 멸종되지 않게 조심하세요
그럼 시~~~작

1. 동기화
동기화에 대해 정리하기 전에, 저번 글에서 쭉 정리했던 프로세스, 스레드의 개념을 종합적으로 정리하면서 동기화에 대해 정리해 보겠습니다.
동시다발적으로 실행되는 프로세스, 스레드들은 서료 협력하며 영향을 주고받습니다. 이 과정에서 자원의 일관성을 보장해야 합니다.
(동기화의 대상은 프로세스, 스레드를 모두 포함하고 있습니다. 아래부터 정리하는 내용에는 제가 프로세스라고만 작성할 텐데, 이 안에는 스레드도 포함되어 있다~라고 생각해 주시면 될 것 같습니다.)
아무튼 자원의 일관성을 유지하기 위해서 프로세스들은 동기화되어야 합니다. 사전적으로 동기화의 의미는 프로세스들의 수행 시기를 맞추는 것입니다. 근데 수행 시기를 맞춘다...라고만 설명하기에는 뭔가 와닿지 않는 느낌이라 동기화의 의미에 대해서 다시 설명해 보자면,
- 1. 실행 순서 제어를 위한 동기화 : 실행되고 있는 프로세스들을 올바른 순서대로 실행하기
- 2. 상호 배제를 위한 동기화 : 동시에 접근해서는 안 되는 자원에 하나의 프로세스만 접근하게 하기
라고 설명할 수 있습니다.
동기화의 대상은 실행의 문맥을 갖는 모든 대상입니다. 그래서 제가 위에서 짤막하게 썼던, 프로세스와 스레드 모두 동기화의 대상이라고 했던 것이 이 실행 문맥을 갖는 대상이기 때문입니다. 근데 편의상 프로세스라고만 할게요!
아래에서 이제 동기화의 두 가지의 의미별로 예시를 들어서 설명해 보도록 하겠습니다.
1-1. 실행 순서 제어를 위한 동기화
실행 순서 제어를 위한 동기화를 설명해 보겠습니다. 먼저 아래 예시 이미지를 참고 부탁드립니다. (Reader Writer Problem)

Book.txt 라는 파일이 있고, Writer 프로세스는 Book.txt 파일에 값을 저장하는 프로세스고, Reader 프로세스는 Book.txt 파일에 저장된 값을 읽어 들이는 프로세스라고 가정해 보겠습니다.
이 두 가지 프로세스가 동시에 실행된다라고 할 때, 이 두 프로세스가 아무렇게나 막 실행되면 안 되겠죠? 실행 순서가 있기 때문에 순서에 맞춰서 실행되어야 합니다. 왜냐면 Reader 프로세스는 'Book.txt 안에 값이 존재한다' 라는 특정 조건이 만족되어야만 실행 가능하기 때문입니다. 두 프로세스가 동시에 실행된다 하더라도, Reader 프로세스는 선행 조건이 있기 때문에 Writer 프로세스가 먼저 실행되고, 그다음에 Reader 프로세스가 실행되어야 합니다.
이렇게 두 개 이상 프로세스가 동시에 실행될 때, 실행 순서를 제어해서 무분별하게 아무렇게나 프로세스들이 실행되지 않도록 제어하는 것이 동기화의 의미다 라고 설명할 수 있습니다.
1-2. 상호 배제를 위한 동기화
상호 배제를 위한 동기화에 대해서도 예시를 들어서 설명해 보겠습니다. (Bank account ploblem)

현재 계좌에 잔액에 10만 원 있다고 가정해 보겠습니다. 프로세스 A는 현재 잔액에 2만 원을 추가하는 프로세스고, 프로세스 B는 현재 잔액에 5만 원을 추가하는 프로세스입니다. 위 순서대로 각 프로세스는 위의 순서대로 실행됩니다.
얼핏 들어서 동기화 없이 A와 B가 동시에 실행되면 계좌 잔액에는 17만 원이 남는 것이 아닐까? 싶겠지만... 아닙니다.

동기화 없이 더한 값이 저장되기 전에 문맥 교환이 이루어진다면 2만 원은 더했지만 저장되지 않은 상태이니 다른 프로세스에서 그 더한 값까지 저장된 잔액이 반영되지 않는 것입니다. 다시 정리하자면, 프로세스 A와 B는 잔액 이라고 하는 데이터를 동시에 사용하는데, 프로세스 실행이 프로세스 B가 잔액이라고 하는 데이터에 접근했기 때문에 이런 예상치 못한 문제가 발생하는 것입니다.
이 문제 발생을 방지하기 위해서는 한 프로세스가 하나의 자원에 접근하고 있을 때는 다른 프로세스는 그 프로세스의 과정이 끝날 때까지 접근을 기다려야 합니다. 이것을 상호 배제라고 하며, 동기화는 이렇게 한 번에 하나의 프로세스만 접근해야 하는 자원에 동시 접근을 피하기 위해서 필요합니다.
한 가지 예제(Producer & Consumer problem, 생산자 소비자 문제)가 더 있는데, 이것도 Bank account ploblem 예제와 유사합니다. 이번 글에서는 이 예제는 생략하도록 할게요!
2. 공유 자원과 임계 구역
아무튼 이렇게 동기화의 의미를 두 가지로 나눠서 설명했는데, 동기화에는 이해해야 하는 두 가지의 개념이 있습니다. 바로 공유 자원과 임계 구역 개념인데, 아래에 정리해 보겠습니다.
- 공유 자원 : 여러 프로세스 혹은 스레드가 공유하는 자원 (전역 변수, 파일, 입출력장치, 보조기억장치 등등...)
- 임계 구역 : 공유 자원에 접근하는 코드 중 동시에 실행하면 문제가 발생하는 코드 영역 (앞선 예시의 '잔액' 변수...)

임계 구역에 진입하고자 하면 진입한 프로세스 이외에는 대기해야 합니다. 위의 그림을 설명하면, 프로세스 A가 임계 구역에 들어가면 프로세스 B는 임계 구역에 들어가고 싶어도 A가 임계 구역에서 나오기 전까지 대기해야 하고, 프로세스 A가 임계 구역에서 나오면 그때 프로세스 B가 임계 구역에 들어갈 수 있습니다.
임계 구역에 프로세스들이 동시에 접근하면 자원의 일관성이 깨질 수 있습니다. 이를 레이스 컨디션(race condition) 이라고 합니다. 앞서 설명했던 Bank account problem, 설명을 생략했던 Producer & Consumer problem 모두 레이스 컨디션에 대한 사례입니다.
운영체제가 임계구역 문제를 해결하는 세 가지 원칙이 있습니다. (상호 배제를 위한 동기화를 위한 세 가지 원칙)
- 상호 배제 (mutual exclusion) : 한 프로세스가 임계 구역에 진입했다면 다른 프로세스는 들어올 수 없다.
- 진행 (progress) : 임계 구역에 어떤 프로세스도 진입하지 않았다면 진입하고자 하는 프로세스는 들어갈 수 있어야 한다.
- 유한 대기 (bounded waiting) : 한 프로세스가 임계 구역에 진입하고 싶다면 언젠가는 임계 구역에 들어올 수 있어야 한다. (임계 구역에 들어오기 위해 무한정 대기해서는 안 된다.)
이 세 가지 원칙은 상호 배제를 위한 동기화를 이해하는데 가장 중요하니 꼭 기억해 두도록 합시다...
3. 동기화 기법
위에서 동기화에 대해서 정리했다면, 이번에는 동기화 기법에 대해서 정리해 보겠습니다. 이번 글에서는 동기화 기법 중 뮤텍스 락, 세마포어, 모니터 이렇게 세 가지 기법을 정리합니다!
3-1. 상호 배제를 위한 동기화 도구 - 뮤텍스 락 (Mutex)
뮤텍스 락은 상호 배제를 위한 동기화 도구(자물쇠 역할) 입니다. 임계 구역을 탈의실이라고 한다면, 뮤텍스 락은 탈의실에 한 명이 들어가면 누가 못 들어오게 문을 잠그거나 커튼을 치잖아요? 그 잠금장치나 커텐 같은 역할을 하는 것이 뮤텍스 락입니다.

뮤텍스 락은 단순한 형태로 구현할 수 있습니다. 전역 변수 하나, 함수 두 개로 구현하는데, 아래에 정리해 보겠습니다. (실제로 구현하는 방식은 이렇게 간단하지 않으나, 수도코드라고 생각해 주시면 됩니다.)
- 자물쇠 역할 : 프로세스들이 공유하는 전역 변수 lock
- 임계 구역을 잠그는 역할 : acquire 함수
- 임계 구역의 잠금을 해제하는 역할 : release 함수

그림에서 등장하는 acquire 함수와 release 함수를 풀어서 설명하자면, 아래처럼 각 함수가 실행됩니다.
- acquire 함수 : 프로세스가 임계 구역에 진입하기 전에 호출, 임계 구역이 잠겨 있다면 임계 구역이 열릴 때까지(lock이 false가 될 때까지) 임계 구역을 반복적으로 확인, 임계 구역이 열려 있다면 임계 구역을 잠그기(lock을 true로 바꾸기)
- release 함수 : 임계 구역에서의 작업이 끝나고 호출, 현재 잠긴 임계 구역을 열기(lock을 false로 바꾸기)
위에서 상호 배제 사례를 해결하기 위해서 프로세스 시작과 끝 위에다가 acquire 함수와 release 함수를 끼워주면 됩니다.
뮤텍스 락의 특징과 장단점을 설명해 보겠습니다. 먼저 단점, 그니까 고려해야 할 사항이라면, 바쁜 대기(busy waiting) 이 있습니다. acquire 함수 내에서 while문을 반복하는 과정에서 발생하는 건데, 이거를 탈의실에 다시 비유한다면 탈의실이 잠겨있는지 탈의실 문을 쉴 새 없이 덜컹덜컹거리는거라고 보면 됩니다. 그래서 이 방법은 썩 좋은 방식은 아닙니다...
3-2. 상호 배제, 실행 순서 제어를 위한 동기화 도구 - 세마포어 (Semaphore)
(여기서는 세마포어의 종류 이진 세마포어, 카운팅 세마포어 중에서 카운팅 세마포어에 대해서 다룹니다.)
위에서 뮤텍스 락은 탈의실 문을 쉴새없이 덜컹거리는 거와 같이 동기화를 하는 거라면, 세마포어는 뮤텍스 락보다는 좀 더 일반화된 방식의 동기화 도구입니다. 뮤텍스 락이 단일 공유 자원에 적용 가능한 동기화 도구라면, 세마포어는 공유 자원이 여러 개 있는 경우에도 적용이 가능합니다.

세마포어는 철도 신호기에서 유래한 단어입니다. 철도에 기차가 지나다닐 때, 멈춰야 하는 구역에서 신호를 받고, 가도 좋다는 신호를 받으면 다시 지나가잖아요? 그러처럼 세마포어는 방식은 임계 구역 앞에서 멈춤 신호를 받으면 잠시 기다리고, 임계 구역 앞에서 가도 좋다는 신호를 받으면 임계구역에 진입하는 방식으로 구현합니다.
세마포어도 단순한 형태로 구현할 수 있습니다. 전역 변수 하나, 함수 두 개로 구현하는데, 아래에 정리해 보겠습니다. (이것도 뮤텍스 락처럼 실제 구현에는 이렇게 간단하지는 않으나, 이것도 수도코드니까 이해하는 용도로 봐주시면 됩니다.)
- 전역변수 S : 임계 구역에 진입할 수 있는 프로세스의 개수 (사용 가능한 공유 자원의 개수)를 나타내는 변수
- wait 함수 : 임계 구역에 들어가도 좋은지, 기다려야 할지를 알려주는 함수
- signal 함수 : 임계 구역 앞에서 기다리는 프로세스에 '이제 가도 좋다' 고 신호를 주는 함수

- 만일 임계 구역에 진입할 수 있는 프로세스 개수가 0 이하라면
- 사용할 수 있는 자원이 있는지 반복적으로 확인하고,
- 임계 구역에 진입할 수 있는 프로세스 개수가 하나 이상이면 S를 1 감소시키고 임계 구역에 진입 (왜 1 감소시키냐면 내가 들어갈 거니까)

- 임계 구역에서 작업을 마친 뒤에 S를 1 증가시킵니다. (왜 1 증가시키나면 내가 나가니까)
이렇게 구현하는 세마포어 wait와 signal 함수는 임계 구역 앞 뒤로 붙여주면 됩니다. 이 두 함수를 임계 구역 앞 뒤로 붙인 예시입니다. 세 개의 프로세스 P1, P2, P3가 두 개의 공유 자원(S=2)에 P1, P2, P3 순서로 접근한다고 가정합니다.

- 프로세스 P1 wait 호출, S는 현재 2이므로 S를 1 감소시키고 임계 구역 진입
- 프로세스 P2 wait 호출. S는 현재 1이므로 S를 1 감소시키고 임계 구역 진입
- 프로세스 P3 wait 호출. S는 현재 0이므로 무한히 반복하며 S 확인
- 프로세스 P1 임계 구역 작업 종료, signal() 호출. S를 1 증가
- 프로세스 P3 S가 1이 됨을 확인. S는 현재 1이므로 S를 1 감소시키고 임계 구역 진입
사실 세마포어 방식의 동기화 기법에서도 Busy waiting이 발생합니다. 3번처럼 S가 0이 아닌지 무한히 반복하며 확인하는 과정에서 Busy waiting이 발생하는데요, 이렇게 탈의실 문을 쉴 새 없이 덜컹거리는 건 CPU 사이클을 냄비 하는 거라 좋은 방법은 아닙니다. 그래서 세마포어에서는 아래와 같은 방법으로 Busy waiting 문제를 해결합니다.
- 사용할 수 있는 자원이 없을 경우, 대기 상태로 만듦 (해당 프로세스의 PCB를 대기 큐에 삽입, 대기 큐에 들어가면 CPU 사이클 필요 없음)
- 사용할 수 있는 자원이 생겼을 경우, 대기 큐의 프로세스를 준비 상태로 만듦 (해당 프로세스의 PCB를 대기 큐에서 꺼내 준비 큐에 삽입)

그러면 위처럼 대기/준비 큐에 넣고 빼는 과정을 추가한다면, 세 개의 프로세스 P1, P2, P3 로 예시를 들었던 과정을 아래처럼 개선할 수 있습니다.
- 프로세스 P1 wait 호출, S는 현재 2이므로 S를 1 감소시키고 임계 구역 진입
- 프로세스 P2 wait 호출. S는 현재 1이므로 S를 1 감소시키고 임계 구역 진입
- 프로세스 P3 wait 호출.
S는 현재 0이므로 무한히 반복하며 S 확인S를 1 감소시키면 S는 -1이므로 본인의 PCB를 대기 큐에 넣고 대기 상태로 전환 - 프로세스 P1 임계 구역 작업 종료, signal() 호출.
S를 1 증가S를 1 증가하면 0이므로 대기 상태였던 P3를 대기 큐에서 꺼내 준비 큐로 옮겨줌 프로세스 P3 S가 1이 됨을 확인. S는 현재 1이므로 S를 1 감소시키고 임계 구역 진입깨어난 프로세스 P3 임계 구역 진입- 프로세스 P2 임계 구역 작업 종료, signal() 호출, S가 1 증가하면 1
- 프로세스 P3 임계 구역 작업 종류, signal() 호출, S가 1 증가하면 2
세마포어는 상호 배제뿐만 아니라, 실행 순서 제어 동기화에도 적용할 수 있습니다. 실행 순서 제어에는 어떻게 적용하냐면, 세마포어 변수 S를 0으로 두고, 먼저 실행할 프로세스 뒤에 signal 함수를, 다음에 실행할 프로세스 앞에 wait 함수를 붙이면 됩니다. (P1 ➡️ P2 순서로 실행 보장)
3-3. 상호 배제, 실행 순서 제어를 위한 동기화 도구 - 모니터 (Monitor)
위처럼 세마포어는 뮤텍스 락에서 발생할 수 있는 Busy waiting 문제도 해결할 수 있고, 뮤텍스 락보다 더 일반화된 방법인데, 매번 임계 구역 앞뒤로 wait 함수와 signal 함수를 호출해야 됩니다. 개발자가 코드를 치다가 실수로 wait()와 signal() 순서를 헷갈려서 반대로 넣는다던지, wait()를 중복해서 사용한다던가 signal()을 중복으로 사용하는 실수를 한다면 문제가 발생할 수 있습니다.
그래서 등장한 동기화 도구가 모니터입니다. 세마포어는 사용자(개발자) 입장에서 다루기 좀 어렵고 번거로운 동기화 도구라면, 모니터는 사용자 입장에서 다루기에 편한 동기화 도구입니다. (모니터 방식을 활용하는 대표적인 프로그래밍 언어로 Java가 있습니다.)
모니터는 세마포어와 마찬가지로 상호 배제, 실행 순서 제어 둘 다 제공하는 동기화 도구입니다.

먼저 상호 배제를 위한 모니터 작동 원리를 설명하자면, 위의 그림처럼 공유 자원과 그 공유 자원에 접근하기 위한 통로를 묶어서 관리합니다. 그리고 이 공유 자원에 접근하고자 하는 프로세스와 스레드는 반드시 특정 인터페이스를 거치는데, 인터페이스를 거치기 전 프로세스는 큐에 삽입됩니다. 그리고 큐에 삽입된 순서대로 (한 번에 하나의 프로세스만) 공유 자원을 이용합니다.
다음으로는 실행 순서 제어를 위한 동기화에 모니터 기법이 작동하는 원리입니다.

실행 순서 제어를 위한 동기화에서는 조건 변수(condition variable)를 이용합니다. 이 조건 변수는 프로세스나 스레드의 실행 순서를 제어하기 위해 사용하는 특별한 변수입니다. (쉽게 말하자면 wait()나 signal()을 호출할 수 있는 특별한 변수)
모니터 내부를 보면, 각각의 조견 변수마다 큐가 만들어지는데, 이 조건 변수에 대한 큐들로서 프로세스나 스레드의 실행 순서를 제어할 수 있습니다. (상호 배제를 위한 큐와 이 조건변수 큐는 다릅니다.)
여기서도 wait() 와 signal()이 등장하는데, 조건 변수 wait()는 대기 상태로 변경, 조건 변수에 대한 큐에 삽입하는 연산을 하고, 조건 변수 signal()은 wait()으로 대기 상태로 접어든 조건 변수를 실행 상태로 변경하는 연산을 합니다.


모니터 안에는 하나의 프로세스만 있을 수 있습니다. 모니터의 큰 작동 원리는 이거고, 작동 방식은 두 가지로 나눠집니다.
- wait()를 호출했던 프로세스는 signal()을 호출한 프로세스가 모니터를 떠난 뒤에 수행을 재개
- signal()을 호출한 프로세스의 실행을 일시 중단하고, 자신이 실행된 뒤 다시 signal()을 호출한 프로세스의 수행을 재개
그래서 실행 순서 제어를 위한 동기화에 모니터 기법을 사용한다면,
- 특정 프로세스가 아직 실행될 조건이 되지 않았을 때에는 wait를 통해 실행을 중단
- 특정 프로세스가 실행될 조건이 충족되었을 때에는 signal을 통해 실행을 재개
위의 로 실행 순서를 제어할 수 있습니다. (어렵따...)
4. 정리
이렇게 동기화를 왜 해야 하는지, 동기화를 하지 않았을 때 발생할 수 있는 레이스 컨디션 사례, 동기화 기법에 대해서 정리해 봤습니다. 강의를 들으면서 역시 동기화는 말로 설명을 들으면 참 어렵다... 라는 생각을 했습니다. 오히려 Redis랑 Kafka 도입해서 코드 치면서 공부하는 게 더 이해하기 쉬웠다 라고 느꼈습니다....... 필요하시다면 위에서 설명하는 방법들을 각자 사용하는 프로그래밍 언어로 구현해 보는 것도 좋을 것 같다 라는 생각입니다.
제가 MySQL, Redis, Kafka를 이용해서 동시성 제어를 하는 방법을 정리한 글이 있는데, 이 글은 Spring 공부 카테고리로 들어가면 찾으실 수 있습니다. (그... 알아서 찾으세요 죄송합니다 글이 4개라서 쭉 읽어보시면 좋을 듯합니다)
아무튼 이번 동기화 글은 여기서 마무리하겠습니다. 그럼 20000~
[자료 출처]
강민철, 《혼자 공부하는 컴퓨터 구조+운영체제》, 한빛미디어, 2022
인프런 - 개발자를 위한 컴퓨터공학 1 : 혼자 공부하는 컴퓨터구조 + 운영체제
🍀
좋아하는 것을 계속 좋아하세요!
반드시 행복해집니다
백엔드 개발자가 되고 싶어서 열심히 헤딩 중인 재영입니다 :-)
[Github] https://github.com/chujaeyeong
[E-mail] chujy1224@gmail.com
'Study > 컴퓨터구조, 운영체제' 카테고리의 다른 글
| 교착 상태(데드 락, Dead Lock)에 대해 정리해보자 (0) | 2025.01.11 |
|---|---|
| CPU 스케줄링과 알고리즘을 정리해보자 (0) | 2025.01.08 |
| 스레드를 정리해보자 (스레드의 구성 요소, 멀티 스레드, 멀티 프로세스) (1) | 2025.01.07 |
| 프로세스 상태와 계층 구조에 대해 정리해보자 (0) | 2025.01.07 |
| 프로세스의 개요와 PCB, 메모리 영역에 대해 정리해보자 (0) | 2025.01.07 |