공식 문서의 번역 입니다. 빠진 내용도 조금 있으니 더 자세한 내용은 공식문서를 봐주세요 !
Swift는 구조화된 방식으로 비동기와 병렬 코드 작성을 지원 합니다. 비동기 코드는 일시적으로 중단 되었다가 다시 실행할 수 있지만 한 번에 프로그램의 한 부분만 실행합니다.
프로그램에서 코드를 일시 중단하고 다시 실행하면 UI 업데이트와 같은 짧은 작업을 계속 진행하면서 네트워크를 통해 데이터를 가져오거나 파일을 분석하는 것과 같은 긴 실행 작업을 계속 할 수 있슴니당
병렬코드는 동시에 코드의 여러 부분이 실행됨을 의미합니다. 예를 들어, 4코어 프로세서의 컴퓨터는 각 코어가 하나의 작업을 수행하므로 코드의 4부분을 동시에 실행이 가능 합니다.
스위프트의 언어 지원을 사용하지 않고 동시성 코드를 작성하는 예시로 Completion Handler가 존재합니다.
하지만 이는 코드의 가독성을 떨어뜨리는 경우가 많습니다.
listPhoto(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
}
위의 코드는 2개의 중첩 클로저를 작성하였고, 더 복잡한 코드에서는 더욱 많아 질것입니다.
비동기 함수는 실행 도중에 일시적으로 중단될 수 있는 특수한 함수입니다.
함수가 비동기임을 나타내려면 던지는 함수 (throwing function)를 나타내기 위해 thorws
를 사용하는 것과 유사하게 파라미터 뒤의 선언에 async
키워드를 작성합니다. 함수가 값을 반환한다면 반환 화살표 ->
전에 async
를 작성합니다.
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code...
return result
}
만약 throws
를 작성해야 한다면 async
뒤에 작성 해주면 됩니다.
비동기 함수를 호출할 때 해당 메서드가 반환될 때까지 실행이 일시 중단됩니다. 중단될 가능성이 있는 지점을 표시하기 위해서 비동기 함수를 호출 할 때 앞에 await
를 작성합니다.
이는 에러가 있는 경우 프로그램의 흐름을 변경 가능함을 나타내기 위해 던지는 함수(throwing function)를 호출할 때 try
를 작성하는 것과 같습니다.
비동기 함수 내에서 실행 흐름은 다른 비동기 함수를 호출할 때만 일시 중단됩니다.
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
위 코드는 갤러리에 모든 사진의 이름을 가져온 다음에 처음 사진을 보여주는 코드임니다
위에 listPhots, downloadPhoto
함수는 네트워크 요청을 필요로 하기 때문에 완료하는데 비교적 오랜 시간이 걸릴 수 있기 떄문에 비동기 함수로 만든다면 위의 코드는 그림이 준비될 때 까지 기다리는 동안 앱의 나머지 코드가 계속 실행 될 수 있습니다.
위 코드의 실행 순서는 다음과 같습니다.
코드는 첫번째 줄에서 실행을 시작하고 첫번째 await
까지 실행됩니다. listPhotos(inGallery:)
함수를 호출하고 반환될 때까지 실행을 일시 중단합니다.
이 코드의 실행이 일시 중단되는 동안 같은 프로그램의 다른 동시 코드가 실행됩니다. 예를 들어 오랜 시간 실행되는 백그라운드 작업이 새 사진 목록을 업데이트 할 수 있습니다. 이 코드는 await
로 표시된 다음 중단지점 또는 완료될 때까지 실행됩니다.
listPhotos(inGallery:)
가 반환된 후에 이 코드는 해당 지점에서 시작하여 계속 실행됩니다. 반환된 값을 photoNames
에 할당합니다.
sortedNames
와 name
을 정의하는 라인은 일반적인 동기 코드입니다. 따라서 중단 지점이 없습니다.
다음 await
는 download(named:)
함수에 대한 호출을 표시합니다. 이 코드는 해당 함수가 반환될 때까지 실행을 다시 일시 중단하여 다른 동시코드에 실행할 기회를 제공합니다.
await
로 표시된 코드의 중단이 가능한 지점은 비동기 함수가 반환되기를 기다리는 동안 현재 코드 부분이 실행을 일시적으로 중단할 수 있음을 나타냅니다. Swift가 현재 쓰레드에서 코드의 실행을 일시 중단하고, 해당 쓰레드에서 다른 코드를 실행하기 때문에 이를 쓰레드 양보(yielding the thread)라고 부릅니다.
await
가 있는 코드는 실행을 일시 중단할 수 있어야 하므로 프로그램의 특정 위치에서만 비동기 함수를 호출할 수 있습니다.
@main
으로 표시된 구조체 클래스 또는 열거형의 staic main()
메서드에 있는 코드중단이 가능한 지점 사이의 코드는 다른 비동기 코드의 중단 가능성 없이 순차적으로 실행됩니다. 아래 코드는 한 갤러리의 사진을 다른 곳으로 이동시킵니다.
let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
add(firstPhoto, toGallery: "Road Trip")
// At this point, firstPhoto is temporarily in both galleries.
remove(firstPhoto, fromGallery: "Summer Vacation")
add
와 remove
사이에 실행되는 코드는 없습니다. 그 시간동안 첫번째 사진은 양쪽 갤러리에 모두 나타나게 됩니다. 이 코드에서는 await
가 추가되지 말아야 함을 나타내기 위해 동기 함수로 리팩토링이 가능합니다.
func move(_ photoName: String, from source: String, to destination: String) {
add(photoName, to: destination)
remove(photoName, from: source)
}
// ...
let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
move(firstPhoto, from: "Summer Vacation", to: "Road Trip")
위의 예제에서 move
함수는 동기 함수기 떄문에 중단 가능한 지점을 포함하지 않는다는 것을 보장할 수 있습니다. 이러한 동기 함수 내부에 중단 가능한 지점을 도입하기위해 비동기 코드를 추가하면 컴파일 에러가 뜹니다.
위의 예제에서 listPhotos(inGallery:)
함수는 비동기적으로 배열의 모든 요소가 준비된 후에 전체 배열을 한번에 반환합니다.
다른 접근방식으로 비동기 시퀀스를 사용하여 한번에 컬렉션의 하나의 요소를 기다리는 것입니다. 이에 대한 조회 동작은 다음과 같습니다
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
일반적인 반복문에 await
를 추가하여서 다음 요소를 사용할 수 있을 때까지 기다리고 각 반복이 시작될 때 잠재적으로 실행을 일시 중단합니다.
await
를 사용하여 비동기 함수를 호출하면 한번에 코드의 한 부분만 실행됩니다. 비동기 코드가 실행되는 동안 호출자는 코드의 다음 라인을 실행하기 위해서 이동하기 전에 해당 코드가 완료될 때까지 기다립니다.
다음 예제를 보겠습니당
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
이 방식에는 단점이 있습니다. 다운로드가 비동기이고 진행되는 동안 다른 작업을 수행할 수 있지만 downloadPhoto
에 대한 호출은 한 번에 하나만 실행됩니다.
각 사진은 개별적으로 또는 동시에 여러개를 다운로드 할 수 있기 떄문에 이렇게 기다리는 것은 불필요 합니다.
따라서 비동기 함수를 호출하고 주변 코드와 병렬로 실행하려면 상수를 정의할 때 맨 앞에 async
를 작성합니다. 상수를 사용하려면 await
를 붙여주시면 됩니다.
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)
이 예제에서는 이전처럼 기다리지 않고 동시에 시작됩니다. photos
가 있는 라인까지 실행이 됩니다. 이 시점에서 프로그램은 이러한 비동기 호출의 결과를 필요로 하므로 세 장의 사진이 모두 다운로드 될 떄까지 실행을 일시 중단하기 위해서 await
을 작성합니다.
이러한 접근 방식에서 생각할 수 있는 방법들입니다.
await
를 사용하여 비동기 함수를 호출합니다. 이것은 순차적으로 실행되는 작업을 생성합니다.async-let
을 사용하여 비동기 함수를 호출합니다. 이는 병렬로 수행할 수 있는 작업이 생성됩니다.await
와 async-let
은 모두 일시 중단되는 동안 다른 코드를 실행할 수 있도록 합니다await
로 표시합니다.Task는 프로그램의 일부로 비동기적으로 실행할 수 있는 작업 단위입니다. 모오오오든 비동기 코드는 어떠한 작업의 일부로 실행됩니다.
작업 그룹을 생성하고 해당 그룹에 하위 작업을 추가할 수도 있습니다. 그렇게 하면 우선순위와 취소를 더 잘 제어할수 있으며 동적으로 작업의 수를 생성할 수 있습니다.
작업은 계층구조로 정렬됩니다. 작업 그룹의 각 작업에는 동일한 상위 작업이 있으며 각 작업에는 하위 작업이 있을 수도 있습니다. 작업과 작업 그룹 간의 명시적 관계 때문에 이 접근 방식을 구조적 동시성(Structured Concurrency) 라고 합니다.
정확성에 대한 일부 책임을 가지고 있지만, 작업간의 명시적 부모 자식 관계를 통해 Swift는 취소 전파와 같은 일부 동작을 처리할 수 있고 Swift는 컴파일 시간에 일부 오류를 감지할 수 있습니다.
// taskgroup을 만드는 withTaskGroup 함수
await withTaskGroup(of: Data.self) { taskGroup in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
taskGroup.addTask { await downloadPhoto(named: name) }
}
}
이전에 설명한 동시성에 대한 구조화된 접근 방식 외에도 Swift는 구조화되지 않은 동시성을 지원합니다.
Task Group
의 일부인 작업과 달리 구조화되지 않은 작업에는 상위 Task
가 존재하지 않습니다. 프로그램이 필요로 하는 방식으로 구조화되지 않는 작업을 관리할 수 있는 완전한 유연성이 있지만 정확성에 대한 완전한 책임도 있습니다.
현재 행위자(actor) 에서 실행되는 구조화되지 않은 작업을 생성하려면 Task.init(priority:oepration:)
초기화 구문을 호출해야 합니다.
더 구체적으로 분리된 작업으로 알려진, 현재 행위자의 일부가 아닌 구조화되지 않은 작업을 생성하려면 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
Swift 동시성은 협동 취소 모델(cooperative cancellation model)을 사용합니다. 각 작업은 실행의 적절한 시점에서 취소되었는지 확인하고 적절한 방법으로 취소에 응답합니다. 수행중인 작업에 따라 일반적으로 다음 중 하나를 의미 합니다.
CancellationError
와 같은 에러 발생취소를 확인하려면 작업이 취소된 경우 CancellationError
를 발생시키는 Task.checkCancellation()
을 호출하거나 Task.isCancelled
의 값을 확인하고 자체 코드에서 취소를 처리 합니다.
취소를 수동으로 전파(알린다는 의미로 받아들이면 될 듯)하려면 Task.cancel()
을 호출해야 합니다.
프로그램을 동시성 조각으로 분리하기 위해 작업(Task)을 사용할 수 있습니다. 작업은 서로 분리되어 있어 같은시간에 안전하게 실행될 수 있지만 작업 간에 일부 정보를 공유해야 할 수도 있습니다. 행위자(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
}
}
위 코드는 온도를 기록하는 행위자를 정의한 코드 입니다. 이 코드를 사용하는 예제를 보겠습니다.
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
위 예제 처럼 일시 중단 지점을 나타내기 위해 await
를 사용합니다.
행위자는 한 번에 하나의 작업만 변경 가능한 상태에 접근할 수 있도록 허용하므로 다른 작업의 코드가 이미 logger
와 상호작용 하고 있는 경우 이 코드는 프로퍼티 접근을 기다리는 동안 일시 중단됩니다.
행위자 내부 코드에서는 프로퍼티에 접근할 때 await
를 작성하지 않습니다.
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
위 메소드는 행위자 내부에서 이미 실행중이므로, max
같은 프로퍼티에다가 await
를 작성하지 않습니다.
이러한 행위자를 사용해서 코드를 작성하는것에 장점으로는, 만약 동일한 인스턴스에 여러 작업들이 상호 작용 한다면 값의 불변성이 깨지는 경우가 생길 수 있습니다.
이럴때 actors
를 사용하여 방지할 수 있습니다. (행위자는 한 번에 해당 상태에 대해 하나의 작업만 혀용하고, 해당 코드는 await
가 일시 중단 지점으로 표시되는 위치에서만 중단될 수 있기 때문에)
클래스의 인스턴스와 같이 행위자의 외부에서 프로퍼티에 접근하려고 하면 컴파일 에러가 발생합니다.
print(logger.max) // error
await
작성 없이 logger.max
에 접근하는 것은 행위자의 프로퍼티가 해당 행위자의 분리된 로컬 상태의 부분이기 때문에 실패합니다. Swift는 행위자 내부의 코드만 행위자의 로컬 상태에 접근할 수 있도록 보장합니다. 이를 행위자 분리(actor isolation)이라고 합니다.
Task
와 actors
는 프로그램을 동시에 안전하게 실행할 수 있는 조각으로 나눌 수 있습니다.
작업 또는 행위자의 인스턴스 내에서 변수와 프로퍼티와 같은 변경 가능한 상태를 포함하는 프로그램의 일부분을 동시성 도메인(concurrency domain)이라고 부릅니다. 어떤 데이터는 데이터가 변경 가능한 상태를 포함하지만 동시 접근에 대해 보호되지 않으므로 동시성 도메인 간에 공유될 수 없습니다.
한 동시성 도메인에서 다른 동시성 도메인으로 공유될 수 있는 타입을 전송 가능 타입(sendable type)이라고 합니다.
예를 들어, 행위자 메서드로 호출될 때 인자로 전달되거나 작업의 결과로 반환될 수 있습니다.
Sendable
프로토콜을 선언하여 전송 가능한 타입으로 표시합니다. 이 프로토콜은 아무런 코드 요구사항은 없지만 의미론적 요구사항이 존재합니다. 일반저긍로 타입을 전송 가능한 것으로 나타내기 위한 세가지 방법이 존재합니다.
@MainActor
로 표시된 클래스나 특정 쓰레드나 큐에서 프로퍼티에 순차적으로 접근하는 클래스와 같이 변경 가능한 상태의 안정성을 보장하는 코드를 가지고 있습니다.비동기 함수를 갑자기 실행할 수는 없고 Task를 init해서 내부에서 작성해주시믄 됨니다.
private extension StarwarsPeopleListViewModel {
func fetchStarwarsPeople() {
Task {
do {
// async 함수인 getPeople
people = try await APIClient().getPeople()
} catch NetworkError.invalidURL {
print("Invalid URL ERROR!")
}
}
}
}
위 코드처럼 클래스 내부에 함수에 Task
를 넣고 내부에 비동기 함수를 자식으로 넣어주면 사용이 가능하다.
참조
https://github.com/unnnyong/AsyncAwaitSample/pull/1/files
https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html