안녕하세요! 피피아노입니다.
이번 포스팅은 저번 포스팅에 이어서 동시성(Concurrency)에 대해서 마저 정리해보겠습니다.
저번 포스팅이 궁금한 분들은 여기를 참고해주세요!
비동기 시퀀스(Asynchronous Sequences)
비동기 시퀀스(asynchronous sequence)는 비동기적으로 요소를 하나씩 기다리면서 처리할 수 있는 강력한 도구입니다. Swift의 AsyncSequence 프로토콜을 사용하여 이러한 비동기 시퀀스를 구현할 수 있습니다. 비동기 시퀀스를 사용하면 일반적인 배열이나 컬렉션과 달리, 요소를 순차적으로 비동기적으로 처리할 수 있습니다.
이전 포스팅에서 listPhotos(inGallery:) 함수는 비동기적으로 배열의 모든 요소가 준비된 후에 전체 배열을 한번에 반환합니다. 하지만 비동기 시퀀스를 이용하면 한번에 컬렉션의 한 요소를 기다리게 됩니다.
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
일반적인 for-in 루프 대신에 위의 예제는 for 다음에 await 를 작성합니다. 비동기 함수 또는 메서드 호출할 때와 마찬가지로 await 작성은 가능한 중단 지점을 나타냅니다. for-await-in 루프는 다음 요소를 사용할 수 있을 때까지 기다리고 각 반복이 시작될 때 잠재적으로 실행을 일시 중단합니다.
Sequence 프로토콜에 준수성을 추가하여 for-in 루프에서 자체 타입을 사용할 수 있는 것과 같은 방식으로 AsyncSequence 프로토콜에 준수성을 추가하여 for-await-in 루프에서 자체 타입을 사용할 수 있습니다.
작업과 작업 그룹(Tasks and Task Groups)
작업(tsak)은 프로그램의 일부로 비동기적으로 실행할 수 있는 작업 단위입니다. 모든 비동기 코드들은 어떠한 task의 일부로 실행되게 됩니다.
task는 한번에 하나의 task만 수행하지만, 여러 task을 생성하면, Swift는 동시에 수행하기 위해 task를 스케쥴링 할 수 있습니다.
async-let 구문은 암시적으로 하위 작업을 생성합니다. 이 구문은 프로그램에서 수행할 task가 미리 정해져 있을 때 유용합니다. 하지만, 작업 그룹(task group)을 생성하면 우선순위와 취소를 더 잘 제어할 수 있으며, 동적으로 task의 수를 생성할 수 있습니다. task group은 child task를 명시적으로 추가할 수 있도록 해줍니다.
task는 계층 구조로 정렬됩니다. task group의 각 task에는 동일한 parent task가 있으며, 각 task에는 child task가 있을 수 있습니다. 이러한 구조적 동시성(structured concurrency) 덕분에 task와 task group 간의 명시적인 관계를 유지할 수 있습니다. 부모(parent)-자식(child) 관계는 여러 이점을 제공합니다:
- parent task은 child task이 완료될 때까지 기다릴 수 있습니다.
- child task이 더 높은 우선순위로 설정되면, parent task의 우선순위도 자동으로 높아집니다.
- parent task이 취소되면, 각 child task도 자동으로 취소됩니다.
- 작업-로컬 값(Task-local value)은 child task에 효율적이고 자동으로 전파됩니다.
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)
}
}
작업 취소 (Task Cancellation)
Swift 동시성은 협동 취소 모델(cooperative cancellation model)을 사용합니다. 각 작업은 실행 중에 적절할 때 취소여부를 확인하고, 적절하게 취소에 응답합니다. 작업의 종류에 따라 취소에 대한 응답은 다음 중 하나에 해당합니다.
- CancellationError 와 같은 에러 발생
- nil 또는 빈 콜렉션 반환
- 부분적으로 완료된 작업 반환
task가 취소되면 Task.checkCancellation() 타입 메서드를 호출하거나, Task.isCancelld 타입 프로퍼티 값을 확인하고 코드에서 직접 해제 처리를 합니다.
구조화되지 않은 동시성(Unstructured Concurrency)
구조화되지 않은 동시성(Unstructured Concurrency)은 비동기 작업을 독립적으로 생성하고 실행할 수 있는 방법을 제공합니다. task group의 일부인 task와 달리 구조화되지 않은 작업(unstructured task)에는 parent task가 없습니다. 프로그램이 필요로 하는 방식으로 구조화되지 않은 작업를 관리할 수 있는 완전한 유연성이 있지만, 정확성에 대한 완전한 책임도 있습니다.
현재 액터(actor)에서 실행되는 구조화되지 않은 작업를 생성하려면 Task.init(priority:operation:) 초기화 구문을 호출해야 합니다. 더 구체적으로 분리된 작업으로 알려진 현재 액터의 일부가 아닌 구조화되지 않은 작업을 생성하려면Task.detached(priority:operation:)클래스 메서드를 호출합니다. 이 모든 동작은 서로 상호작용 할 수 있는 task를 반환합니다.
예를 들어 결과를 기다리거나 취소하는 경우가 해당됩니다.
let newPhoto = // ... some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value
액터(Actors)
actor는 동시성 코드간에 정보를 안전하게 공유할 수 있게 해줍니다.
클래스와 마찬가지로 actor는 참조 타입이므로, 참조 타입의 특징이 actor에게도 적용됩니다.
클래스와 다르게 actor는 한 번에 하나의 작업만 변경 가능한 상태에 접근할 수 있도록 허용하므로 여러 작업의 코드가 actor의 동일한 인스턴스와 상호작용하는 것을 안전하게 만들어줍니다.
actor TemperatureLogger {
let label: String
var measurements: [Int]
private(set) var max: Int
init(label: String, measurement: Int) {
self.label = label
self.measurements = [measurement]
self.max = measurement
}
}
actor 키워드를 사용하여 actor를 도입하고 중괄호로 정의합니다. TemperatureLogger actor는 actor 외부의 다른 코드가 접근할 수 있는 프로퍼티가 있으며 actor 내부의 코드만 최대값을 업데이트 할 수 있게 max 프로퍼티를 제한합니다.
구조체와 클래스와 같은 초기화 구문으로 actor의 인스턴스를 생성합니다. actor의 프로퍼티 또는 메서드에 접근할 때 일시 중단 지점을 나타내기 위해 await 를 사용합니다.
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
이 예제에서 logger.max 에 접근하는 것은 일시 중단 지점으로 가능합니다. acotor는 한 번에 하나의 작업만 변경 가능한 상태에 접근할 수 있도록 허용하므로 다른 작업의 코드가 이미 logger와 상호 작용하고 있는 경우 이 코드는 프로퍼티 접근을 기다리는 동안 일시 중단됩니다.
반면에 acotor의 일부인 코드는 acotor의 프로퍼티에 접근할 때 await를 작성하지 않습니다.
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
이 메서드는 actor 내부에서 실행되므로, actor의 상태에 접근할 때 await를 사용할 필요가 없습니다. 이는 actor가 내부적으로 한 번에 하나의 작업만 상태를 변경할 수 있도록 보장하기 때문입니다.
그러나, 메서드 실행 도중 actor의 상태는 일시적으로 일치하지 않을 수 있습니다. 예를 들어, 새로운 측정값을 추가한 후 최대 온도를 업데이트하기 전에 measurements 배열은 새로운 값이 포함된 상태일 수 있지만, max 값은 아직 업데이트되지 않았을 수 있습니다. 이러한 상황에서 여러 작업이 동일한 actor 인스턴스와 상호작용할 수 없도록 제한함으로써, 일시적인 불일치 상태를 방지할 수 있습니다.
이런 방식으로 actor는 상태를 안전하게 관리하고, 동시성 문제를 최소화하며 데이터의 일관성을 유지합니다. actor는 이러한 상태 변경의 일관성을 보장하여, 여러 작업이 동시에 actor 상태를 변경하는 것을 방지합니다.
전송 가능 타입(Sendable Types)
Swift에서는 프로그램을 동시성 안전하게 실행하기 위해 작업 (Tasks)과 액터 (Actors)를 사용합니다. 작업이나 액터의 인스턴스 내에서 변수와 프로퍼티와 같은 변경 가능한 상태를 포함하는 부분을 동시성 도메인 (Concurrency Domain)이라고 부릅니다. 어떤 데이터는 데이터가 변경 가능한 상태를 포함하지만 동시 접근에 대해 보호되지 않으므로 동시성 도메인 간에 공유될 수 없습니다.
전송 가능 타입 (Sendable Type)은 한 동시성 도메인에서 다른 동시성 도메인으로 안전하게 공유될 수 있는 타입을 의미합니다. 이러한 타입은 액터의 메서드 인수나 작업의 결과로 전달될 수 있습니다.
전송 가능 타입을 정의하기 위해서는 Sendable 프로토콜을 사용합니다. 이 프로토콜은 명시적인 코드 요구사항은 없지만, Swift에서는 다음과 같은 의미론적 요구사항을 적용합니다:
- 값 타입과 전송 가능 데이터: 값 타입 (예: 구조체, 열거형)이고, 그 내부의 변경 가능한 상태가 전송 가능한 다른 데이터로 구성되어 있을 때. 예를 들어, 전송 가능한 저장 프로퍼티를 가진 구조체는 안전합니다.
- 불변 상태의 타입: 변경 가능한 상태가 없으며, 상태가 변경 불가능한 다른 전송 가능한 데이터로만 구성된 경우. 예를 들어, 읽기 전용 프로퍼티만 있는 구조체 또는 클래스는 전송 가능할 수 있습니다.
- 안정성 보장 타입: @MainActor로 표시된 클래스나 특정 쓰레드나 큐에서 프로퍼티에 순차적으로 접근하는 클래스와 같이, 상태 변경의 안정성을 보장하는 코드가 있는 경우. 이러한 타입은 동시성 도메인 간의 안전한 전송을 보장합니다.
struct TemperatureReading: Sendable {
var measurement: Int
}
let reading = TemperatureReading(measurement: 25)
위 예제에서 TemperatureReading 구조체는 전송 가능한 프로퍼티만 가지고 있으며, Sendable 프로토콜을 채택하여 다른 동시성 도메인 간에 안전하게 전달될 수 있습니다.
반대로, 전송 불가능한 타입의 예를 보면
class FileDescriptor {
let rawValue: Int
}
@available(*, unavailable)
extension FileDescriptor: Sendable { }
FileDescriptor는 Sendable로 표시되지 않으므로, 동시성 도메인 간에 안전하게 전달될 수 없습니다.
이러한 기준을 통해, Swift에서는 동시성 도메인 간의 데이터 전송 시 안전성을 보장할 수 있습니다.
오늘은 여기까지 :)
감사합니다.
잘못된 내용이 있거나 더 좋은 내용 피드백은 언제나 환영합니다!
궁금하신 부분은 댓글로 질문 부탁드립니다!
'Apple > Swift' 카테고리의 다른 글
[Swift] Actor 이해하기 (1/2) (27) | 2024.11.04 |
---|---|
[Swift] URL 살펴보기 (0) | 2024.08.07 |
[Swift] 동시성(Concurrency) 톺아보기 (1/2) (0) | 2024.07.23 |
[Swift] 고차함수(Higher-order function) 이해하기 (2) | 2024.06.27 |
[Swift] Implicitly Unwrapped Optional (암묵적 옵셔널 추출)과 Optional Chaining (옵셔널 체이닝) (0) | 2024.04.11 |