[운영체제] 동시성(Concurrency)

 

1. 동시성이란?

여러 작업을 동시에 실행되는 것처럼 보이게 하는 것을 말한다.

 

 

2. 프로그램 / 프로세스 / 스레드

동시성에 대해 자세하게 알아보기 앞서 알아둬야하는 개념들이 있다.

정의

프로그램: 단순한 코드 덩어리로 실행되지 않는 이상 특정 목적을 수행하기 위한 명령문의 집합

프로세스: 프로그램이 실제로 실행되어 메모리 상에 올라가 실행 중인 것을 의미

스레드: 프로세스 안에서 프로세스의 자원을 이용해서 실제로 작업을 수행하는 작업자

 

 

멀티 프로세스란???

  •  하나의 운영체제에서 프로세스를 동시에 여러 개 실행시킬 수 있게 하는 기술

 

만약 이 멀티 프로세스가 아니라 싱글 프로세스라면???

 

우리가 게임을 하다가 카카오톡을 하고, 인터넷을 켜서 유튜브를 보다가 다시 게임을 한다고 가정해 보자. 싱글 프로세스 환경에서는 한 번에 하나의 작업만 실행할 수 있다.

 

예를 들어:

  1. 게임을 하다가 친구로부터 카카오톡 메시지를 받으면, 게임을 종료해야 카카오톡을 확인할 수 있다.
  2. 카카오톡을 하다가 게임 공략을 찾아보려면, 카카오톡을 종료하고 인터넷 브라우저를 켜서 검색을 해야한다.
  3. 유게임 꿀팁을 본 후, 다시 게임을 하기 위해 인터넷 브라우저를 종료하고 게임을 실행해야 한다.

이처럼 싱글 프로세스 환경에서는 여러 작업을 번갈아 가며 하나씩 실행해야 하기 때문에 매우 불편하다. 반면에 멀티 프로세스 환경에서는 게임을 하면서 동시에 카카오톡 메시지를 확인하고, 인터넷 브라우저를 보고 게임을 할 수 있어 훨씬 더 효율적이고 편리하다.

 

 

멀티 스레드란???

  • 하나의 프로세스 내에서 둘 이상의 스레드가 동시에 작업을 수행하는 것

 

그럼 멀티 스레드가 무슨 역할을 하는데?

 

 간단하게 웹 브라우저를 예로 들어보자. 보통 우리는 웹 브라우저에서 여러 탭을 켜놓고 사용한다. 이때 각 탭을 별도의 스레드로 실행하여 한 탭에서 작업을 수행하는 동안 다른 탭에서 페이지 로딩 등의 작업을 동시에 처리한다.

 

 다른 예시로는 음악 프로그램이 있다. 음악 프로그램에서 여러 작업을 병렬로 처리할 수 있는데

  1. 음악을 재생하는 스레드
  2. 사용자 클릭 등을 처리하는 스레드
  3. 앨범 아트나 가사를 표시하는 스레드 
  4. 이 외의 다양한 스레드

한 프로세스 안에서 위의 스레드들이 병렬적으로 작업을 처리하면서 성능과 효율성을 향상시킨다.

 

 

3. 메모리 구조

1. 멀티 프로세스 환경에서의 메모리 구조

  • 하나의 운영체제에서 여러 프로세스들이 각각의 독릭접인 메모리 공간을 할당
  • 하나의 프로세스 내부에 코드, 데이터, 힙, 스택 영역들이 각각 존재

 

2. 멀티 스레드 환경에서의 프로세스 메모리 구조

  • 멀티 스레드 환경에서 프로세스 메모리 구조를 더욱 자세하게 보면, 스레드 내부에 스택 영역이 존재하며 코드, 데이터, 힙 영역을 서로 공유하게 됨
  • 코드, 데이터, 스택 영역컴파일 시 메모리 크기가 결정
    • 컴파일 시 메모리 크기가 결정되게 되는 것을 정적 할당
  • 힙 영역 같은 경우 런타임 시 메모리 크기가 결정
    • 런타임 시 메모리 크기가 결정되게 되는 것을 동적 할당

 

  • 메모리 주소값은 코드, 데이터, 힙 순서대로 낮은 주소값부터 할당
  • 스택 영역은 높은 주소값부터 할당
  • 힙 영역과 스택 영역의 메모리 영역은 서로 공유
  • 이때 스택 영역의 메모리 주소값이 지나치게 많이 할당되어 힙 영역을 침범하는 것을 Stack Overflow
  • 힙 영역의 메모리 주소값이 지나치게 많이 할당되어 스택 영역을 침범하는 것을 Heap Overflow

 

 

4. 동시성 이슈

4-1. 동시성 이슈란???

 여러 작업이 동시에 실행될 때 발생할 수 있는 문제

 

 

예를 들어 은행 계좌에서 돈이 입금되고 출금되는 두 가지 작업이 동시에 일어난다면??

 

  1. 현재 계좌에 100만원이 있고, 스레드 A가 50만원을 입금하려 하며, 스레드 B가 30만원을 출금하려고 한다.
  2. 스레드 A, B가 동시에 계좌 잔액이 100만원인 것을 읽는다.
  3. A가 50만원을 더해 150만원으로 업데이트를 하고
  4. 동시에 B가 30만원을 빼서 70만원으로 업데이트한다.

그렇다면 실제로 120만원이여야하는데 동시성 이슈로 인해 잘못 계산되어 70만원이 된다.

 

 

동시성 이슈로는 Race Condition, Deadlock, Starvation 등이 있다.

 

4-2. Race Condition

여러 스레드들이 같은 데이터에 접근해서 결과값에 영향을 줄 수 있는 상태

 

위의 은행 계좌의 예시가 대표적인 Race Condition이다.

 

위 문제를 해결하기 위해서는 상호 배제를 통해 공유 자원에 하나의 스레드접근하도록 설계해야 한다. (Mutex, semaphore 등..)

상호 배제(Mutual exclusion): 공유 데이터에 접근하는 부분을 임계 영역으로 지정하고, 하나의 스레드만 접근하도록 설계
임계 영역(Critical Section): 공유 자원이라고도 하며, 둘 이상의 스레드가 동시에 접근해서는 안 되는 영역

 

4-2-1. Mutex

  • 공유 자원에 접근하는 스레드들에게 lock이라는 bool type 변수를 통해 권한을 주고, lock을 취득한 스레드만 공유 자원에 접근 가능 하게 하는 방법
  • 공유 자원에 접근하기 전까지 스레드들은 반복문을 통해 lock이 해제되었는지 기다리게 되는데 이를 Spinlock이라고 한다.
  • 하지만! Spinlock 방식은 대기 순서를 보장하지 않아, 계속해서 공유 자원에 접근하지 못하는 스레드가 발생할 가능성이 있기 때문에 동시성 이슈인 Starvation(기아상태) 문제가 발생할 수 있다.

 

예를 들어 1,2,3,4 스레드가 있다고 해보자.

  1.  1번 스레드가 먼저 lock을 취득해 공유 자원에 접근해 있고 나머지 2,3,4가 순차적으로 공유 자원에 접근해 spinlock을 하고 있다.
  2.  1번 스레드가 작업을 끝내고 lock을 해제하여 2,3,4 중 4가 lock을 취득해 공유 자원에 접근한다.
  3.  마찬가지로 4가 작업을 끝내고 lock을 해제하였지만 이번엔 3이 lock을 취득한다. 그리고 추가적으로 다른 스레드 5,6,7,8.... 이 spinlock 상태로 lock을 취득하기 위해 대기하고 있다.

 

위 같은 상황이 계속 이어질 때 2번 스레드가 lock을 취득하지 못할 수 있어서 Starvation 문제가 발생할 수 있는 것이다.

 

 

4-2-3. Semaphore

  • 공유 자원에 접근할 수 있는 스레드 개수를 Semaphore라는 Integer 변수로 관리하는 기법
  • Semaphore 변수가 0이 된다면, 공유 자원에는 더 이상 스레드들이 접근할 수 없게 되고, 대기 Queue에 이후 스레드들을 관리
  • 즉, 순서 대기 보장이 되기에 Starvation 문제가 발생하지 않음
  • 하지만, Semaphore를 취득한 여러 스레드들끼리도 Race Condition 문제가 발생할 수 있기에 근복적으로 Race Condition 문제를 해결하지는 못함

 

4-2-4. Binary Semaphore

  • Semaphore과 동일하지만, 공유 자원에 접근하는 Semaphore을 이진수의 Binary  형태로 관리하기 때문에 스레드가 한 개만 접근이 가능
  • 따라서 Race Condition 문제 발생 X
  • Mutex와 비슷하지만 공유 자원에 접근하지 못하는 스레드들을 대기 Queue에서 관리하므로 Starvation 문제 발생 X

 

 

 

 

4-3. Deadlock

교착 상태라고도 하며, 두 개 이상의 작업이 서로 끝나기만을 기다리는 상태

 

 

예를 들어 프로세스 A, B가 있고 프린터와 스캐너가 있다고 가정해 보자.

이때 프로세스 A는 프린터를 먼저 사용하고, 그다음에 스캐너를 사용해야 한다.

반면 프로세스 B는 스캐너를 먼저 사용하고, 그 다음에 프린터를 사용해야 한다.

 

  1.  프로세스 A가 프린터를 점유, 프로세스 B가 스캐너를 점유한다.
  2.  프로세스 A가 이제 스캐너를 사용하려 하지만, 이미 점유 상태라 대기 상태에 빠진다.
  3.  마찬가지로 프로세스 B도 프린트를 사용하려 하지만, 이미 점유 상태라 대기 상태에 빠진다.
  4. 두 프로세스는 각각 서로가 점유한 자원을 기다리며 무한 대기에 빠지게 된다.

 

4-3-1. Deadlock 발생 조건

  • 상호 배제
    • 공유 자원에 하나의 스레드만 접근이 가능해야 한다
  • 점유 대기
    • 자원을 하나 보유한 상태에서 다른 스레드에 할당된 자원을 점유하기 위해 대기하는 스레드가 존재해야 한다.
  • 비선점
    • 다른 스레드에게 할당된 자원을 강제로 빼앗을 수 없다.
  • 순환 대기
    • 대기하는 스레드들의 집합이 순환 형태로 자원을 대기하고 있어야 한다.

 

4-3-2. Deadlock 해결 방법

  • 예방
    • Deadlock 발생 조건 중 1개를 발생시키지 않도록 설정
  • 회피
    • 발생할 가능성이 있는지 Banker's Algorithm을 통해 예측하고, 발생할 가능성이 있다면 요청을 거절
  • 탐지 / 복구
    • Deadlock을 허용하지만 데드락이 발생했는지 여부를 지속적으로 탐지하고, 발생한 경우 Deadlock Recovery 방식을 통해 Deadlock 회복
  • 무시
    • 아무 처리도 하지 않음
    • Deadlock을 해결하는 데 드는 Resource가 크기 때문에, 대부분의 운영체제에서는 무시를 가장 많이 채택

 

 

5. Swift에서 동시성 이슈

 

그렇다면 어떻게 해야 Swift에서 동시성 이슈를 해결할 수 있을까?

 

크게 두 가지 방법이 존재한다.

1. GCD

2. Swift Concurrency

 

5-1. GCD

5-1-1. DispatchQueue.sync

  • sync로 보내게 되면 결국 공유 자원에 접근하는 Thread는 1개가 보장되므로 동시성 문제를 해결할 수 있음
  • Dispatch Barrier

 

5-1-2. NSLock

  • Mutex와 동일한 기법으로, 앱 내에서 여러 실행 스레드의 작업을 조정하는 객체

 

lock(before: Date:)

  • 특정 시점까지 lock을 획득하도록 시도하는 메서드


unlock()

  • lock을 해제하여 다른 스레드가 접근할 수 있도록 설정하는 메서드


try()

  • lock을 획득할 수 있는지 시도하는 메서드

 

 

5-1-3. DispatchSemaphore

  • Semaphore와 동일한 기법으로, 공유 자원에 접근할 수 있는 스레드 개수를 지정하여 접근을 제어하는 객체

wait()

  • Semaphore 변수를 감소시키는 메서드


signal()

  • Semaphore 변수를 증가시키는 메서드

 

 

5-2. Swift Concurrency

5-2-1. async - await

  • async - await를 사용하게 되면, 비동기 작업과 이에 필요한 함수 파라미터들을 Heap 영역에 올리고, 작업 제어권을 system에게 넘김
  • 즉 system이 비동기 작업을 처리하는 스레드를 재할당하기에, await를 만나기 전 시점에 동작하던 스레드가 await 이후에 같은 스레드에서 작업하는 것을 보장하지 않음!
  • 또 async - await는 비동기 작업을 성능적으로 향상시키고, 직관적인 코드 작성을 구성하게 해주는 기술이지 동시성 이슈를 해결하기 위한 방법은 아님
  • 따라서 Race Condition을 해결하기 위한 방법으로 actor에 대한 개념이 등장

 

5-2-2. actor

  • Sendable protocol을 채택한 타입으로 하나의 task만 내부 상태를 조작하도록 설정한 객체
  • Sendable protocol 같은 경우, 동시성 상황에서 안전하게 데이터를 공유할 수 있는 타입
  • 만일 여러 task들이 actor에 접근하게 되면, 위 task들을 내부적으로 await을 통해 직렬화시켜 하나씩 처리하도록 함. (이를 Serialization이라 함)
  • MainActor 같은 경우 actor + Main Thread의 동작을 보장하는 개념