-
ConcurrencyiOS/Swift 공식문서 2022. 2. 23. 13:23
Concurrency
Swift는 비동기/병렬 코드를 built-in 방식으로 지원한다. 비동기 코드는 일시정지했다가 나중에 실행될 수 있지만 한 번의 프로그램의 한 부분만 실행된다. 이러한 방식으로 데이터를 네트워크에서 가져오거나 파일을 parsing하는 등의 오래 걸리는 작업을 계속하는 동안 UI 업데이트와 같은 짧은 시간동안의 작업을 계속 진행할 수 있다.
병렬 코드는 여러 조각의 코드가 동시에 실행되는 것을 말한다. 예를 들어, 4코어 프로세서는 동시에 4개의 코드를 실행할 수 있다. 병렬 코드와 비동기 코드를 사용하는 프로그램은 한 번에 여러 작업을 수행할 수 있다. 이러한 프로그램은 외부 시스템을 기다려야 하는 작업을 일시 정지하고, memory-safe한 방식으로 코드를 작성할 수 있게 한다.
비동기/병렬 코드로 인한 추가적인 스케줄링은 복잡성을 증가시킨다. Swift는 일부 compile-time checking이 가능한 방식으로 개발자의 의도를 표현할 수 있게 한다. 예를 들어, mutable한 상태에 안전하게 접근할 수 있게 하기 위해 actor를 사용할 수 있다.
그러나, 느리고 버그가 있는 코드에 동시성을 추가한다고 해서 코드가 빠르고 정확히 동작할 것이라는 보장은 없다. 오히려 문제가 더 심각해질 수 있다. 그러나, 동시성에 대한 Swift의 언어 수준의 지원은 Swift가 compile time에 문제를 파악할 수 있게 한다.
앞으로는 "동시성"이라는 단어를 비동기/병렬 코드의 common combination을 지징하는 의미로 사용할 것이다.
note
Swift의 동시성 모델은 스레드를 기반으로 만들어졌지만, 개발자는 스레드와 직접적으로 상호작용하지 않는다. Swift의 비동기 함수는 실행 중인 스레드를 포기할 수 있다. 그러면 첫번째 함수가 block되는 동안 해당 스레드의 다른 비동기 함수가 실행되게 된다.
Swift의 언어적 지원 없이 동시성 코드를 작성할 수도 있지만, 그 경우 가독성이 떨어지는 경향이 있다. 다음 코드는 사진 이름들의 리스트를 다운로드하고, 리스트의 첫번째 사진을 다운로드하고, 해당 사진을 사용자에게 보여주는 코드이다.
listPhotos(inGallery: "Summer Vacation") { photoNames in let sortedNames = photoNames.sorted() let name = sortedNames[0] downloadPhoto(named: name) { photo in show(photo) } }
이런 단순한 케이스조차도, 코드가 일련의 completion handler로 작성되어야 하기 때문에 여러 중첩 클로젿들을 작성하게 된다.
Defining and Calling Asynchronous Functions
비동기 함수/메서드는 실행 중에도 일시정지될 수 있는 함수/메서드이다. 이는 끝까지 실행되거나, 에러를 던지거나, 반환되지 않는 일반적인 동기 함수/메서드와 반대이다. 비동기 함수는 이 3가지 중 하나를 할 수 있지만, 도중에 일시정지할 수도 있다는 점이 다르다. 비동기 함수/메서드 내부에서 개발자는 일시정지될 수 있는 지점을 표시한다.
비동기라는 것을 나타내기 위해, 매개변수들 뒤에
async
키워드를 작성해야 한다.func listPhotos(inGallery name: String) async -> [String] { let result = // ... some asynchronous networking code ... return result }
에러 던지기도 해야 하는 경우
throw
앞에async
를 적는다.
비동기 메서드를 호출하면, 해당 메서드가 반환될 때까지 실행이 중단된다. 그리고 중간에 일시정지할 수 있는 지점을 표시하기 위해 비동기 함수를 호출할 때 앞에
await
로 표시해준다. 비동기 메서드 안에서, 실행 흐름은 오직 다른 비동기 메서드가 호출되었을 때에만 일시정지된다. 일시정지는 절대 암시적이거나 선점적(preemptive)이지 않다. 이는 모든 일시정지 포인트가await
로 표시된다는 것을 의미한다.
다음 코드의 실행 과정을 살펴보자.
let photoNames = await listPhotos(inGallery: "Summer Vacation") let sortedNames = photoNames.sorted() let name = sortedNames[0] let photo = await downloadPhoto(named: name) show(photo)
a. 코드가 첫번째 줄부터 실행되면서 첫번째
await
까지 실행된다.listPhotos(inGallery:)
함수를 호출하고, 해당 함수가 반환될 때까지 실행이 중지된다.
b. 위의 코드의 실행이 일시정지된 동안, 같은 프로그램의 다른 동시성 코드가 실행된다. 예를 들어, 새로운 사진 갤러리를 업데이트 하는 등 오래 걸리는 백그라운드 작업을 수행할 수 있다.
c.
listPhotos(inGallery:)
가 반환된 이후, 실행을 계속한다.photoNames
에 반환값을 할당한다. 2, 3번째 줄은await
가 없으므로 일시정지가 가능한 포인트가 없고, 동기적 코드이다.
d. 다음
await
는downloadPhtoo(named:)
함수에 대한 호출을 표시한다. 위의 코드는 해당 함수가 반환될 때까지 실행을 멈춘다. 그리고 다른 동시성 코드가 실행될 기회를 얻는다.
e.
downloadPhtoo(named:)
함수가 반환된 후, 반환 값이photo
에 할당되고,show(_:)
의 매개변수로 들어간다.
실행중인 코드가
await
에 도달하면 실행을 멈추고 비동기 함수/메서드가 반환되기를 기다리는 것을 스레드 양보(yielding the thread)라고 한다. 내부적으로는, 현재 스레드에서 해당 코드를 실행하는 것을 일시중지하고, 다른 코드를 실행하는 것이다.await
가 있는 코드는 실행을 일시정지할 수 있어야 하기 때문에, 프로그램에서 특정 지점만 비동기 함수/메서드를 호출할 수 있는 것이다.
note
Task.sleep(nanoseconds:)
메서드는 동시성 동작 방식을 배우기 위해 간단한 코드를 작성할 때 유용하다. 주어진 시간동안 아무 동작도 하지 않고 반환된다.func listPhotos(inGallery name: String) async throws -> [String] { try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // Two seconds return ["IMG001", "IMG99", "IMG0404"] }
Asynchronous Sequences
위의
listPhotos(inGallery:)
함수는 모든 element가 준비된 후, 배열 전체를 비동기적으로 한 번에 반환한다. 다른 접근법이 있는데, asynchronous sequence를 사용하여 각 element가 하나씩 준비되기를 기다리는 것이다.for-in
문에서for
뒤에await
를 쓰면, 함수/메서드를 호출할 때처럼 일시정지 포인트를 나타낼 수 있다.for-await-in
문은 각 iteration을 시작할 때마다 실행을 일시정지하고, 다음 element가 준비될 때까지 기다린다.
for-in
문을Sequence
프로토콜을 따르는 타입에 사용하는 것처럼,for-await-in
문은AsyncSequence
프로토콜을 따르는 타입에 사용할 수 있다.
Calling Asynchronous Functions in Parallel
비동기 함수를
await
로 호출하면 한 시점에 하나의 코드 조각만 실행된다. 비동기 코드가 실행되는 동안, 비동기 함수를 호출한 코드는 다음 줄을 실행하기 전에 해당 코드가 끝나기를 기다린다. 비동기 함수를 3번 호출하는 다음 코드를 살펴보자.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(named:)
의 각 호출은 한 시점에 하나만 실행된다는 것이다. 각 사진이 완전히 다운로드되고 나서야 다른 사진을 다운로드할 수 있는 것이다.
비동기 함수를 호출하고 주변 코드와 병렬적으로 실행하기 위해서는, 상수 선언 앞에
async
를 적어야 한다.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)
두 접근접의 차이점은 다음과 같다.
a.
await
는 다음 줄의 코드가 함수 호출 결과에 따라 달라지는 경우 사용하자. 이는 작업이 순차적으로 진행되게 한다.
b. 함수 호출 결과가 다음 코드에서 필요하지 않은 경우async-let
을 사용하자. 이는 작업이 병렬적으로 진행되게 한다.
c.await
와async
모두 일시 중단되는 동안 다른 코드가 실행될 수 있게 한다.
d. 두 경우 모두 비동기 메서드가 반환될 때까지 일시정지를 해야 할 경우 해당 지점을await
로 표시한다.
Tasks and Task Groups
task는 프로그램의 일부로서 비동기적으로 실행될 수 있는 작업의 단위이다. 모든 비동기 코드는 어떤 task의 일부로서 실행된다. 위에서 다룬
async-let
문법도 child task를 생성해준다. task group을 새로 만들어서 그 안에 child task들을 추가하는 것도 가능하다. 이러한 방식을 통해 우선순위와 취소에 대한 컨트롤을 할 수 있고, 동적인 개수의 task들을 생성할 수 있다.
task들은 계층 구조로 정렬된다. task group의 모든 task는 같은 부모 task를 가지게 되며, 각 task는 자식 task를 가질 수 있다. 이러한 task와 task group의 명시적 관계 때문에, 이러한 접근법을 구조화된 동시성(structured concurrency)이라고 한다. 이러한 task들 간의 부모-자식 관계는 Swift가 취소 전파와 같은 동작들을 처리할 수 있게 한다. 그리고 Swift가 컴파일 시점에 에러를 감지할 수 있게 한다.
await withTaskGroup(of: Data.self) { taskGroup in let photoNames = await listPhotos(inGallery: "Summer Vacation") for name in photoNames { taskGroup.addTask { await downloadPhoto(named: name) } } }
Unstructured Concurrency
Swift는 구조화되지 않은 동시성도 지원한다. task gruop의 일부인 task들과는 달리, unstructured task는 부모 task가 없다. 프로그램에게 필요한 방식으로 unstructured task들을 완전히 유연하게 관리할 수 있지만, 정확성에 대한 책임은 개발자에게 있다. 현재 실행중인 actor에서 unstructured task를 생성하려면
Task.init(priority:operation:)
을 호출해야 한다. 현재 actor에 속하지 않은 unstructured task를 생성하려면Task.detached(priority:operation:)
메서드를 호출해야 한다. 두 메서드 모두 해당 task와 상호작용할 수 있게 하는 task handle을 반환한다.let newPhoto = // ... some photo data ... let handle = Task { return await add(newPhoto, toGalleryNamed: "Spring Adventures") } let result = await handle.value
Task Cancellation
Swift의 동시성은 협력 취소 모델(cooperative cancellation model)을 사용한다. 각 task는 실행의 적절한 지점에서 취소되었는지를 확인하고, 적절한 방식으로 취소에 응답한다. 수행 중인 작업에 따라, 다음 중 하나를 의미한다.
a.
CancellationError
와 같은 에러를 던지는 것
b.nil
이나 빈 컬렉션을 반환하는 것
c. 일부만 완료된 작업을 반환하는 것
취소 여부를 확인하기 위해
Task.checkCancellation()
을 호출하거나,Task.isCancelled
의 값을 확인할 수 있다. 전자의 경우 task가 취소된 경우CancellationError
를 던진다. 취소를 manual하게 전파하기 위해서는Task.cancel()
을 사용하자.
Actors
actor는 클래스처럼 reference type이지만, 클래스와는 달리 한 번에 하나의 task만
actor
의 mutable state에 접근할 수 있게 한다. 이 덕분에 여러 task와 안전하게 상호작용할 수 있다.
다음 예시를 살펴보자. 온도를 기록하는
TemperatureLogger
를 정의한다.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의 프로퍼티나 메서드에 접근하는 경우, 잠재적인 일시정지 지점을 표시하기 위해
await
를 사용한다. 아래 코드에서,logger.max
가 일시정지 지점이다. 하나의 task만 actor의 mutable state에 접근할 수 있기 때문에, 다른 task의 코드가 이미 actor와 상호작용하고 있으면, 코드는 프로퍼티에 접근하기 위해 대기한다.let logger = TemperatureLogger(label: "Outdoors", measurement: 25) print(await logger.max) // Prints "25"
반면, actor에서의 코드는 프로퍼티에 접근할 때
await
를 사용하지 않는다. 예를 들어, 다음 코드와 같이TemperatureLogger
를 새로운 온도로 업데이트하는 메서드가 있다고 하자.extension TemperatureLogger { func update(with measurement: Int) { measurements.append(measurement) if measurement > max { max = measurement } } }
update(with:)
메서드는 이미 actor 안에 있기 때문에max
와 같은 프로퍼티에 접근할 때await
로 표시하지 않는다. 이 메서드에서 한 번에 하나의 task만 mutable state에 들어갈 수 있는 이유를 알 수 있다. actor의 상태에 대한 일부 업데이트의 경우 일시적으로 불변성을 깨뜨린다.update
메서드에서measurement
에 append를 한 이후이며max
를 업데이트하기 이전인 시점의 경우, 해당 actor는 일시적으로 일관성이 없는 상태이다. 업데이트 도중에 새로운 측정이 추가되면 logger 데이터가 일치하지 않게 되는 문제가 발생한다.
이를 방지하기 위해 Swift의 actor를 사용할 수 있다. actor는 한 시점에 하나의 task만 허용하고, 해당 코드는
await
가 일시정지 지점을 표시한 곳에서만 인터럽트될 수 있기 때문이다.update(with:)
에서는 일시정지 지점이 없기 때문에, 업데이트 도중에 다른 코드가 접근할 수 없다.
await
없이 접근하려고 하면 다음과 같이 컴파일 에러가 발생한다. actor의 프로퍼티는 actor의 독립 상태의 일부이기 때문이다. Swift는 actor 내부의 코드만 actor의 로컬 상태에 접근 할 수 있도록 보장한다. 이러한 보장을 actor isolation이라고 한다.print(logger.max) // Error
Reference
https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html
'iOS > Swift 공식문서' 카테고리의 다른 글
Nested Types (0) 2022.02.23 Type Casting (0) 2022.02.23 Error Handling (0) 2022.02.22 Optional Chaining (0) 2022.02.22 Deinitialization (0) 2022.02.21