프로그래밍 공부/컴퓨터공학 기초

동기화(Synchronization) Mutex와 Semaphore

Wonuk 2022. 12. 28. 10:03
반응형
해당 포스팅은 우아한 테크코스
이브, 배카라의 Synchronization을 기반으로 작성되었습니다.
https://www.youtube.com/watch?v=ImWjQ1Bxjrs&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=8

 


사전 지식 - CPU 동작 방식

하나의 CPU에서 여러개의 프로세스 혹은 스레드를 실행하는 방식.

프로세스 동작 방식

프로세스의 경우 CPU의 자원이 있어야만 실행이 가능합니다.

여러개의 프로세스를 실행할 경우 Context Switch를 통해 CPU 자원을 번갈아 사용합니다.

해당 과정에서 코드 실행과 대기를 반복하는 방식으로 동작하게 됩니다.

컴퓨터에서는 이 과정이 빠르게 실행되기 때문에 사용자의 입장에서는 두개의 프로세스가

동시에 실행되는것처럼 보입니다.

 

멀티 프로세서 환경에서의 동작 방식

실제로 CPU자원이 프로세스별로 할당되어 동시에 실행되게 됩니다.

 

 

프로세스와 스레드

프로세스 - 메모리 내에서 작업을 하는 하나의 작업 단위

하나의 프로세스 내에서 스레드라는 여러 실행 흐름으로 나뉩니다.

이것을 멀티스레딩 방식이라고 합니다.

 

프로세스는 자기 내부에서 여러개의 스레드를 가짐으로써

실행 흐름을 여러개로 나눠서 실행할 수 있습니다.

 

이때 스레드들도 CPU의 자원을 할당받아야 실행할 수 있기 때문에

하나의 프로세스 내에서 하나의 CPU를 번갈아가면서 할당하는 방식으로 진행하게 됩니다.

 

 

 


공유 자원 동시 접근 문제

두개의 프로세스가 있고 공유 메모리에 0으로 초기화 된 count라는 공유 변수가 있습니다.

각각의 프로세스는 코드의 한줄씩만 실행하면 되기 때문에

두개의 프로세스 실행결과 메모리에 0이라는 값이 남을것이라고 예상할 수 있습니다.

 

하지만 사실 count++ 이라는 코드한줄은 개발자 입장에서는 한줄이지만

CPU가 이해하기 위한 어셈블리어로 컴파일되는 과정에서

위 세가지 단계로 나누어지게 됩니다.

 

  1.  메모리에서 CPU 레지스터로 공유 변수 값을 가져오는 작업 - LOAD
  2. 가져온 값으로 연산을 수행하는 작업 - INC
  3. 실행한 결과를 다시 메모리에 저장하는 작업 - STORE

위쪽의 사진에서 각각의 프로세스는 이렇게 세 단계로 나뉜 코드를 실행하게 됩니다.

 

이상황에서 왼쪽의 프로세스1이 먼저 CPU를 할당 받았다면,

1. LOAD 연산을 하기 위해서 공유 메모리에서 0이라는 값을 LOAD 합니다.

2. 값을 하나 증가 시킵니다.

Context Switch 발생!

3. 메모리에 저장합니다.(실행 못함)

3. 프로세스 2로 CPU의 주도권이 넘어가게됨

4. 프로세스 2에서 값을 LOAD해와야 하지만 아직 프로세스1이 연산 결과를 메모리에 저장하지 않아서

    또 0이라는 값을 LOAD하게 됩니다.

5. 값을 하나 감소시킵니다.

6. 메모리에 저장합니다.

Context Switch 발생!

프로세스 1이 CPU를 점유하게 되는데 이때 STORE를하면 1이라는 값이 저장되게 됩니다.

 

결과 - count값이 0이 아닌 1이 남게 됩니다.

 

 

 

경쟁 상태와 임계구역

경쟁 상태 (Race condition)
공유 자원에 동시에 여러 개의 프로세스가 접근하려고 할 때, 그 실행 결과가
접근이 발생한 특정 순서에 의존하는 상황
임계 구역 (Critical section)
공유 자원에 접근하는 부분

공유 자원에 동시에 여러 개의 프로세스가 접근하려고 할 때
그 실행 결과가 접근이 발생한 특정 순서에 의존하는 상황을 경쟁 상태라고 합니다.

 

그리고 이 공유자원에 접근함으로써 경쟁 상태가 발생할 수 있는 구역을 임계구역 이라고 합니다.

 

위 사진의 프로세스에서는 count라는 공유 변수에 접근하는 코드가

임계 구역이라고 할 수 있습니다.

각각의 프로세스는 본인의 할일을 잘 마쳤지만

개발자 입장에서는 경쟁 상태가 발생했기 때문에 예상하지 못한 결과를 맞이하게 됩니다.

 

 

경쟁 상태 발생 상황 - 프로세스와 스레드

원론적인 동기화의 원인이나 해결 방안은 프로세스와 스레드에 동일하게 적용할 수 있는데

프로세스와 스레드가 메모리 구조에있어서 차이가 있기 때문에

경쟁 상태 발생 상황은 조금 다릅니다.

 

멀티 스레딩 환경에서 경쟁 상태 발생 상황

스레드는 하나의 프로세스 내에서 여러 개로 나뉜 실행 흐름 단위이기 때문에

지역변수 저장을 위한 스택(Stack)은 스레드별로 각자 갖지만

나머지 프로세스 데이터들은 같은 메모리 영역을 사용하게 됩니다.

 

그래서 스레드들이 프로세스의 공유 데이터 영역에 접근하는 순간

경쟁상태가 발생할 수 있습니다.

 

프로세스 경쟁 상태 발생 상황 

프로세스는 메모리 내에서 각자 독립적인 주소 공간을 갖는 작업의 단위입니다.

그래서 원칙적으로는 다른 프로세스의 주소 공간에는 접근하지 않는데도 불구하고

경쟁 상태가 발생하는 상황이 생깁니다.

 

1. 커널 내부 데이터에 접근하는 루틴 - 프로세스

운영체제는 CPU를 포함한 여러 하드웨어 자원들을 효율적으로 관리하는 역할을 합니다.
만약에 악의적인 프로세스가 디스크나 다른 프로세스의 주소 공간에 접근하려고 한다면
운영체제는 이를 방지할 수 있어야 합니다.

하드웨어에서는 운영체제를 지원하기 위해서 두가지의 실행모드를 지원합니다.

 

  • 유저 모드 - 디스크 입출력과 같은 하드웨어 자원에 대한 접근 권한 일부 제한
  • 커널 모드 - 운영체제가 특권을 얻어서 모든 하드웨어 자원에 접근 가능

 

프로세스 2도 커널 모드에서 동일한 주소를 가진

count값을 수정하려 한다면 경쟁상태가 발생합니다.

 

1 - 1. 커널 내부 데이터에 접근하는 루틴 - 멀티 프로세서

Context Switch는 발생하지 않겠지만,

그럼에도 실행 순서에 따른 이슈는 남아있습니다.

 

예를들어 속도가 더 빠른 프로세스2가 STORE 연산까지

프로세스 1보다 빠르게 끝난다면 같은 상황이 발생합니다.

 

 

2. 협력적인 프로세스

  • 정보 공유 ex) 공유 폴더에 접근하려는 프로세스가 여러개인 경우
  • 빠른 태스크 처리를 위한 병렬 처리 프로세스
  • 여러 프로세스들이 협력하는 모듈 형태의 시스템
프로세스들이 협력하기 위해서는 별도의 프로세스간 통신 기법이 필요합니다.
이때 Shared Memory 기법을 활용한다면
프로세스간 실제 물리적인 메모리 영역을 공유하기 때문에 경쟁상태가 발생할 수 있습니다.
그래서 이 경우에도 추가적인 동기화 매커니즘을 필요로 합니다.

Shared Memory 공유 메모리 환경에서 프로세스간 메모리 영역을 공유하며 협력

 

 

그렇다면 이러한 공유 변수와 실행 순서에 따른
동기화 이슈를 방지하기 위해서는 어떻게 해야할까?

주로 OS 레벨에서 제공하는 대표적인 동기화 기법인
뮤텍스(Mutex)와 세마포어(Semaphore)가 있습니다.

 

 


뮤텍스와 세마포어(Mutex, Semaphore)

예시) 캠퍼스에 JPA 책이 한권 있다.

이때 두명의 크루가 같은 책을 보려고하면 경쟁상태가 발생한다.

위와같은 상황을 방지하기 위해서 사서를 한명 배치합니다.

이제부터는 책을 읽기위해서는 책에 직접 접근 하는것이 아니라

사서를 통해서만 책에 접근할 수 있게됩니다.

 

만약 크루 1이 먼저 접근해서 책을 읽고 싶다고하면

사서는 책의 상태를 확인하고 아무도 읽고있지 않다면 책을 읽게해주고

크루2가 책을 읽고 싶다고 한다면 현재 크루1이 읽고있는 상황이기 때문에

사서는 거절하게 됩니다.

 

상호 배제 Mutex(Mutual Exclusion)
하나의 프로세스가 임계 영역 내에 있다면 이 프로세스의 동작이
끝날 때까지 다른 프로세스가 임계 영역에 들어올 수 없도록
제한하는 알고리즘

 

운영체제에서는 뮤텍스를 위해 락(Lock)이라는 매커니즘을 제공합니다.

 

책을 읽을 수 있는 상태라면 크루1은 사서를통해 Lock을 걸어 잠그면서

책에 접근하여 읽을 수 있습니다.

 

이 상황에서 크루2가 접근하려고 한다면 사서가 크루2의 접근을 가로막습니다.

이후 크루1이 책을 다 읽으면 본인이 걸었던 락을 해제하면서 임계 구역을 빠져나갑니다.

 

그제서야 크루2는 락을 걸어잠그면서 책을 읽으러 들어올 수 있습니다.

락은 구현 방식에 따라서 한가지 성능 이슈가 존재합니다.
Spin Lock

 

Spin Lock

 

공유 자원에 접근할 때 Lock이라는 메서드를 사용하는데

만약에 누군가 이미 공유 자원을 사용하고 있다면 while문이 계속 실행되게 됩니다.

이러한 Lock을 Spin-lock이라고 합니다.

사서의 입장에서 본다면 크루2의 요청만 받아주는 상황이 아니라

모든 크루의 요청을 받아주어야 하기때문에 굉장히 자원이 낭비되는 상황이 발생합니다.

이 문제를 해결하고자 책 읽는 방법을 다른 방법으로 변경했습니다.

 

크루2는 대기실에서 잠을자고 있다가 크루1이 임계 영역에서 빠져나와

크루2가 접근할 수 있게된다면 잠에서 깨어나 컨텍스트 스위칭을 합니다.

이 시간 이후에 JPA책으로 접근할 수 있게 되는 것입니다.

 

변경된 코드

스핀락의 경우 반복문이 돌기때문에 성능이 좋지않다고 생각하는데

공유 자원 사용 시간이 Context Switch 시간보다 짧다면 더 효율적입니다.

공유 자원 사용 시간이 짧으면 대기실에 크루를 재워둘 필요가 없고

Context Switch는 많은 시간을 잡아 먹기 때문에 Spin-Lock을 사용할 수 있습니다.

단, 이 경우는 멀티 코어일 경우에만 허용됩니다.

 

만약 싱글 코어의 경우에는 Spin Lock의 성능이 좋지 못합니다.

크루2가 접근하려면 자원을 할당받아야 하고

크루1이 임계 영역 밖으로 나오려면 자원을 다시 할당 받으며

Context Switch가 반복적으로 발생합니다.

 

결론 - Spin Lock을 사용하는 경우

공유 자원 사용 시간 < Context Switch 시간 + 멀티 코어

 

 

Semaphore

책을 읽고싶은 사람이 있다면 book Count를 확인하여

크루가 책에 접근할 수 있게 되고

크루들이 모든 책을 읽고있어서 book Count가 0이라면

 

사서는 book Count를 -1로 만들어주고

크루6은 대기자 명단에 이름을 적고 대기실에서 잠을자게 됩니다.

 

코드

 

정리

반응형