GCD
Thread & Queue
- Task: 일
- Thread: 노동자
- Queue: 대기 행렬 (FIFO 구조)
GCD: Grand Central Dispatch
- GCD는 Dispatch Queue 사용: OS가 Thread로 알아서 분산 처리
Dispatch Queue 종류
- Main Queue: 1개, Serial, Main Thread
- Global Queue: Concurrent, QoS 종류 6개
- Custom Queue: Serial/Concurrent 선택 가능, QoS 설정 가능
Operation Queue
- GCD + @ 라고 생각하면 됨
- 동시에 실행할 수 있는 동작의 최대 수 지정 가능
- 동작 일시 중지 및 취소 가능
Sync vs Async
- Sync: Queue로 보낸 작업이 완료 될 때까지 기다림
- Async: Queue로 보낸 작업을 안 기다리고 다음 코드 실행 (끝나기를 기다리지 않음)
Serial vs Concurrent
- Serial queue (직렬 큐): 한개의 쓰레드에서 전부 진행 → 순서 예측 O
- Concurrent queue (병렬 큐): 여러개의 쓰레드에서 진행 → 순서 보장 X
Concurrency
async
- 함수 정의 뒷부분에 async를 붙이면 해당 함수는 비동기라는 것을 나타냄
- async 함수는 concurrent context 내부 즉, 다른 async 함수 내부 혹은 Task 내부에서 사용 가능
func listPhotos(inGallery name: String) async -> [String] {
let result = await asyncFunction()
return result
}
await
- async 함수를 호출하기 위해 await가 필요하고, 이는 potential suspension point로 지정 된다는 것을 의미
- Suspend 된다는 것은 해당 thread에 대한 control을 포기한다는 것
- 해당 코드를 돌리고 있던 thread에 대한 control은 system에게 가고, system은 해당 thread를 사용하여 다른 작업 가능
print("비동기 함수 호출 전")
await asyncFunction()
- await로 인한 중단은 해당 thread에서 다른 코드의 실행을 막지는 않음
- function은 suspend 되고, 다른 것들이 먼저 실행 될 수 있고 그렇기에 그 동안 앱의 상태가 크게 변할 수 있음
- thread를 차단하는 대신 control을 포기해 작업을 중지 및 재개할 수 있는 개념을 도입
print("비동기 함수 호출 전")
await asyncFunction()
print("비동기 함수 호출 후")
- 정리하자면, await로 async 함수를 호출하는 순간 해당 thread control 포기
- 따라서 async 작업 및 같은 블럭에 있는 다음 코드들을 바로 실행하지 못함
- thread control을 system에게 넘기면서, system에게 해당 async 작업도 schedule
- system은 다른 중요한 작업이 있다면 먼저 실행하고, 특정 thread control을 줘서 async 함수와 나머지 코드를 resume
Task
SwiftUI task에서 비동기 작업하기
- Task는 비동기 작업 단위: A unit of asynchronous work
- 격리되어(isolated), 독립적으로(independently) 비동기 작업을 수행
- 값이 공유될 상황이 있을때는 Sendable 체킹을 통해 Task가 격리된 상태로 남아있는지 체크
print("1")
Task {
print("2")
}
print("3")
- Task 안에서의 작업은 처음부터 끝까지 순차적으로 실행
- await를 만나면 작업은 몇번이고 중단될 수는 있지만, 실행 순서가 변경되지는 않음
Task {
let fish = await catchFish()
let dinner = await cook(fish)
await eat(dinner)
}
- Data Race: 여러 Task가 동시에 일을 하고 있지만 동일한 객체(Class)를 참조하기 때문에 발생
- Sendable 프로토콜: 동시에 사용해도 안전한 타입 (Actor)
Concurrency +
Parallel
- 이렇게 하면 한 번에 하나의 코드만 실행
- 비동기 코드가 실행되는 동안, 호출자는 다음 코드를 실행하기 전에 해당 코드가 완료될 때까지 기다림
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
- 비동기 함수를 호출하고 주변 코드와 병렬로 실행 하는것도 가능
- 정의할 때 let 앞에 async를 작성한 다음 상수를 사용할 때마다 await를 사용
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
TaskGroup
- TaskGroup으로 여러 병렬 작업을 합쳐 모든 작업이 완료되면 결과를 반환 받을 수 있음
let images = await withTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
let photoURLs = await loadPhotoUrls()
for photoURL in photoURLs {
taskGroup.addTask { await downloadPhoto(url: photoURL) }
}
var images = [UIImage]()
for await result in taskGroup {
images.append(result)
}
return images
}
AsyncStream
- AsyncStream은 순서가 있고, 비동기적으로 생성된 요소들의 sequence
- yield로 스트림에 Element를 제공
- finish로 정상적으로 스트림을 종료
let digits = AsyncStream<Int> { continuation in
for digit in 1...10 {
continuation.yield(digit)
}
continuation.finish()
}
for await digit in digits {
print(digit)
}
- Throw가 가능한 AsyncThrowingStream도 존재
let digits = AsyncThrowingStream<Int, Error> { continuation in
for digit in 1...10 {
continuation.yield(digit)
}
continuation.finish(throwing: error)
}
do {
for try await digit in digits {
print(digit)
}
} catch {
}
continuation.onTermination = { termination in
switch termination {
case .finished:
print("finished")
case .cancelled:
print("cancelled")
}
}
Continuation
- 기본적으로
Continuation
은 비동기 호출 이후에 일어나는 코드 - 클로저 기반의 completion handler 비동기 처리를 async/await 형태로 바꾸기 위해 존재
func fetch(completion: @escaping ([String]) -> Void) {
let url = URL(string: "https://www.hohyeonmoon.com")!
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data {
if let something = try? JSONDecoder().decode([String].self, from: data) {
completion(something)
return
}
}
completion([])
}.resume()
}
func fetch() async -> [String] {
await withCheckedContinuation { continuation in
fetch { something in
continuation.resume(returning: something)
}
}
}
let something = await fetch()
- 위와 같이 클로저 비동기 처리를 async로 변경 가능
- continue에서 두 번 이상 resume을 호출하면 안됨
- resume을 호출하지 않아도 Task가 무기한 일시 중단된 상태로 유지
- 즉, resume은 정확히 한번 호출되어야 함
Actor
Actor
- 공유 데이터에 접근해야 하는 여러 Task를 조정
- 외부로부터 데이터를 격리하고, 한 번에 하나의 Task만 내부 상태를 조작하도록 허용
- 동시 변경으로 인한 Data Race를 피함
- Actor의 목적이 shared mutable state를 표현하는 것이기 때문에 class와 같은 reference 타입
actor SharedWallet {
let name = "공유 지갑"
var amount = 0
init(amount: Int) {
self.amount = amount
}
func spendMoney(ammount: Int) {
self.amount -= ammount
}
}
Task {
let wallet = SharedWallet(amount: 10000)
let name = wallet.name
let amount = await wallet.amount
await wallet.spendMoney(ammount: 100)
await wallet.amount += 100
}
- 1: 상수는 변경 불가능하기 때문에 어느 스레드에서 접근해도 안전하고 actor 외부에서도 바로 접근 가능
- 2: actor 외부에서 변수 접근시 await 필요
- 3: actor 외부에서 메서드 호출시 await 필요
- 4: 컴파일 에러, actor 외부에서 actor 내부의 변수를 변경할 수 없음
MainActor
- MainActor: main thread를 나타내는 특별한 global actor
- await이 사용되었다는 것은, 해당 thread에서 다른 코드가 실행될 수 있도록 실행 중인 함수가 중지될 수 있다는 의미
await MainActor.run {
}
await MainActor.run {
}
- 따라서 메인 스레드에서 한꺼번에 작업이 이뤄지길 원하는 경우에는 관련 함수를 run block에 그룹화 해야함
- 그래야 해당 함수들 사이에는 일시 중단 없이 호출이 실행되도록 할 수 있음
await MainActor.run {
}
- Main Thread에서 실행되어야 하는 코드를 @MainActor 표시로 함수, 클로저, 타입 등에 적용 가능
- class, struct, enum 같은 type에 붙으면, 내부에 있는 모든 property와 method가 isolated 되고 main thread에서 동작
actor SomeActor {
let id = UUID().uuidString
@MainActor var myProperty: String
init(_ myProperty: String) {
self.myProperty = myProperty
}
@MainActor func changeMyProperty(to newValue: String) {
self.myProperty = newValue
}
func changePropertyToName() {
Task { @MainActor in
myProperty = "hohyeon"
}
}
}
참고