Swift6가 나오면서 컴파일 단계에서 동시성 문제를 강하게 잡더라구요.
그래서 더 이상 Swift Concurrency 공부를 미룰 수 없겠다 싶어 차근차근 학습해보려고 합니다.
오늘은 Task에 대해서 공식문서와 함께 알아볼거에요.
Task 공식 문서: https://developer.apple.com/documentation/swift/task
Task 란?
비동기적인 작업의 단위(A unit of asyncchronous work) 입니다.
@frozen
struct Task<Success, Failure> where Success : Sendable, Failure : Error
Overview 개요
작업(Task)는 생성 직후에 바로 실행될 수 있으며, 명시적으로 시작하거나 예약할 필요가 없습니다.
Tasks can start running immediately after creation; you don’t explicitly start or schedule them.
🤔 Task.실행() 과 같이 명시적으로 실행하지 않아도 Task 를 생성하면 바로 작업이 실행된다는 것 같습니다.
작업을 생성한 후에는 인스턴스를 사용하여 해당 작업과 상호작용합니다. 예를 들어, 작업이 완료될 때까지 기다리거나 취소할 수 있습니다.
After creating a task, you use the instance to interact with it — for example, to wait for it to complete or to cancel it. It’s not a programming error to discard a reference to a task without waiting for that task to finish or canceling it.
🤔 인스턴스를 생성해 변수에 담아서 해당 task와 상호작용하면 된다는 것 같네요
작업이 완료되거나 취소될 때까지 기다리지 않고 작업에 대해 참조를 버리는 것은(discard)은
프로그래밍 오류가 아닙니다.
It’s not a programming error to discard a reference to a task without waiting for that task to finish or canceling it.
작업은 참조 유지 여부와 관계없이 실행됩니다. 그러나 작업에 대한 참조를 버리면(discard) 해당 작업의 결과를 기다리거나 작업을 취소할 수 있는 기능을 포기하게 됩니다.
A task runs regardless of whether you keep a reference to it. However, if you discard the reference to a task, you give up the ability to wait for that task’s result or cancel the task.
🤔 중요한 대목인 것 같습니다. 해당 task의 참조가 사라져도 작업은 계속 실행된다네요.
분리된 작업(detached task)이거나 하위 작업(child task)일 수 있는 현재 작업(current task) 에 대한 명령(operations)을 지원하기 위해, Task는 yield()
와 같은 클래스 메소드도 제공합니다. 이러한 메소드는 비동기적이므로 항상 기존 작업(existing task)의 일부로 호출됩니다.
To support operations on the current task, which can be either a detached task or child task, Task also exposes class methods like yield(). Because these methods are asynchronous, they’re always invoked as part of an existing task.
🤔 yield()는 명시적으로 스레드 점유권을 OS에게 넘겨주는 코드입니다. 나머지 내용들은 무슨 뜻일까요..?
작업의 일부(part of the task)로 실행되는 코드만 해당 작업(task)과 상호작용할 수 있습니다.
현재 작업(current task)와 상호작용하려면, Task의 static 메소드 중 하나를 호출해야 합니다.
Only code that’s running as part of the task can interact with that task. To interact with the current task, you call one of the static methods on Task.
🤔 current task는 따로 변수에 담겨있지 않을테니 참조를 모를테고 그래서 Task의 static 메소드로 상호작용해야한다는거겠죠?
작업의 실행(tasks execution)은 작업이 실행된(where the task ran) 일련의 기간(series of period)으로 볼 수 있습니다.
각 기간은 일시 중단 시점(suspension point) 또는 작업 완료(completion)시 종료됩니다.
A task’s execution can be seen as a series of periods where the task ran. Each such period ends at a suspension point or the completion of the task.
🤔 이게 무슨 말일까요.. 흠
이러한 실행 기간은 PartialAsyncTask의 인스턴스로 표시됩니다. 사용자 정의 실행자(custom executor)를 구현하지 않는 한, 부분 작업(partial tasks)과 직접 상호 작용하지 않습니다.
These periods of execution are represented by instances of PartialAsyncTask. Unless you’re implementing a custom executor, you don’t directly interact with partial tasks.
🤔 PartialAsyncTask를 찾아보니 UnownedJob의 typealias 던데, Task 내 부분 작업에 대해 직접 상호작용할 수 없다는 뜻일까요?
Task Cancellation 작업 취소
작업(Task)은 취소를 가리키기 위한 공유된 메커니즘(shared mechanism)이 포함되어 있지만, 취소를 처리하는 방법에 대해 공유된 구현(shared implementation)은 포함되어 있지 않습니다.
Tasks include a shared mechanism for indicating cancellation, but not a shared implementation for how to handle cancellation.
작업에서(in the task) 수행 중인 작업(work)에 따라 해당 작업을 중단하는 올바른 방법이 다릅니다.
Depending on the work you’re doing in the task, the correct way to stop that work varies.
마찬가지로, 작업의 일부로 실행되는 코드는 중단이 적절할 때마다 취소를 확인하는 책임이 있습니다.
Likewise, it’s the responsibility of the code running as part of the task to check for cancellation whenever stopping is appropriate.
여러 부분(multiple pieces)이 포함된 장기 작업(long-task)에서는 여러 지점에서 취소를 확인하고 각 지점에서 취소를 다르게 처리해야 할 수도 있습니다.
In a long-task that includes multiple pieces, you might need to check for cancellation at several points, and handle cancellation differently at each point.
🤔 위의 4 문장을 이해해보면, task 안에 여러 개의 work가 있을 수 있는데 work 마다 올바른 중단 방법이 다르니 취소에 대한 구현은 직접해야한다는 뜻으로 들리네요.
작업(work)을 중단하기 위해 오류를 발생시키기만 하면 되는 경우, 취소를 확인하기 위해 Task.checkCancellation() 함수를 호출합니다.
취소에 대한 다른 응답에는 지금까지 완료된 작업을 반환(returning the work), 빈 결과 반환(returning an empty result) 또는 nil 반환(returning nil)이 포함됩니다.
If you only need to throw an error to stop the work, call the Task.checkCancellation() function to check for cancellation. Other responses to cancellation include returning the work completed so far, returning an empty result, or returning nil.
취소는 순전히 Boolean 상태입니다. 취소 이유와 같은 추가 정보를 포함할 방법은 없습니다.
이는 여러 가지 이유로 작업이 취소될 수 있으며 취소 프로세스 중에 추가 이유가 발생할 수 있다는 사실을 반영합니다.
Cancellation is a purely Boolean state; there’s no way to include additional information like the reason for cancellation. This reflects the fact that a task can be canceled for many reasons, and additional reasons can accrue during the cancellation process.
🤔 위의 두 문장은 취소하는 방법에 대한 내용인 것 같습니다.
Task closure lifetime 작업 클로저의 수명
작업(Task)은 주어진 작업(given task)에 의해 실행될 코드가 포함된 클로저를 전달함으로써 초기화됩니다.
Tasks are initialized by passing a closure containing the code that will be executed by a given task.
이 코드가 완료(run to completion)된 후, 작업(Task)은 실패(failure) 또는 결과 값(result value)으로 완료되며, 이 클로저는 즉시 해제됩니다.
After this code has run to completion, the task has completed, resulting in either a failure or result value, this closure is eagerly released.
작업 객체(task object)를 유지한다고 해서 클로저가 무기한 유지(indefinitely retain)되는 것은 아닙니다. 작업(Task)이 완료된 후 작업(Task)이 보유한 모든 참조가 해제되기 때문입니다. 따라서 작업(Task)은 드물게 약한 참조를 캡쳐할 필요가 있습니다.(tasks rearely need to capture weak references to values)
Retaining a task object doesn’t indefinitely retain the closure, because any references that a task holds are released after the task completes. Consequently, tasks rarely need to capture weak references to values.
예를 들어, 다음 코드 조각에서는 액터를 약한 것으로 캡처할 필요가 없습니다. 작업이 완료되면 액터 참조를 놓아 태스크와 이를 보유한 액터 사이의 참조 순환이 깨지기 때문입니다.
For example, in the following snippet of code it is not necessary to capture the actor as weak, because as the task completes it’ll let go of the actor reference, breaking the reference cycle between the Task and the actor holding it.
struct Work: Sendable {}
actor Worker {
var work: Task<Void, Never>?
var result: Work?
deinit {
// 작업(task)이 여전히 유지되고 있더라도,
// 그것이 완료되면 액터와 참조 사이클을 유발하지 않음
print("deinit actor")
}
func start() {
work = Task {
print("start task work")
try? await Task.sleep(for: .seconds(3))
self.result = Work() // self 캡쳐
print("completed task work")
// 작업(task)이 끝나면 참조가 해제됨
}
// 작업(task)에 대한 강한 참조를 유지함
}
}
이렇게 사용하면 됩니다.
await Worker().start()
액터는 start() 메서드의 self 사용에 의해서만 유지되며 구조화되지 않은 작업이 완료될 때까지 기다리지 않고 start 메서드가 즉시 반환된다는 점에 유의하세요. 작업이 완료되고 해당 클로저가 삭제되면 액터에 대한 강력한 참조도 해제되어 액터가 예상대로 초기화를 해제할 수 있습니다.
액터는 start() 메서드의 self 로서만 참조가 유지되며, start 메소드는 구조화되지 않은 작업(Task)이 완료될 때까지 기다리지 않고 즉시 리턴합니다.
작업(Task)이 완료(completed)되고 해당 클로저가 파괴(destroyed)되면, 액터에 대한 강한 참조도 해제되어 액터가 예상대로 해제될 수 있습니다.
Note that the actor is only retained by the start() method’s use of self, and that the start method immediately returns, without waiting for the unstructured Task to finish. Once the task is completed and its closure is destroyed, the strong reference to the actor is also released allowing the actor to deinitialize as expected.
따라서 위 호출의 결과는 일관되게 다음과 같이 출력됩니다.
Therefore, the above call will consistently result in the following output:
start task work
completed task work
deinit actor
🤔 task 블럭 내의 self 는 강한참조가 발생해 작업(task)이 완료되기 전까진 self가 deinit되지 않습니다. 그래서 필요한 경우 weak self를 사용할 수 있는데, 자세한 내용은 task 내에서 weak self를 붙여야 하는 이유 이 내용을 보면 참고가 될 것 같습니다 !
'iOS' 카테고리의 다른 글
네이버 클라우드 VPC 서버에 연결이 되지 않는 문제 (0) | 2024.11.03 |
---|---|
Swift Concurrency - Sendable (2) | 2024.10.25 |
Swift Task 내에서 weak self를 붙여야 하는가 (4) | 2024.10.20 |
SwiftUI 프로젝트에서 AppDelegate, SceneDelegate 사용하기 (0) | 2024.10.12 |
SwiftLint SPM으로 설치하기 (2) | 2024.10.12 |