반응형
기초 개념
스레드와 실행컨텍스트
- 스레드 : 운영체제 관리하는 실행단위로, 하나의 프로세스 내에서 여러개의 스레드가 동시에 실행될 수 있다. 스레드는 독립적인 스택 및 레지스터를 가지고 있으며 CPU가 실행할 수 있는 최소 단위이다. 스레드는 병렬성을 구현하는데 사용된다.
- 실행 컨텍스트 : 실행중인 코드의 실행상태를 추상화하는 개념으로, 실행 컨텍스트는 해당 코드의 실행위치, 변수값, 호출스택 등을 저장한다. 비동기적 실행을 구현하는데 사용된다. 각 실행 컨텍스트가 해당 코드의 실행 상태를 저장하고 있어 해당 코드가 중단되고 다시 실행될 수 있다.
- 예를 들어 함수를 호출하면 해당 함수는 독립적인 실행 컨텍스트를 가진다.
- Swift에서는 Task로 실행 컨텍스트의 개념을 대체한다.
- 하나의 스레드는 여러개의 실행 컨텍스트를 가질 수 있기 때문에 하나의 스레드에서 여러 코드 블록이 동시에 실행될 수도 있다. 반대로 여러개의 스레드가 하나의 실행 컨텍스트를 공유할 수도 있다.
async / sync와 concurrent / serial
- async / sync와 concurrent / serial 는 별개의 개념이고 정말 기본적인 개념인데 매번 헷갈림.
- async / sync
- async : 특정 작업을 하러가던 말던 남은 작업 수행함
- sync : 특정 작업 하러가면 끝날 때까지 기다림
- concurrent / serial
- concurrent : 여러 스레드가 나눠서 작업하도록함 (순서보장못함)
- serial : 한 스레드가 혼자 작업하도록함 (순서보장가능)
async / await 함수가 등장한 배경
1. completion 형태 코드 문제점
- 먼저 기존에 자주 사용하던 completion 형태의 비동기 처리 코드를 살펴보자.
- 사진의 이름목록 중 첫번째 사진을 다운로드하고 사용자에게 표시하는 코드이다.
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
}
- 비교적 간단한 케이스임에도 콜백지옥이 깊어질 것이 상당히 우려되는 형태이다.
- 이를 극복할 방법은 없을까? (1)
2. GCD의 문제점
- 기존 GCD는 동시성 작업을 처리할 때 특정 스레드가 block 중이라면 다른 처리해야할 스레드를 새로 불러와서 필요한 작업을 수행한다.
- 이렇게 처리하는 이유는 지속적으로 코어가 작업중인 스레드를 보유하도록 하기 위함이며, 새로운 작업에 세마포어와 같이 block 중인 스레드를 재개할 수 있도록 돕는 코드가 있을 수 있기 때문이다.
- 하지만 block된 스레드가 많아지면서 스레드가 계속 생성되면 데드락, 메모리 및 스케줄링 오버헤드를 야기하는 thread explosion를 야기할 수 있다.
- 메모리 오버헤드 : block된 스레드에서 보유중인 메모리 및 리소스의 축적
- 스케줄링 오버헤드 : 스레드 변경에 따른 과도한 context switch (성능저하)
- 이를 극복할 방법은 없을까? (2)
async / await의 등장 및 장점
- 이러한 (1), (2)의 문제를 해결하기 위해 만들어진 것이 async / await 로 대표되는 Swift의 Concurrency이다.
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
- 이를 사용하면 위와 같이 비동기코드를 동기적으로 작성할 수 있어 콜백지옥 문제를 해결할 수 있다.
- 또한 Swift의 Concurrency는 최대로 실행되는 스레드가 코어의 개수와 동일하고 스레드간 context switch도 일어나지 않는다.
- 스레드의 block 대신 작업의 재실행을 추적하는 continuation 객체가 존재한다. context switch를 수행하는 대신 같은 스레드 내부의 continuation 객체만을 전환한다. (context switch 대신 함수 호출 정도의 비용만 소모)
- 이러한 특성으로 thread explosion 문제도 해결된다.
async / await 의 작업 흐름
- async / await 의 ‘async’ 역시 일반적인 async와 같은 의미로 async 함수가 실행되면 특정 작업을 하러감과 무관하게 다른 작업을 이어서 수행할 수 있다.
- async / await에서 async를 호출하면 기존 실행 컨텍스트를 처리하던 스레드는 자유로운 상태가 된다. 즉 작업권을 다른 곳에 넘겨줄 수 있게 된 것이다. 다만 보통은 현재 스레드를 그대로 다음 async 작업처리에 이용하는 것이 효율적이기 때문에 같은 스레드를 사용하는 경우가 많다.
- 사실 개인적인 생각으로는 어떤 스레드를 사용하는지가 그리 중요한 것은 아니라고 생각한다.
- 애플이 스레드 관리 작업을 추상화하여 제공하는 만큼, ‘아 이렇게하면 Swift가 다양한 스레드를 활용해 동시적으로 작업을 처리하겠구나’ 정도로 이해하면 충분하다.
- 현재의 실행 컨텍스트를 그대로 유지하면서 async 함수의 작업을 처리한다.
- async 함수의 작업이 마무리되면 await 아래의 나머지 코드가 이어서 실행된다.
- 이 때 주의할 점은 시스템이 스레드를 적당히 분배하면서 처리하는 것이기 때문에 async 함수 호출 전의 스레드와 await 이후 작업의 스레드는 같지 않을 수 있다는 점이다.
- 앱의 상태도 그 동안 충분히 변할 수 있다.
반응형
'🍎 Apple > Concurrency & GCD' 카테고리의 다른 글
[Concurrency] Continuation (2) | 2023.03.31 |
---|---|
[Concurrency] Task (0) | 2023.03.31 |
[Swift] GCD 정리하기 (2) | 2022.05.30 |
[Swift] Operation Queue (0) | 2022.05.27 |
[Swift] Actor (4) | 2022.03.08 |