Concurrency
스위프트는 구조화된 방식의 비동기적이고 병렬 수행되는 코드 작성에 대한 지원이 내장되어있다. 비동기(Asynchronuous) 코드는 일시중단 되었다가 나중에 재개할 수 있지만, 한번에 프로그램의 한 부분만 실행된다. 프로그램에서 코드를 중단하고 재개하면 네트워크에서 데이터를 불러오거나, 파일 파싱과 같은 긴 시간이 걸리는 작업을 계속 진행하면서 UI 업데이트와 같은 짧은 시간이 걸리는 작업을 계속할 수 있다. 병렬 코드(Parallel code)는 동시에 실행되는 코드들을 의미한다. —예를 들어 4코어 프로세서를 가진 컴퓨터는 각각의 코어가 코드 하나씩을 실행하게 하여, 네 개의 코드를 동시에 실행할 수 있다. 병렬과 비동기 코드를 사용하는 프로그램은 한번에 여러 작업을 수행할 수 있다; 외부 시스템을 기다리는 작업을 일시 중단하고, 메모리-세이프 방식으로 더 쉽게 작성할 수 있게 해준다.
병렬 또는 비동기 코드의 추가 스케줄링 유연성은 복잡성이 증가하는 비용이 있다. 스위프트는 일부 컴파일 타임 체킹으로 의도를 표현할 수 있게 해준다. —예를 들면, 액터를 사용하여 변경 가능한 상태(mutable state)에 안전하게 접근할 수 있다. 하지만, 너무 느리거나 버그가 있는 코드에 동시성을 추가한다고 해서 그 코드가 빨라지거나, 정확하게 동작한다는 보장은 없다. 실제로 동시성을 추가하면 코드 디버깅이 더 힘들어질 수도있다. 하지만 동시성이 필요한 코드에 스위프트의 언어 레벨의 동시성 지원을 사용하면 컴파일 타임에 문제를 캐치하는데 도움을 줄 수 있다.
이 챕터에서 동시성은 비동기 코드와 병렬 코드의 조합을 뜻한다.
Note
동시성 코드를 이전에 작성해 봤으면 스레드와 함께 사용했을 것이다. 스위프트의 동시성 모델은 스레드 위에 빌드되지만, 직접적으로 스레드와 상호작용할 필요는 없다. 스위프트의 비동기 함수는 실행 중인 스레드를 포기할 수 있다. 첫 번째 함수가 중단되면 그 스레드의 다른 비동기 함수가 실행된다. 비동기 함수가 재개되면, 스위프트는 함수가 실행된 스레드에 어떠한 보장도 하지 않는다.
스위프트의 언어 지원이 없더라도 동시성 코드를 작성할 수 있지만, 그러한 코드들의 가독성은 나쁘다. 예를 들어 다음의 코드는 사진의 이름 리스트를 다운받고, 리스트 안의 첫 번째 사진을 다운받고, 그 사진을 유저에게 보여주는 코드이다.
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
}
이러한 간단한 케이스에도, 컴플리션 핸들러가 연속적으로 작성되어야 하므로 중첩된 클로저를 작성하게 된다. 이러한 스타일에서는 더 깊은 중첩이 있는 코드를 빠르게 다루기 어려워진다.
Defining and Calling Asynchronous Functions
비동기 함수(혹은 메소드, 이하 메소드는 생략)는 실행 도중 일시정지 할 수 있는 특별한 종류의 함수이다. 이는 완료될 때 까지 실행되거나, 에러를 발생시키거나, Never를 리턴하는(주: Never를 리턴하는 함수가 존재한다.) 일반적인 동기 함수와는 대조된다. 비동기 함수도 여전히 저 세가지중 하나를 하지만, 무언가를 기다릴 때는 잠시 정지할 수도 있다. 비동기 함수의 본문에서, 실행이 일시중단될 수 있는 부분을 지정해야 한다.
함수가 비동기 함수임을 나타내려면, throws 키워드를 작성하는 것과 비슷하게 함수의 파라미터 뒤에 async 키워드를 작성한다. 함수가 값을 리턴한다면, async의 앞에 리턴 화살표(->)를 작성한다. 예를 들어, 다음은 갤러리에서 사진들의 이름을 불러오는 코드이다.
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
비동기 함수면서, 쓰로잉 함수인 경우에는 async를 throws 앞에 작성한다(async throws).
비동기 메소드를 호출하면, 해당 메소드가 리턴할 때 까지 실행이 중단된다. 중단이 가능한 지점을 지정하기 위해 호출 앞에 await를 작성한다. 쓰로잉 함수를 호출할 때, try를 쓰는 것과 비슷하다. 비동기 메소드 안에서, 실행 플로우는 다른 비동기 메소드를 호출할 때만 중단된다. — 중단은 절대로 암시적이거나 선점적이지 않다. — 이는 중단이 가능한 모든 지점이 await로 표시되어야 한다는 것을 뜻한다.
예를 들어 아래의 코드는 갤러리 내부의 모든 사진의 이름을 불러오고, 첫 번째 사진을 보여준다.
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
listPhotos(inGallery:)와 downloadPhoto(named:) 함수 둘 다 네트워크 요청을 해야하므로, 완료될 때까지 오랜 시간이 걸릴것이다. 두 함수의 리턴 애로우 앞에 async를 작성하여 둘 다 비동기 함수로 만들면, 이 코드가 사진이 준비될 때 까지 기다리는 시간 동안, 앱의 나머지 코드가 계속 실행된다.
다음은 위 예제에서 가능한 실행 순서중 하나를 나열한 것이다.
- 코드는 첫 번째 줄에서 시작되어 첫 번째 await까지 실행된다. listPhotos(inGallery:) 함수를 실행하고, 그 함수가 리턴할 때 까지 (코드의)실행을 중단한다.
- 코드의 실행이 중단되었을 때, 프로그램 내부의 다른 동시성 코드가 실행된다. 예를 들면, 새 포토 갤러리의 리스트를 업데이트 하는것과 같은 백그라운드 작업을 한다. 그러한 코드 역시, 다음 await를 만나거나 작업을 완료할 때 까지 실행된다.
- listPhotos(inGallery:)가 리턴되면, 이 코드는 해당 지점에서 다시 재개된다. 이 경우에는 photoNames에 리턴된 값을 할당한다.
- sortedNames와 name을 정의하는 라인은 await가 없기 때문에, 일반적인 동기 코드다. 따라서 정지할 수 있는 부분이 존재하지 않는다.
- 다음 await는 downloadPhoto(named:) 함수 호출에 표시되어 있다. 이 코드는 함수가 리턴할 때 까지, 다시 한번 실행을 멈추고, 다른 동시성 코드가 실행될 기회를 준다.
- downloadPhoto(named:)가 리턴하면, 그 리턴 값은 photo에 할당되고, slow(_:)의 아규멘트로 넘어간다.
코드에서 await로 표시된 정지 가능한 지점은 비동기 함수나 메소드가 리턴할 때 까지 기다리는 동안, 현재 코드가 실행을 멈출수도 있다는 것을 나타낸다. 이것은 스레드 양보(yielding the thread)라고도 하는데, 스위프트는 현재 스레드에 있는 코드의 실행을 멈추고 그 스레드에 있는 다른 코드를 실행하기 때문이다. await가 있는 코드는 실행을 멈출 수 있어야 하기 떄문에 다음과 같은 프로그램의 특정 부분에서만 비동기 함수나 메소드를 호출할 수 있다:
- 비동기 함수, 메소드, 프로퍼티의 본문에 있는 코드
- @main으로 지정된 스트럭처, 클래스, 열거형의 static main() 메소드 내부의 코드
- 구조화 되지 않은 차일드 태스크(child task)
정지 가능한 지점 사이에 있는 코드는 다른 동시성 코드가 인터럽션 할 가능성 없이 순차적으로 실행된다. 예를 들면, 아래의 코드는 하나의 갤러리에서 다른 갤러리로 사진을 옮긴다.
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(_:toGallery:)와 remove(_:fromGallery)사이에 다른 코드를 실행할 방법은 존재하지 않는다. 그 시간 동안, 첫 번째 사진은 두 갤러리에 동시에 존재하여 일시적으로 앱의 불변성을 깨뜨린다.(주: 앱의 불변성이 깨진 불안정한 시점이기 때문에 동시성 작업이 존재해서 코드가 중단되면 안되고, 작업을 마무리 지어야 한다.) 이 코드에 await가 추가되면 안되는 것을 명확하게 나타내기 위해, 이 코드를 동기 함수로 리팩토링 할 수 있다.
func move(_ photoName: String, from source: String, to destination: String) {
add(photoName, toGallery: destination)
remove(photoName, fromGallery: source)
}
// ...
let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
move(firstPhoto, from: "Summer Vacation", to: "Road Trip")
위의 예시에서 move(_:from:to:)은 동기 함수이므로, 이 함수가 중단 가능한 지점을 절대 포함할 수 없다는 것을 보장할 수 있다. 나중에 이 함수에 동시성 코드를 추가하려 하면, 버그가 아닌 컴파일 타임 에러가 발생한다.
Note
Task.sleep(until:tolerance:clock:)메소드는 동시성을 배우기 위해 간단한 코드를 작성할 때 유용한 메소드이다. 이 메소드는 아무것도 하지 않지만, 리턴하기 전에 주어진 ns만큼 대기한다. 다음은 네트워크 작업에 걸리는 시간을 시뮬레이션 하기 위해 sleep(until:tolerance:clock:)을 사용하는 버전의 listPhotos(inGallery:)함수이다.
func listPhotos(inGallery name: String) async throws -> [String] { try await Task.sleep(until: .now + .seconds(2), clock: .continuous) return ["IMG001", "IMG99", "IMG0404"] }
원문: https://books.apple.com/kr/book/the-swift-programming-language-swift-5-7
The Swift Programming Language (Swift 5.7)
Computing & Internet · 2014
books.apple.com
'Swift > The Swift Programming Language' 카테고리의 다른 글
[Swift] Concurrency(동시성) - 3 (0) | 2023.06.26 |
---|---|
[Swift] Concurrency(동시성) - 2 (0) | 2023.06.26 |
[Swift] Error Handling(에러 처리) - 2 (0) | 2023.06.22 |
[Swift] Error Handling(에러 처리) - 1 (1) | 2023.06.21 |
[Swift] Optional Chaining(옵셔널 체이닝) - 2 (0) | 2023.06.20 |
댓글