지난 시간에는 동시성 코드의 개요와 async-await를 이용한 비동기처리와 병렬처리, 비동기 시퀀스에 대해서 다뤘다 이번시간에는 비동기 처리에 필요한 Task에 대해서 알아보려 한다.
Task란 무엇일까?
Task는 프로그램의 일부로 비동기적으로 실행할 수 있는 작업 단위를 말한다.
모든 비동기 코드는 어떠한 Task의 일부로 실행되는데
Task는 한번에 하나의 작업만 수행하지만, 여러 작업을 생성하면, Swift는 동시에 수행하기 위해 작업을 스케쥴링 할 수 있다. (= 동시 작업의 우선순위와 순서를 지정 할 수 있다)
이전에 코드를 한번 보자
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)
async-let을 이용해서 photos에서 비동기 함수를 묶었는데
이러한 작업은 암시적으로 작업을 생성한다.
이처럼 Task로 작업단위를 명시하지 않으면 swift에서 자동으로 Task를 처리하고 계층을 나눈다. 명시적으로 Task를 사용하면 작업의 계층을 지정하고 작업의 순서등을 지정할 수 있다.
Task {
let result = await fetchData()
print(result)
}
Task는 {}중괄호로 묶은 형태로 사용되며 내부에서 비동기 처리를 도와주는 방법이다.
TaskGroup은 여러개의 비동기 코드를 처리하도록 해주는 코드그룹이다.
await withTaskGroup(of: Int.self) { group in
// 여러 개의 비동기 작업 추가
group.addTask {
return await fetchData1()
}
group.addTask {
return await fetchData2()
}
// 그룹 내의 각 작업의 결과를 처리
for await result in group {
print("Result: \(result)")
}
}
다음과 같은 형태로 사용되며 .addTask{ }를 통해서 처리할 비동기 코드를 추가할 수 있다.
(여기서 group은 TaskGroup의 인스턴스이다.)
특징 | Task | TaskGroup |
---|---|---|
역할 | 하나의 비동기 작업을 실행하고 관리 | 여러 개의 비동기 작업을 병렬로 실행 및 관리 |
작업 수 | 한 번에 하나의 작업 | 여러 개의 작업을 동시에 처리 |
결과 처리 | 단일 작업의 결과를 처리 | 그룹 내 모든 작업의 결과를 처리 |
용도 | 간단한 비동기 작업 | 복수의 비동기 작업을 처리할 때 적합 |
실행 제어 | 비동기 작업을 개별적으로 실행 | 작업 간의 동기화 및 결과 수집 가능 |
- Task는 하나의 비동기 작업을 실행하고, 동기화 없이 병렬로 작업을 처리하고자 할 때 사용
- TaskGroup은 여러 개의 비동기 작업을 병렬로 처리하고, 그 결과를 관리하거나 작업 간 동기화를 보장해야 할 때 적합함
만약 네트워크에서 데이터를 가져온다거나, 이미지등의 데이터를 불러오는 경우 작업 시간이 너무 길어져 작업을 취소해야 하는 경우가 생길 수 있다.
이럴때 사용하는 것이다.
작업취소에는 여러가지 방법이 있지만 관통하는 내용은 동일하다.
- Task의 취소는 즉각적인 강제 종료가 아님
- 취소 가능 작업: Task는 실행 중에 취소될 수 있다
- 비동기 함수 내부에서 작업이 취소되었는지 확인하고, 취소된 경우 적절히 처리해야 한다
이 3가지를 지키는것을 권장한다.
Task {
for i in 1...10 {
if Task.isCancelled {
print("Task has been cancelled")
return
}
print(i)
await Task.sleep(1_000_000_000) // 1초 대기
}
}
// 3초 후 작업 취소
Task {
await Task.sleep(3_000_000_000) // 3초 대기
Task.current?.cancel() // 현재 작업 취소
}
↳ 이 코드에서 첫 번째 Task는 1부터 10까지 숫자를 출력하는 작업을 수행합니다. 3초 후 두 번째 Task가 실행되면서 첫 번째 작업을 취소하게 되고, 작업이 취소되면 Task.isCancelled 상태를 확인하여 작업을 종료합니다.
Task.current?.cancel()로 Task를 종료시킨다.
앞서 말했던 것처럼 작업을 종료시키면 종료된 작업에 대해서 별도의 처리를 해주어야 한다.
Task {
for i in 1...10 {
try await Task.checkCancellation() // 작업이 취소되면 CancellationError를 던짐
print(i)
await Task.sleep(1_000_000_000) // 1초 대기
}
}
// 3초 후 작업 취소
Task {
await Task.sleep(3_000_000_000)
Task.current?.cancel() // 작업 취소
}
이 방법은 checkCancellation()을 사용해서 CancellationError를 던지는 형식으로 작업종료를 처리한다.
Actor는 동시성을 지원하는 클래스라고 생각하면 된다.
기존의 클래스의 경우 인스턴스 생성이나 내부 프로퍼티 접근에 있어서 동시성처리가 불가능 하다 하지만 Actor의 경우 이러한 접근에 대해 동시성을 지원하는 클래스라고 생각하면 된다.
actor BankAccount {
var balance: Double = 0.0
// 잔액을 업데이트하는 프로퍼티
func deposit(amount: Double) {
balance += amount
}
// 잔액을 조회하는 프로퍼티
func getBalance() -> Double {
return balance
}
}
let account = BankAccount()
Task {
await account.deposit(amount: 100.0)
let currentBalance = await account.getBalance()
print("Current balance: \(currentBalance)")
}
액터의 경우 내부 프로퍼티 접근에 비동기로 접근할 수 있기 때문에 await키워드를 사용해서 접근해야 한다.
기존의 클래스의 경우 비동기로 접근하면 경합이 일어날 수 있다
쉽게 말하면 여러 Task가 동시에 같은 클래스 내부 데이터에 접근 할 수 있고, 이 때문에 오류가 발생할 수 있다는 말이다.
액터의 경우 동시접근을 제어하며 접근을 처리하기 때문에 이러한 동시 처리에서 오류를 줄여준다.
<<정리>>
1. 데이터 경합 방지: actor는 내부 상태에 대한 동시 접근을 방지하여, 여러 Task가 동시에 데이터를 수정하는 문제를 해결합니다.
2. 자동 동기화: actor는 동시성 모델을 기본적으로 지원하므로, 개발자가 명시적으로 동기화를 처리할 필요가 없습니다.
3. 비동기 작업 관리: 비동기 작업을 처리할 때 await 키워드를 사용해 안전하게 처리할 수 있습니다.
- 동시성 작업을 관리하기 위해서 Task와 TaskGroup을 사용해서 코드를 제어 할 수 있다.
- Task의 경우 중간에 종료 할 수 있다, 이 경우 취소된 작업에 대해서 별도의 처리가 필요하다.
- 동시성 처리에 특화된 Actor가 있다, 클래스와 비슷하다.