[iOS] Concurrency 학습하기 전에..

Hyunndy·2023년 2월 7일
0

iOS-Concurrency

목록 보기
1/4

🐸

Swift의 async/await을 학습하기에 앞서 너무나도 기본적으로 알고있어야할 관련 개념들에 대해 간단히 정리하고 넘어가려고 합니다.


1️⃣ 프로세스(Process)

등장배경

컴퓨터가 발전하면서 여러 프로그램이 메인 메모리에 적재되어 병행 실행되었다.
이런 변화는 컴퓨터의 효율을 높여주었지만, 프로그램에 대한 강력한 통제도 요구되기 시작했다.
이 때 프로세스라는 개념이 등장했다.

개념

간단히 실행 중인 프로그램 을 말한다.

하드디스크에 저장되어 있던 응용 프로그램을 실행하면 실행을 위해서 메모리에 적재되기 위한 메모리 할당이 일어난다.
이 때 응용 프로그램에게 할당된 메모리 영역(주소 공간)으로 바이너리 코드가 올라가게되고, 이 순간부터 프로그램은 프로세스라고 불린다.

스케줄링의 대상이 되는 작업(Task)와 같은 의미로 쓰인다.
프로세스의 내부에는 최소 1개 이상의 스레드(Thread)가 있는데, 실제로는 스레드 단위로 스케줄링을 한다.

메모리 영역


프로세스는 메모리에 위와 같은 주소 공간을 갖는 능동적인 객체이다.

Stack 영역

  • 일시적인 데이터를 저장하는 영역
  • 호출된 함수의 복귀주소, 지역변수, 매개변수, 리턴 값 등이 저장되며 함수가 종료되면 제거된다.
  • 컴파일 타임에 크기가 결정되기 때문에 무한히 할당할 수 없으며, 재귀 함수 연속 호출이나 지역 변수의 크기가 너무 커져 Stack 영역을 넘어서게 되면 Stack Overflow가 발생한다.

Heap 영역

  • 동적할당을 위한 자유 메모리 영역
  • 런타임에 크기가 결정되며, 동적 할당 등으로 Heap영역의 메모리를 사용할 수 있다.
  • 사용한 다음 반드시 해제해야하며, 안하면 Memory leak이 발생한다.
  • 클래스의 인스턴스, 클로저 등등 참조 타입(Reference Type)으로 선언 시 생성된 변수는 스택영역에 생성되고 객체는 힙 영역에 생성된다.
    ex) let person = Person() 일 때, Person 인스턴스는 힙 영역에, 지역 변수 person은 스택 영역에 할당된다.

Data 영역

  • 전역변수 또는 정적(static) 변수가 저장되는 영역
  • 전역/정적 변수를 참조한 코드는 컴파일 후 프로세스 실행 중에 동적으로 Data 영역의 주소값을 가리키도록 변경된다.
  • 실행 도중 값이 바뀔 수 있으니 읽고-쓰기가 가능한 영역이다.

Code 영역

  • 프로그램을 실행시키는 실행 파일 내의 명령어(소스코드)가 저장되는 영역

관점

사용자 👩‍💻 : (코드, 데이터, 힙, 스택) 등 메모리 영역을 갖는 실행중인 프로그램
시스템 💻 : 처리상태에 있는 데이터 구조

프로그램은 프로세서(ex. CPU)를 할당 받아야 실행 상태의 프로세스가 될 수 있다.

프로세서 할당은 어떻게 받는건데..?

CPU는 하나인데... 동시에 실행해야할 프로세스는 한가득이다.
이럴 때 CPU는 고속으로 여러 프로세스를 일정한 기준으로 순서를 정해서 실행한다. -> 스케줄링

이 때 파일에 관련된 자원에 대한 참조(장치&메모리 관리), 프로세스 지원과 협력에 대한 정보(교착 상태, 보호, 동기화) 교환을 합니다.

스케줄링

아무튼 CPU는 스케줄링 알고리즘을 이용해서 CPU 할당 순서 및 방법을 결정합니다.
이 때 프로세스는 ready(준비), blocked(대기, 보류), running(실행) 상태를 번갈아가며 실행됩니다.

스케줄링 기법까지 보면 전공책을 펴야하기 때문에 프로세스는 기본적으로 이만큼만 정리하겠습니다.
다음은 스레드 입니다.


2️⃣ 스레드(Thread)

개념

프로세스 내에서 실제로 작업을 실행하는 주체, 단위 입니다.

스레드는 프로세스처럼 프로세서를 사용하는 기본 단위이면서 프로그램을 실행하는 프로세스 내의 개체, 즉 명령어를 독립적으로 실행할 수 있는 하나의 제어 흐름 입니다.

포함 내용

프로세스는 최소 한 개 이상의 스레드를 가지며, 각 스레드는 다음 내용을 갖습니다.

  • 스레드 실행 정보(실행, 준비 등)
  • 실행 스택
  • 스레드 지역 데이터
  • 프로세스 메모리와 자원에 대한 접근, 같은 스레드의 실행 환경 정보
    -> 같은 프로세스에 속한 스레드들끼리는 Data, Code, Heap 영역을 공유한다.

스레드의 장점

스레드를 사용하면 자원을 공유하며 한 프로세스 내에서 동시 작업(= 병렬 수행)이 가능합니다.
이 때 다음의 이점을 갖습니다.

  • 사용자 응답성 증가 -> 긴 작업 수행이나 응용프로그램 일부가 블락되도 다른 스레드에서 사용자에게 응답 가능
  • 프로세스 자원과 메모리 공유 가능
  • 경제성
  • 다중 프로세서 구조 활용 가능

3️⃣ 테스크(Task)

프로세스, 스레드와 가장 헷갈리는 개념 1등 테스크가 나왔습니다.
테스크는 무엇일까요?

개념

테스크는 비동기(Aynchronous)적으로 실행되는 단일 작업 을 의미합니다.
프로세스와 스레드와 혼동되기 쉬운데, Task는 단일 작업!이고 Process, Thread는 작업을 제어하는 제어 흐름! 라고 구분합니다.
Thread 위에서 Task 작업이 실행된다. 라고 이해하면 되겠네요.


여기까지 async/await과 관련된 OS 관련 용어들을 봤고...
이제 방법론(?) 용어들을 보겠습니다.

4️⃣ Synchronous(동기) 와 Asynchronous(비동기)

동기/비동기 방식은 이 글이 정말 유익했습니다. 거의 이 글을 다시 정리하는..

동기/비동기 방식은 프로세스의 수행 순서 보장에 대한 메커니즘 입니다.
(딱! 맞는 표현이 써있었습니다 ㅠㅠ)

즉, 메인쓰레드에서 다른 쓰레드에 Task를 주고, 그걸 기다리냐 안기다리냐의 개념입니다.

Synchronous(동기)

저 글을 읽기 전까지 동기란 현재 작업의 요청과 응답이 동시에 발생하는 것으로 알고있었는데,
모든 예제의 그림이나 글을 봐도 저 글에서 말한 현재 작업의 응답과 다음 작업을 요청하는 타이밍을 동시로 맞추는 방식 이라고 이해하면 훨씬 편한 것 같다.

하나 알고있어야 할것은 결국 이 <방식> 으로 <작업>의 순서를 정한다는 것이다.
-> Synchronous 방식으로 Process를 실행시킬 것이다.

예를들어...
hyunndy 객체는 가습기 객체가 물부족 알럿을 띄우기 전까진 공부를 그만둘 수 없습니다.

이걸 Synchronous 방식으로 코드를 구현했다고 하면..

hyunndy: 공부시작()
가습기: 가동() X 2h
가습기: 물부족!()
hyunndy: 공부끝()

콜 스택이 이렇게 쌓일것입니다.
여기서 hyunndy 프로세스는 가습기 process가 물 부족 알럿을 띄우기 전까지는 절대 종료될 수 없습니다.

이 방식을 동기 방식 + 블로킹 방식이라고 하는데,
지금까지 전 동기 방식을 이렇게만 이해하고 있었던 것 같습니다.
🙅‍♀️ Synchronous?!!!! 요청 작업이 끝나기 저까지 절!!대 다른 작업 수행할 수 없어!!

아닙니다.
Hyunndy 객체는 공부시작() 호출 후 가습기 객체의 물부족! 알럿 전 까지 공부끝()을 수행할 수 없어.
라는 작업의 <순서>만 보장되면,
Hyunndy 객체는 가습기 객체가 가동() 함수를 호출하는 동안 과자먹기() 등 다른 작업을 수행할 수 있습니다.

따라서 Synchronous 방식은 작업의 순차적인 흐름만 지켜진다면 블로킹 || 논블로킹 방식 둘 다 상관 없습니다.

비동기(Asynchronous)

동기와 반대로 현재 작업의 응답과 다음 작업을 요청의 타이밍이 일치하지 않아도 되는 방식이다.

동기 방식은 상위 프로세스(Hyunndy)가 하위 프로세스(가습기)의 작업의 종료 시점을 알고 있어야 합니다. 하위 프로세스의 작업이 완료되어 응답이 와야 내가 공부끝() 작업을 요청할 수 있었으니까요.

하지만 비동기는! 종료가되던말던 신경쓰지 않습니다. 요청한 작업의 응답이 안와도 다음 작업의 요청을 할 수 있습니다.

비동기 방식 + 블로킹 방식은 Swift에서는 쓰지마세요. 정도니까 넘어가고,
비동기 방식 + 논블로킹 방식은 여러개의 작업을 동시에 처리할 수 있어서 효율적이지만, 너무 복잡하게 얽힌 비동기 처리(ex. completion의 중첩 무덤)때문에 휴먼 에러등이 발생할 수 있다.

그래서! 우리는 async/await라는 문법으로 비동기 방식(asynchronous)의 흐름을 좀 더 명확하게 인지할 수 있다.


여기까지 프로세스의 수행 순서 보장에 대한 메커니즘인 Synchronous&Asynchronous 방식을 보았습니다.

근데 지금까지 살펴본..프로세스, 스레드, 테스크, 동기, 비동기와 가장 연관있는 iOS 개념이 무엇일까요?
바로 GCD입니다.
GCD는 API이죠? GCD가 사용하는것은 DispatchQueue입니다.

방식(동기/비동기)를 알아봤으니 실제 작업(ex. 프로세스, 스레드, 테스크)를 실행시킬 도구에 대한 특성도 알아봐야겠죠..?
다음 알아볼 개념은 Queue(대기 행렬)의 특성인 Serial/Concurrency 입니다.

5️⃣ Serial(직렬)와 Concurrency(동시)

Queue의 작동방식에 대한 개념입니다.
메인 스레드에서 분산 처리 시킨 작업들이 다른 1개의 쓰레드에서 수행되냐, 여러 쓰레드에서 수행되냐의 차이입니다.

Serial(직렬)

Serial이란 무엇일까요?
파파고에 쳐보면 순차적인이라는 뜻이 나옵니다.

즉, Serial Queue는 들어온 작업을 순차적으로 실행시키는 Queue입니다.

정리하자면 다음과 같은 특징을 갖습니다.

  • 들어온 작업을 순차적으로 실행시킨다.
  • 한 번에 하나의 Task만 실행할 수 있다. (async로 호출해도 Synchronous하게 실행됨) asyc던 sync던 한 번에!! 하나의!!! Task!!!
    -한 번에 하나밖에 못하기 때문에 (보통 메인스레드에서) 분산 처리 시킨 작업이 다른 한 개의 Thread에서만 실행된다.

DispatchQueue의 main Queue가 바로 Serial Queue죠.

print("start")
DispatchQueue.main.async {
    for _ in 0...5 {
        print("async")
    }
}
for _ in 0...5 {
    print("sync")
}
print("end")

의 실행결과가..

start
sync
sync
sync
sync
sync
end
async
async
async
async
async

요렇습니다.
async가 비동기니까 중간에 껴있을것 같지만, SerialQueue기 때문에 먼저 쌓인 작업들이 순차적으로 실행된 후에 다음에 들어온 작업이 실행됩니다.

Concurrency(동시)

Concurrency는 동시 실행된다는 뜻입니다.

즉, ConcurrentQueue는 Queue에 들어온 작업을 동시에 실행 시킵니다.

다음과 같은 특징을 갖습니다.

  • 각 작업들이 동시에 실행된다.
  • (보통 메인스레드에서) 분산 처리 시킨 작업을 여러개의 다른 Thread에서 실행된다.

느낀점

제일 헷갈렸던 것
동기 <-> 비동기: 프로세스의 수행 순서에 대한 개념
직렬 <-> 병렬: 큐의 동작 방식에 대한 개념
각자 전혀 관련없는 대륙들이라는 것.

여기까지 async/await를 공부하다보면 꼬리의 꼬리를 물고 나오는 개념들에 대해 한 번 정리해보았습니다.
내일부터는 async/await를 헷갈리지 않게 학습해보도록 하겠습니다ㅎㅎ

profile
https://hyunndyblog.tistory.com/163 티스토리에서 이사 중

0개의 댓글