이전글 - Concurrency(2) Getting started with async/await
Modern Concurrency in Swift 를 읽고 간단 정리
Getting to Know AsyncSequence
AsyncSequence는 비동기적으로 Element를 생성할 수 있는 시퀀스를 나타내는 프로토콜
- Swift Sequence와 동일
- 일반적인 Sequence에서는 다음 Element를 즉시 사용 가능
- AsyncSequence에서는 await 키워드를 사용하여 다음 요소가 준비될 때 까지 기다려야 한다.
for문
for try await item in asyncSequence {
// Next item for asyncSequence
}
while문
var iterator = asyncSequence.makeAsyncIterator()
while let item = try await iterator.next() {
...
}
일반 Sequence에서 사용하는 연산자 사용 가능
for try await item in asyncSequence
.dropFirst(5)
.prefix(10)
.filter(10)
.map { "Item:\($0)" } {
// Next item for asyncSequence
}
}
파일의 내용을 한 번에 모두 읽지 않고, 필요한 만큼만 순차적으로 읽기
let bytes = URL(fileURLWithPath: "myFile.txt").resourceBytes
for await character in bytes.characters {
...
}
for await line in bytes.lines {
...
}
AsyncSequences 프로토콜을 채택하여 커스텀 시퀀스 만들기
struct CountdownAsyncSequence: AsyncSequence {
typealias Element = Int
let start: Int
func makeAsyncIterator() -> CountdownAsyncIterator {
return CountdownAsyncIterator(current: start)
}
}
struct CountdownAsyncIterator: AsyncIteratorProtocol {
var current: Int
mutating func next() async -> Int? {
guard self.current > 0 else { return nil }
let result = self.current
self.current -= 1
return result
}
}
let countdown = CountdownAsyncSequence(start: 5)
for await number in countdown {
print(number)
}
AsyncStream을 활용하여 직접 커스텀 비동기 시퀀스를 만들기
let timerStream = AsyncStream<Date> { continuation in
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
continuation.yield(Date()) // 매 초마다 현재 시간 제공
}
continuation.onTermination = { _ in
timer.invalidate() // 스트림 종료 시 타이머 무효화
}
}
for await date in timerStream {
print("Current time: \(date)")
}
Cancelling Tasks
동시성 모델에서 작업을 취소하는 것은 중요하다.
- TaskGroup, async let을 사용하는 경우, 시스템이 필요할 때 작업을 취소할 수 있다.
ㄴ ex. 작업이 더 이상 필요하지 않을 때 (화면을 나가거나 다른 작업 시작)
ㄴ ex. 시스템 리소스 절약 목적
- Task API들을 사용하여 직접 task cancellation 전략을 만들 수 있다.
- 작업이 취소되었다면 true
- 현재 작업의 우선순위
- 작업 취소
- 작업이 취소 됐다면 명시적으로 CancellationError 예외 던짐
- 현재 작업을 suspend 처리하고, 다른 작업들을 처리할 기회 제공
- 이 과정에서 작업이 자동으로 취소될 가능성도 있다.
Manually Canceling Tasks
- task modfiier 안에서 비동기 작업을 진행했다면, 뷰가 disappear 될 때 자동으로 cancel 된다.
- 그렇지 않은 경우 직접 cancel 처리해줄 수 있어야 한다.
- 아래 코드는 downloadTask와 그 하위 task들을 모두 cancel 시켜주는 예시
struct MyView: some View {
@State private var downloadTask: Task<Void, Error>?
var body: some View {
DowloadInfo(downloadWithUpdatesAction: {
downloadTask = Task {
...
}
})
.onDisappaer {
downloadTask?.cancel()
}
}
}
Storing State in Tasks
- 각 비동기 task는 자신의 context를 가지며 수행된다.
- 또한 task는 다른 task를 호출할 수 있다.
- task 간에 서로 상호작용할 수 있기 때문에 데이터를 안전하게 격리(isolation)하는 것이 쉽지 않다.
이 문제를 해결하는 방법 중 하나가 task-local 이라는 프로퍼티 래퍼.
- Swift 뷰에서 하위 뷰에게 environment 를 주입하여 값을 공유하는 것과 동일한 방식
- Task와 그 자식 Task들이 공통된 고유 데이터에 접근하도록 한다.
책의 설명이 빈약해서 공식문서 참고
선언
- @TaskLocal
- static 또는 전역 프로퍼티로 선언되어야 한다.
typealias TraceID = String
enum Example {
@TaskLocal
static var traceID: TraceID?
}
// Global task local properties are supported since Swift 6.0:
@TaskLocal
var contextualNumber: Int = 12
기본 값
- TaskLocal 값이 설정 또는 바인딩되지 않았을 때 읽으려고 하면, 기본 값이 반환된다.
- 동기 컨텍스트에서도 기본 값
ㄴ 비동기 Task에서 동기 메서드를 호출하면, 동기 메서드 내에서 기본 값이 읽힌다.
ㄴ 이 컨텍스트를 일치 시키려면 withValue 바인딩 필요
enum Example {
@TaskLocal
static let traceID: TraceID = TraceID.default
}
바인딩
- TaskLocal 값은 직접 설정할 수 없다.
- 항상 스코프 내에서 바인딩 된다.
- $traceID.withValue()
- Detached task는 task-local 값을 상속받지 않지만, Task { ... } 초기화로 생성된 작업은 task-local 값을 상속받아 사용
func performTask() async {
await Example.$traceID.withValue("12345") {
print("1", Example.traceID) // "12345"
await someAsyncTask()
}
print("2", Example.traceID) // nil (스코프 종료로 바인딩 해제)
}
func someAsyncTask() async {
print("3", Example.traceID) // "12345"
}
await performTask()
// 1 Optional("12345")
// 3 Optional("12345")
// 2 nil
- 자식 task에게도 바인딩 잘 된다.
await Example.$traceID.withValue("12345") {
Task {
print(Example.traceID) // "12345"
}
}
- 비동기 작업 외부에서도 task-local 값을 바인딩하고 사용할 수 있다.
- Swift는 task-local 값을 쓰레드-로컬 저장소에 저장하므로 동기 함수에서도 일관되게 사용할 수 있다.
ㄴ 동기 함수가 비동기 Task 내에서 호출될 때, task-local 값을 통해 작업의 컨텍스트를 공유 할 수 있겠다.
ㄴ 이 함수들이 동일한 컨텍스트 정보를 필요로 할 수 있음.
func enter() {
Example.$traceID.withValue("1234") {
print(read()) // always "1234", regardless if enter() was called from inside a task or not:
}
}
func read() -> String {
if let value = Example.traceID {
"\(value)"
} else {
"<no value>"
}
}
// 1) Call `enter` from non-Task code
// e.g. synchronous main() or non-Task thread (e.g. a plain pthread)
enter()
// 2) Call 'enter' from Task
Task {
enter()
}
'iOS > Concurrency' 카테고리의 다른 글
Concurrency (5) Intermediate async/await & Checked Continuation (0) | 2024.09.01 |
---|---|
Concurrency (4) Custom Asynchronous Sequences With AsyncStream (0) | 2024.08.24 |
Concurrency (2) Getting started With async/await (0) | 2024.08.15 |
Concurrency (1) Why Modern Swift Concurrency? (0) | 2024.08.14 |
swift docs - Concurrency (1) | 2023.11.03 |