프로세스와 스레드
- process: 실행중인 프로그램
- 프로그램이 실행되어 CPU를 할당받고 메모리에 올라가게 되면(동적인 상태가 되면) 작업으로써의 단위 개념을 갖게 되는 것
- thread: 프로세스 안에 포함되어 있는 실행 흐름의 단위.(= 실행 흐름의 최소)
- 작업(process)은 코드 덩어리의 연산을 통해 이루어지는데, 연산은 연속적인 특성 ⇒ 하나의 흐름을 만듦. 이것이 thread
하나의 process는 최소 한 개의 thread를 가진다.
💡 thread의 필요성
- process는 서로 분리된 작업 영역을 갖고 있기 때문에 자원 공유가 어려움
- thread는 메모리를 공유하며 동작할 수 있기 때문에 thread가 필요
“복잡한 process가 n개의 thread를 사용하여 작업을 하는 것”
async와 await에서 thread 사용
- 메인 스레드(Main Thread)
- UI 업데이트와 사용자 이벤트 처리를 담당
- 백그라운드 스레드(Background Thread)
- 메인 스레드와 별도로 실행, 복잡한 작업이나 시간이 오래 걸리는 작업을 처리하는 데 사용
- 비동기 작업은 기본적으로 백그라운드 스레드에서 실행
- DispatchQueue
- 스레드 관리를 위한 기능을 제공
- 작업을 특정 스레드에 할당하거나 특정 시간에 실행되도록 예약
동기(sync)
- 해당 작업이 끝날 때까지 다른 작업들은 기다림
비동기(async)
- 해당 작업이 끝나든 말든 신경 쓰지 않고 나머지 작업을 바로 실행
- 작업 시간이 오래걸리는 것들은 비동기로 처리
- ex) 이미지 다운로드, 네트워킹
“일을 비동기로 보내놓고 신경을 안쓰고 있는데.. 그럼 실제 해당 작업이 언제 끝나는지는 어떻게 알지?”
Swift에서는 클로저를 통해 해당 시점을 알려줌. 이게 **completionHandler** 이다.
비동기 작업의 completionHandler 사용 문제점
func makeCrunchyCookie(completion: @escaping ((Cookie) -> Void)) {
makeDough { dough in // ✅ (1)
self.chillDough(dough: dough) { ripedDough in // ✅ (2)
self.bakeCookie(dough: ripedDough, time: bakeTime) { cookie in // ✅ (3)
self.drawFace(cookie: cookie) { crunchyCookie in // ✅ (4)
completion(crunchyCookie)
}
}
}
}
}
- 중첩되는 코드 블럭으로 인한 가독성 저하
- completion handler를 호출하지 않는 실수 가능성
- self 에 접근
- **Retain cycle(순환 참조)**와 메모리 누수를 유발할 수 있음
- ⇒ 무분별한 [weak self]의 사용은 런타임 오버헤드를 발생
- ⇒ nil(값이 없음)을 체크해야 하는 수고를 거쳐야 함.
async, await 문법의 도입
- async: 비동기 함수임을 나타낸다.
- await: async 키워드가 표시된 메소드나 함수의 리턴을 기다림
💡 즉, async 함수는 비동기적으로 동작할 수 있고, await 키워드를 사용해 비동기 함수의 결과를 대기할 수 있음!
앞서 살펴본 예제를 async, await로 변경한 코드
func makeCrunchyCookie() async throws -> Cookie {
let dough = try await makeDough() // ✅ (1)
let ripedDough = try await chillDough(dough: dough) // ✅ (2)
let cookie = try await bakeCookie(dough: ripedDough, time: bakeTime) // ✅ (3)
let crunchyCookie = try await drawFace(cookie: cookie) // ✅ (4)
return crunchyCookie
}
async, await 문법의 장점
- completion handler가 사라지면서, 비동기 함수를 한 줄로 처리할 수 있어 훨씬 간결해보임
- retain cycle이 발생할 우려도 사라짐
- 개발자가 실수로 try await 키워드를 빼먹은 경우에는 컴파일러가 오류를 알려줘 조금 더 안정적인 에러 핸들링이 가능
TCA와 Combine
- TCA: Swift 언어를 기반으로한 애플리케이션 아키텍처
- Combine: 함수형 반응형 프로그래밍 프레임워크
- 비동기 프로그래밍을 위한 기능을 제공하며, TCA와 함께 사용
⇒ Combine은 강력한 비동기 프로그래밍 도구이지만, TCA는 단순한 동기적인 상태 관리 아키텍처
<aside> 💡 TCA에서 Combine 의존성을 떼어내는 이유
- 유연성과 독립성
- Combine을 완전히 의존하면 비동기 처리를 위해 앱 전체에 Combine을 도입 → 앱의 규모나 복잡성에따라 어려움
- 쉬운 테스트
- Combine을 완전히 의존하면 테스트할 때 Combine의 비동기 동작을 처리 → 코드의 작성, 실행 복잡
- 유지 보수성
- Combine은 계속해서 업데이트되는 프레임워크로, 버전 간의 호환성 문제가 발생 </aside>
async
- 함수 이름 뒤에 async 가 붙으면 이 함수는 비동기
- async코드는 동시(concurrent) 컨텍스트 에서만 실행 가능.
- 즉, 다른 async 함수 내 에서, 또는 **Task {}**를 통해 수동으로 concurrent context를 제공할 때 그 안에서 가능함.
await
- async 함수를 호출하기 위해서는 await 키워드가 필요
- 예시에서 호출하고 있는 URLSession 함수도 async 임을 알 수 있음.
suspend
“await 를 만나면 그곳에서 suspend 될 수 있다”
⇒ 일시 중단 (suspend) 은 해당 스레드가 다른 동작을 수행할 수 있게 제어권을 놓아주겠다는 의미
sync에서의 thread 제어권
1. 동기 함수(sync) 일때
A 함수에서 B 라는 동기 함수(sync)를 호출하면, A 함수가 실행되던 스레드의 컨트롤을 B 함수에게 전달
→ B 함수가 끝날때까지 해당 스레드는 완전히 점유되어서 다른 일 수행 X
→ B 함수가 끝나면 다시 A 함수에게 스레드에 대한 컨트롤 돌려줌.
2. 비동기 함수(async) 일때
B 함수는 async 함수이기 때문에 중간에 suspend 될 수 있음. (그러면 원래 A 함수도 suspend 됨)
→ 스레드에 대한 제어권은 system에게 가고, 시스템은 스레드를 사용하여 다른 작업을 수행할 수 있게 됨.
- 그렇게 시스템이 열심히 우선 순위 등을 판단해가면서 여러 작업들을 실행하다가,
- 어느 시점에 이르면 일시 중단된 비동기 함수를 계속 실행하는 작업이 가장 중요하다고 판단하는 순간이 옴.
- 그때 해당 함수를 재개(resume) 하고, 비동기 함수는 할당 받은 스레드를 제어하고 작업을 계속함
정리
- await로 async 함수를 호출하는 순간, 즉 Suspension point를 만나는 순간 해당 스레드 제어권 포기
- 따라서 async 작업 및 같은 블록에 있는 아래 코드들을 스레드 잡아서 바로 실행 못함
- 스레드 제어권을 시스템에게 넘기면서, 시스템에게 원래 async 작업도 하라고 함
- 시스템은 “다른게 더 중요해보이는데?” 하고 해당 스레드에서 다른 작업 먼저 실행할 수 있음
- 그러다가 시스템이 원래 async 함수가 중요해지는 순간이 왔다고 판단하면 “이제 resume 해” 라고 하고, 그 스레드에게 특정한 스레드 제어권을 주어서 마저 실행 됨 (이때 resume 되는 스레드는 다른 스레드일 수 있음)
참고
https://www.notion.so/sbmo/swift-async-await-e4ffa7403a3542fcbbae93b869989f9f?pvs=4#92c5afd374f94498bce13e102c0b8490