아침에 9시부터 일어나서 바로 학습정리 중이다..레전드 갓생.. 하지만 블로그 글이 올라가는 시간은 12시.....껄껄 ~
오늘은 Task에 대해서 알아볼 예정이다.ᐟ.ᐟ 왜냐 ~ 그냥 ~
Apple 공식문서에 따르면 Task
는 비동기 작업의 단위를 의미한다.
모든 비동기 코드는 task의 한 부분으로서 실행된다.Task
는 한 번에 하나만 실행될 수 있는데, Task
를 여러 개 만듦으로써 Swift가 여러 개의 Task
를 동시에 실행시킬 수 있도록 스케줄링할 수 있다.
init(priority: TaskPriority?, operation: sending () async -> Success)
, init(priority: TaskPriority?, operation: sending () async throws -> Success)
와 같은 방식으로 인스턴스를 생성할 수 있으며, operation 매개변수에 들어가는 클로저가 우리가 실행할 작업이라고 생각하면 된다.
이 외에 간단한 Task
의 특징들은 다음과 같다.
Task
는 생성과 동시에 따로 명시적으로 시작하거나 스케줄링을 하지 않아도 바로 실행시킬 수 있다.Task
가 생성되고 난 후에는 인스턴스를 사용해 작업이 완료될 때까지 기다리거나 취소시키는 등의 상호작용이 가능하다.Task
의 작업이 끝나거나 취소되기 전에Task
의 참조를 없애는 것이 가능하다. 하지만 참조를 없애면 작업의 결과를 받거나 취소시키는 것은 불가능해진다.
Task Group
TaskGroup
을 사용하여 child task 등의 상속관계를 만들 수 있고, 이를 활용하여 각각의 Task 우선순위나 cancellation등을 더 효과적으로 제어할 수 있다.
Task
는 계층 구조로 배열될 수 있는데, 지정된 TaskGroup
내부의 각 task들은 동일한 상위 작업(parent task)이 있고, 각각의 task들은 하위 작업(child task)을 가질 수 있다. 이러한 접근 방식을 구조화된 동시성(structured concurrency)라고 부른다.
이와 같은 명시적인 parent-child 관계의 task들은 아래와 같은 이점들을 갖는다.
- 상위 작업에서는 하위 작업이 완료될 때까지 기다려야한다.
- 하위 작업이 높은 작업의 우선순위를 가지고 있다면, 상위 작업의 우선순위는 자동적으로 더 높아진다.
- 상위 작업이 취소되면 자동적으로 하위 작업들도 취소된다.
- Task-local value들은 효율적이고 자동으로 하위 작업에 전파된다.
마지막 이점인 Task-local value들이 효율적으로 하위 작업에 전파된다는 뜻은.. 정확하게 이해하지 못했다.. 🫠
TaskGroup
은 withTaskGroup(of:)
메서드를 활용해서 만들 수 있다.
아래 코드는 withTaskGroup(of:)
메서드를 활용하여 task group을 만드는 예제 코드이다.
func listPhotos(inGallery name: String) async -> [String] {
return ["IMG001", "IMG99", "IMG0404"]
}
func downloadPhoto(named: String) async -> Data { return Data() }
func show(_ photo: Data) { }
await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
for await photo in group {
show(photo)
}
}
withTaskGroup(of:)
메서드 내부에 addTask
메서드를 활용하여 group 내부에 Task
를 추가하는 것을 확인할 수 있다. 이렇게 추가된 task들은 두 번째 for문 내부에서 show
함수를 실행시키게 되는데, 이 때 각각의 Task
들이 group에 append되는 순서는 뒤죽박죽이기때문에 photoNames
의 순서와 show가 되는 이미지의 순서는 다를 수 있다.
Task Cancellation
Swift Concurrency는 협력적 취소 모델(cooperative cancellation model)을 사용한다.
각각의 작업은 적절한 시점에 취소 여부를 확인하고 응답한다. 각각의 작업에 따라 취소에 대해 응답한다는 것은 일반적으로 다음 중 하나를 의미하게 된다.
CancellationError
와 같은 에러를 던진다.nil
혹은 빈 collection을 반환한다.- 부분적으로 완료된 작업을 반환한다.
이미지 다운로드와 같은 작업은 그 이미지의 크기가 너무 크거나 네트워크가 불안정하면 시간이 오래걸릴 수 있다. 이러한 경우 task가 완료될 때까지 기다리지 않고 유저가 작업을 중단할 수 있게 해야하는데, 이러한 작업을 하기 위해선 task의 취소를 확인해야한다. task의 취소를 확인할 수 있는 방법엔 다음 2가지가 있다.
Task.checkCancellation()
타입 메서드 호출Task.isCancelled
타입 프로퍼티 읽기
아래 예시는 이미지의 string
배열을 받아오는 async 함수 listPhotos(inGallery:)
를 통해 이미지 이름을 가져오고 TaskGroup
에 각각의 이미지 이름에 맞는 이미지를 다운받는 task를 append하는 flow이다.
let photos = await withTaskGroup(of: Optional<Data>.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await downloadPhoto(named: name)
}
guard added else { break }
}
var results: [Data] = []
for await photo in group {
if let photo { results.append(photo) }
}
return results
}
맨 처음 for문에서 .addTaskUnlessCancelled
메서드를 활용하여 해당 TaskGroup
이 취소되었는지 확인한다. 이후 isCancelled
타입 프로퍼티를 활용하여 task가 취소되었는지 확인하고, 취소되지 않은 task의 경우 group에 append한다. 만약 TaskGroup
이 취소되었거나 child task 중 하나가 취소되면 그 즉시 첫 번째 for문이 종료되고 group에 append 되어있는 task에 있는 이미지들만 results 배열에 append되어 반환되게된다.
여기서 잘 이해가 안가는 부분은 첫 번째 for문 내부의 guard문인데.. 어떻게 child task가 취소되었는지 확인하는건지… 이해가 잘 가지 않는 부분이다..
해당 이미지의 첫 번째 목차를 직독직해해보면 isCancelled
타입 프로퍼티를 사용해서 task의 취소를 확인하는 것 같은데.. 도대체 어떻게 확인하는것인지 이해가 잘 가지 않는다;;;
Task closure lifetime
위에서도 잠깐 언급되었던 것 같이 Task
는 후행클로저를 통해 실행될 작업을 생성할 수 있다.
이렇게 생성된 클로저 내부의 코드는 작업이 완료되고 나면 메모리에서 해제된다. Task
객체가 남아있다고 해도, 해당 Task
가 completion되고 나면 내부의 참조는 해제됨으로 따로 약한 참조(weak self
)를 해 줄 필요는 없다.
아래 예시코드를 보자.
struct Work: Sendable {}
actor Worker {
var work: Task<Void, Never>?
var result: Work?
deinit {
// even though the task is still retained,
// once it completes it no longer causes a reference cycle with the actor
print("deinit actor")
}
func start() {
work = Task {
print("start task work")
try? await Task.sleep(for: .seconds(3))
self.result = Work() // we captured self
print("completed task work")
// but as the task completes, this reference is released
}
// we keep a strong reference to the task
}
}
/// start task work
/// completed task work
/// deinit actor
Task
의 클로저 내부에서 self 캡처하는 것을 확인할 수 있다. 하지만 해당 클로저의 작업이 끝나면 참조는 해제되고 Task
와 actor(Worker
) 사이의 순환참조가 없어지기 때문에 따로 약한 참조를 해 줄 필요가 없는 것이다.
위에서도 언급했듯이 웬만한 경우엔 Task
클로저 내부에서 약한 참조를 할 필요가 없지만 아ㅏㅏㅏ주 간혹가다 Task
내부에서도 약한참조를 써야할 경우도 있긴 한 것 같다.. 예를 들면 async 함수가 매우매우매우 delay 되어서 끝나지 않는 경우..? 정확하진 않지만..
굉장히.. 러프하게 concurrency의 Task
에 대해 알아보았다.Actor
의 isolation을 먼저 했어야했는데... 모종의 이유로 일단 그건 미루기 ^_^.... 다음에 정리할세요 ~
참고자료
Task | Apple Developer Documentation
A unit of asynchronous work.
developer.apple.com
Documentation
docs.swift.org
Visualize and optimize Swift concurrency - WWDC22 - Videos - Apple Developer
Learn how you can optimize your app with the Swift Concurrency template in Instruments. We'll discuss common performance issues and show...
developer.apple.com
'Swift' 카테고리의 다른 글
[Swift] Actor(1) (4) | 2024.10.21 |
---|---|
[Swift] Property wrapper (0) | 2024.10.06 |
[Swift] Swift 빌드 과정 (2) | 2024.09.08 |
[Swift] Value Type과 Reference Type (4) | 2024.09.01 |