Concurrency
async
- 함수 정의 뒷부분에 async를 붙이면 해당 함수는 비동기라는 것을 나타냄
- async 함수는 concurrent context 내부 즉, 다른 async 함수 내부 혹은 Task 내부에서 사용 가능
func listPhotos(inGallery name: String) async -> [String] {
let result = await asyncFunction()
return result
}
- 함수뿐만이 아니라, 프로퍼티나 생성자에도 async 사용 가능
- 예를들어, read-only 프로퍼티는 async 사용 가능
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}
await
- async 함수를 호출하기 위해 await가 필요하고, 이는 potential suspension point로 지정 된다는 것을 의미
- Suspend 된다는 것은 해당 thread에 대한 control을 포기한다는 것
- 해당 코드를 돌리고 있던 thread에 대한 control은 system에게 가고, system은 해당 thread를 사용하여 다른 작업 가능
print("비동기 함수 호출 전")
await asyncFunction()
- await로 인한 중단은 해당 thread에서 다른 코드의 실행을 막지는 않음
- function은 suspend 되고, 다른 것들이 먼저 실행 될 수 있고 그렇기에 그 동안 앱의 상태가 크게 변할 수 있음
- thread를 차단하는 대신 control을 포기해 작업을 중지 및 재개할 수 있는 개념을 도입
- 정리하자면, await로 async 함수를 호출하는 순간 해당 thread control 포기
- 따라서 async 작업 및 같은 블럭에 있는 다음 코드들을 바로 실행하지 못함
- thread control을 system에게 넘기면서, system에게 해당 async 작업도 schedule
- system은 다른 중요한 작업이 있다면 먼저 실행하고, 특정 thread control을 줘서 async 함수와 나머지 코드를 resume
print("비동기 함수 호출 전")
await asyncFunction()
print("비동기 함수 호출 후")
for await id in staticImageIDsURL.lines {
let thumbnail = await fetchThumbnail(for: id)
collage.add(thumbnail)
}
let result = await collage.draw()
Task
SwiftUI task에서 비동기 작업하기
- Task는 비동기 작업 단위: A unit of asynchronous work
- 격리되어(isolated), 독립적으로(independently) 비동기 작업을 수행
- 값이 공유될 상황이 있을때는 Sendable 체킹을 통해 Task가 격리된 상태로 남아있는지 체크
print("1")
Task {
print("2")
}
print("3")
- Task 안에서의 작업은 처음부터 끝까지 순차적으로 실행
- await를 만나면 작업은 몇번이고 중단될 수는 있지만, 실행 순서가 변경되지는 않음
Task {
let fish = await catchFish()
let dinner = await cook(fish)
await eat(dinner)
}
- Task cancellation을 확인하는 두가지 방법
- Task는 cancel 되고 즉시 멈추지 않음
- Cancellation은 어디서나 체크할수 있음
try Task.checkCancellation()
if Task.isCancelled { }
- 아래 코드는 unstructured Task를 딕셔너리에 담고 있다가, 취소하는 코드
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = Task {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
display(thumbnails, in: cell)
}
}
func collectionView(_ view: UICollectionView, didEndDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
thumbnailTasks[item]?.cancel()
}
}
- Detach Task를 통해 별도로 백그라운드에서 Task를 실행하는 코드
- 캐싱은 Main Thread에서 할 필요가 없고, 다른 썸네일이 fetch에 실패해도 캐싱 안할 이유가 없음
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = Task {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
Task.detached(priority: .background) {
writeToLocalCache(thumbnails)
}
display(thumbnails, in: cell)
}
}
}
- Data Race: 여러 Task가 동시에 일을 하고 있지만 동일한 객체(Class)를 참조하기 때문에 발생
- Sendable 프로토콜: 동시에 사용해도 안전한 타입 (Actor)
Concurrency +
async let
- 아래와 같은 코드는 한 번에 하나의 비동기 코드만 실행
- 비동기 코드가 실행되는 동안, 호출자는 다음 코드를 실행하기 전에 해당 코드가 완료될 때까지 기다림
func fetchOneThumbnail(withID id: String) async throws -> UIImage {
let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
let (data, _) = try await URLSession.shared.data(for: imageReq)
let (metadata, _) = try await URLSession.shared.data(for: metadataReq)
guard let size = parseSize(from: metadata),
let image = UIImage(data: data)?.byPreparingThumbnail(ofSize: size) else {
throw ThumbnailFailedError()
}
return image
}
- 비동기 함수를 호출하고 주변 코드와 병렬로 실행 하는것도 가능
- 정의할 때 let 앞에 async를 작성한 다음 상수를 사용할때 await를 사용
func fetchOneThumbnail(withID id: String) async throws -> UIImage {
let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
async let (data, _) = URLSession.shared.data(for: imageReq)
async let (metadata, _) = URLSession.shared.data(for: metadataReq)
guard let size = parseSize(from: try await metadata),
let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size) else {
throw ThumbnailFailedError()
}
return image
}
- async let을 사용하면 실제 함수는 바로 실행 되는것을 알 수 있음
- 하지만 값을 사용할때까지 기다리지는 않음
func execute() async {
async let one = sleepOne()
async let two = sleepTwo()
print("await: ", await one)
print("await: ", await two)
}
func sleepOne() async -> Int {
try? await Task.sleep(for: .seconds(1))
print("sleepOne completed")
return 1
}
func sleepTwo() async -> Int {
try? await Task.sleep(for: .seconds(2))
print("sleepTwo completed")
return 2
}
withTaskGroup
- TaskGroup으로 여러 병렬 작업을 합쳐 모든 작업이 완료되면 결과를 반환 받을 수 있음
let images = await withTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
let photoURLs = await loadPhotoUrls()
for photoURL in photoURLs {
taskGroup.addTask { await downloadPhoto(url: photoURL) }
}
var images = [UIImage]()
for await result in taskGroup {
images.append(result)
}
return images
}
AsyncStream
- AsyncStream은 순서가 있고, 비동기적으로 생성된 요소들의 sequence
- yield로 스트림에 Element를 제공
- finish로 정상적으로 스트림을 종료
let digits = AsyncStream<Int> { continuation in
for digit in 1...10 {
continuation.yield(digit)
}
continuation.finish()
}
for await digit in digits {
print(digit)
}
- Throw가 가능한 AsyncThrowingStream도 존재
let digits = AsyncThrowingStream<Int, Error> { continuation in
for digit in 1...10 {
continuation.yield(digit)
}
continuation.finish(throwing: error)
}
do {
for try await digit in digits {
print(digit)
}
} catch {
}
continuation.onTermination = { termination in
switch termination {
case .finished:
print("finished")
case .cancelled:
print("cancelled")
}
}
withCheckedContinuation
Continuation
은 비동기 호출 이후에 일어나는 코드- 클로저 기반의 completion handler 비동기 처리를 async/await 형태로 바꿀때 자주 사용
withCheckedContinuation
, withCheckedThrowingContinuation
- 위와 같이 클로저 비동기 처리를 async로 변경 가능
- continue에서 두 번 이상 resume을 호출하면 안됨
- resume을 호출하지 않아도 Task가 무기한 일시 중단된 상태로 유지
- 즉, resume은 정확히 한번 호출되어야 함
func fetch(completion: @escaping ([String]) -> Void) {
let url = URL(string: "https://www.hohyeonmoon.com")!
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data {
if let something = try? JSONDecoder().decode([String].self, from: data) {
completion(something)
return
}
}
completion([])
}.resume()
}
func fetch() async -> [String] {
await withCheckedContinuation { continuation in
fetch { something in
continuation.resume(returning: something)
}
}
}
let something = await fetch()
- Delegate 패턴에서 async를 사용할때도
withCheckedContinuation
을 사용할수 있음
class ViewController: UIViewController {
private var activeContinuation: CheckedContinuation<[Post], Error>?
func sharedPostsFromPeer() async throws -> [Post] {
try await withCheckedThrowingContinuation { continuation in
self.activeContinuation = continuation
self.peerManager.syncSharedPosts()
}
}
}
extension ViewController: PeerSyncDelegate {
func peerManager(_ manager: PeerManager, received posts: [Post]) {
self.activeContinuation?.resume(returning: posts)
self.activeContinuation = nil
}
func peerManager(_ manager: PeerManager, hadError error: Error) {
self.activeContinuation?.resume(throwing: error)
self.activeContinuation = nil
}
}
Actor
Actor
- 공유 데이터에 접근해야 하는 여러 Task를 조정
- 외부로부터 데이터를 격리하고, 한 번에 하나의 Task만 내부 상태를 조작하도록 허용
- 동시 변경으로 인한 Data Race를 피함
- Actor의 목적이 shared mutable state를 표현하는 것이기 때문에 class와 같은 reference 타입
actor SharedWallet {
let name = "공유 지갑"
var amount = 0
init(amount: Int) {
self.amount = amount
}
func spendMoney(ammount: Int) {
self.amount -= ammount
}
}
Task {
let wallet = SharedWallet(amount: 10000)
let name = wallet.name
let amount = await wallet.amount
await wallet.spendMoney(ammount: 100)
await wallet.amount += 100
}
- 1: 상수는 변경 불가능하기 때문에 어느 스레드에서 접근해도 안전하고 actor 외부에서도 바로 접근 가능
- 2: actor 외부에서 변수 접근시 await 필요
- 3: actor 외부에서 메서드 호출시 await 필요
- 4: 컴파일 에러, actor 외부에서 actor 내부의 변수를 변경할 수 없음
MainActor
- MainActor: main thread를 나타내는 특별한 global actor
- await이 사용되었다는 것은, 해당 thread에서 다른 코드가 실행될 수 있도록 실행 중인 함수가 중지될 수 있다는 의미
await MainActor.run {
}
await MainActor.run {
}
- 따라서 메인 스레드에서 한꺼번에 작업이 이뤄지길 원하는 경우에는 관련 함수를 run block에 그룹화 해야함
- 그래야 해당 함수들 사이에는 일시 중단 없이 호출이 실행되도록 할 수 있음
await MainActor.run {
}
- Main Thread에서 실행되어야 하는 코드를 @MainActor 표시로 함수, 클로저, 타입 등에 적용 가능
- class, struct, enum 같은 type에 붙으면, 내부에 있는 모든 property와 method가 isolated 되고 main thread에서 동작
actor SomeActor {
let id = UUID().uuidString
@MainActor var myProperty: String
init(_ myProperty: String) {
self.myProperty = myProperty
}
@MainActor func changeMyProperty(to newValue: String) {
self.myProperty = newValue
}
func changePropertyToName() {
Task { @MainActor in
myProperty = "hohyeon"
}
}
}
참고