티스토리 뷰
동시성 (Concurrency)
Swift는 구조화된 방식으로 비동기 및 병렬 코드 작성을 지원합니다.
비동기 코드(asynchronous code) 는 일시정지되거나 재개 될 수 있지만, 한 번에 프로그램의 한 부분만 실행됩니다.
병렬 코드(parallel code) 는 여러 코드조각들을 동시에 실행하는것을 말합니다.
예를들어, 4개의 코어 프로세서를 가진 컴퓨터가 각 코어마다 하나의 task(코드)를 맡아, 동시에 4개의 코드조각을 실행 할 수 있습니다.
병렬 및 비동기 코드를 사용하는 프로그램은 한번에 여러 연산을 실시하며, 외부의 시스템을 기다리는 연산은 잠시 멈추고 메모리 안전한(memory-safe) 방식으로 작성하기 쉽게 만듭니다.
추가적인 스케줄링 유연함에서 병렬 또는 비동기식 코드에는 복잡도 증가도 따릅니다.
Swift는 개발자의 의도를 표현할 수 있도록 컴파일 시간 체크를 활성화 합니다.
예를들어, actors를 사용하여 변경 가능한 상태(mutable state)에 안전하게 접근할 수 있습니다.
사실, 동시성을(concurrency) 추가하면 코드를 더 복잡하고, 디버그하기 어렵게 만들 수 도 있지만, 동시성이 필요한 코드에 Swift의 언어 레벨이 동시성을 사용한다는 건 Swift가 컴파일 시간에 문제를 잡아내도록 도와줄 수 있습니다.
아래부터는 동시성 = 비동기 + 병렬 로 봐주시면 되겠습니다.
Swift의 언어지원을 사용하지 않고 동시 코드를 작성하는것도 가능은하지만, 가독성이 떨어집니다.
아래는 사진들의 리스트를 다운로드 -> 리스트의 첫번째 사진 다운로드 -> 사용자에게 사진을 보여주는 예제입니다.
listPhotos(inGallery: "Summer Vaction") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in show(photo) }
}
위와 같은 단순한 경우에도, 연속적인 completion handlers로 코드를 작성해야 하기 때문에, 클로저가 중첩되어 코드가 복잡해지며 다루기 어려워질 수 있습니다.
비동기 함수 정의와 호출 (Defining and Calling Asynchronous Functions)
비동기함수 또는 비동기 메소드는 어느정도 실행중일 때 일시정지 시킬 수 있는 특별한 함수, 메소드 입니다.
완료할때까지 작동되거나, 에러를 던지거나, 반환하지 않거나 하는 일반적인 동기 함수,메소드와는 대조 됩니다.
비동기 함수, 메소드도 위의 세 가지를 하지만, 기다리는 도중 일시정지를 할 수 있습니다.
함수나, 메소드가 비동기라는걸 나타내려면, 파라미터()뒤에 async 키워드를 사용합니다. Error handling 때 했던 throws 표시할 때와 유사한 방식입니다.
값을 반환하는 함수나 메소드면, async를 반환 화살표 (->) 앞에 작성합니다.
아래는 갤러리에서 사진 이름을 가져올 때 사용할 수 있는 예시 입니다.
func listPhotos(inGallery name: String) async -> [String] {
let result = // ==== 비동기 네트워크 코드 ====
return result
}
비동기와 던지는것 (throwing)까지 하는 함수나 메소드는 throws 앞에 async를 써줍니다.
비동기 메소드를 호출할때, 메소드가 반환될때까지 실행이 잠시 중단됩니다.
호출 하는곳 앞에 await 키워드를 써서 멈춤 가능 지점을 표시해줍니다. 이는, throwing 함수를 호출할때 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 키워드를 써서 둘다 비동기로 만듦으로써 사진이 준비될때까지 기다리는동안 나머지 앱의 코드들이 계속해서 작동되게 합니다.
동시성을 이해하기 위해 위 예제의 실행 순서는 이렇습니다.
// 1번 설명 let photoNames = await listPhotos(inGallery: "Summer Vacation")
1. (await) listPhotos(inGallery:) 함수가 호출되고 함수가 리턴될때까지 기다리는동안 실행이 잠시 중단됩니다.
2. 위 코드가 잠시 중단된 동안, 같은 프로그램에 있는 다른 동시성 코드는 작동 합니다.
ㄴ> (질문) 다른동시성 코드란? async, await이 붙은건가 ?
예를들어, 오래걸리는 백그라운드 작업이 새로운 사진 갤러리들의 리스트를 계속 업데이트할 수 있습니다.
그 코드도 await로 표시된 코드 또한 다음 일시 중단 지점까지 또는 완료할 때 까지 실행합니다.
3. listPhotos(inGallery:) 가 반환된 후, 이 코드는 해당 지점에서 시작하여 계속 실행되고, 반환 됐던 값은 photoNames에 할당합니다.// 4번 설명 let sortedNames = photoNames.sorted() let name = sortedNames[0]
4. sortedNames 과 name 으로 정의한 줄(line)은 동기적 코드 입니다. 해당 줄에는 await로 표시된 것이 없기 때문에, 가능한 중지 지점이 없습니다.// 5번 설명 let photo = await downloadPhoto(named: name)
5. 다음 await 는 downloadPhoto(named:) 함수에 있습니다. 이 코드도 함수가 반환될 때까지 잠시 중단되며, 다른 동시성 코드가 실행할 기회를 줍니다.// 6번 설명 show(photo)
6. downloadPhoto(named:) 가 반환된 후, photo에 반환된 값이 할당되고 show(_:) 를 호출할때 인자값으로 넘겨줍니다.
Swift가 현재 thread에서 코드 실행을 일시중지 하는 대신 해당 thread에서 다른코드를 실행하는걸 'yielding the thread' Thread 넘겨주기 라고 합니다.
await이 있는 코드는 실행을 잠시 멈출 수 있어야 하기 때문에, 함수나 메소드는 프로그램의 정해진 곳에서만 호출할 수 있습니다.
- 비동기 함수, 메소드, 속성의 본문 안에 있는 코드
- @main 으로 표시한 구조체, 클래스, 열거형의 정적 main() 메소드 안에 있는 코드
Task.sleep(nanoseconds:) 메소드는 단순한 코드를 작성하여 동시성 동작을 파악하는데 유용합니다.
하지만 반환되기전 까지 nanoseconds의 시간 만큼 기다려야 합니다.
다음은 가상으로 네트워크 작업 대기를 알아보기 위해 sleep(nanoseconds:)를 사용하는 listPhotos(inGallery:) 함수 입니다.
func listPhotos(inGallery name: String) async throws -> [String] {
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2초
return ["IMG001", "IMG99", "IMG0404"]
}
비동기 시퀀스 (Asynchronous Sequences)
이전 listPhotos(inGallery:) 함수에서 모든 배열의 요소가 준비된 후, 한번에 전체 배열을 비동기적으로 반환합니다.
또 다른 접근 방법은 비동기 시퀀스를 사용해서 콜렉션의 한 요소를 한번에 하나씩 기다리는겁니다.
다음은 비동기 시퀀스가 반복되는것을 보여줍니다.
import Foundation
let handle = FileHandle.withStandardInput
for try await line in handle.bytes.lines { // < 어떤건지 모르겠다..
print(line)
}
일반적인 for-in 반복문을 사용하는 대신, 위의 예제 처럼 for문 이후에 await을 썼습니다.
비동기함수, 메소드를 호출할때와 같이 await을 사용하여 일시중단 지점을 나타냅니다.
for-await-in 반복문은 다음 요소가 사용가능 해질때까지 기다릴때, 각 반복의 시작 부분에서 임시적으로 실행이 중단 될수 있습니다.
자신만의 타입을 for-in 문에서 사용하려면 Sequence 프로토콜을 준수하면 되는것과 같이,
for-await-in 문에서 사용하려면 AsyncSequence 프로토콜을 준수하면 됩니다.
비동기 함수를 병렬로 호출 (Calling Asynchronous Functions in Parallel)
await로 비동기 함수를 호출하면 한번에 코드의 한부분만 실행 됩니다.
비동기 코드가 실행 되는 동안, 호출부분은 다음 코드로 넘어가기 전 코드가 끝나길 기다립니다.
예를들어, 갤러리에서 처음 세개의 사진을 가져오기 위해, 아래처럼 세 개의 downloadPhoto(named:) 함수호출을 기다릴 수도 있을 겁니다.
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:) 함수를 한번에 한번씩만 호출한다는 것입니다. 각 사진을 다음 다운로드가 시작되기전에 완벽하게 다운로드 합니다. (대충 동기적으로, 순서대로 다운받는다는 얘기)
이걸 여러개 다운로드 할때 동시에 다운받게 하려면 (비동기적으로 다운받게 하려면), let (상수) 앞에 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)
위 예제에서, 세개의 downloadPhotos(named:) 호출이 이전 함수가 완료되기까지 기다리지 않고 시작합니다.
만약 충분한 시스템 자원이 있다면, 동시에 시작할 수 있습니다.
여기서 함수호출 부분에 await를 표시하지 않은 이유는 await를 쓰면 함수의 결과를 기다리기 위해 코드를 잠시 멈추기 때문!
대신, photos에 await을 사용해 세 장의 사진이 모두 내려받아 질때까지 코드를 일시 중지합니다.
위의 두 접근법의 차이점은 아래와 같습니다.
- await로 비동기함수를 호출하면 코드는 함수의 결과에 의존하게 됩니다. (순차적으로 작업 생성)
- async-let 으로 비동기함수를 호출하면 결과가 필요하지 않습니다. (병렬로 수행할수 있는 작업 생성)
- await과 async-let 은 코드가 일시중지되는동안, 다른 코드가 작동되게 해줍니다.
- 두 가지 경우 모두 일시 중단 지점을 await로 표시하여, 필요하면 비동기 함수가 반환할 때까지 실행이 중지될거라는걸 나타내줍니다.
Tasks and Task Groups
task(작업)은 프로그램에서 비동기적으로 작동하게해주는 작업 단위 입니다.
모든 비동기 코드는 작업의 일부분으로서 작동합니다.
앞에서봤던 async-let 구문에서는 자식 task을 생성합니다. task group을 생성해 해당 group에 자식task를 추가할 수도 있으며, 이렇게 하면 우선순위와 취소 작업을 좀더 정교하게 컨트롤할 수 있고, 동적 수의 작업을 생성할 수 있습니다.
Q. (동적수의 작업 생성..?)
작업들은 계층구조로 나열됩니다. task group에 있는 각각의 작업은 같은 부모task를 갖고 있으며, 각 작업은 자식task들을 가질 수 있습니다. task와 task group간의 명시적 관계 때문에, 이 접근방식을 구조적 동시성(structred concurrency) 이라고 합니다.
개발자가 정확성에 대한 책임이 있지만, 작업간에 명시적인 부모-자식 관계로 Swift는 취소 전파(propagating) 같은 일부 동작을 처리하고, 컴파일 타임에 에러를 감지합니다.
await withTaskGroup(of: Data.self) { taskGroup in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
taskGroup.addTask {
await downloadPhoto(named: name)
}
}
}
구조화되지 않은 동시성 (Unstructed Concurrency)
Swift에선 구조화되지않은 동시성도 지원합니다. task group의 일부인 작업들과 다르게, 구조화되지않은 작업은 부모task가 없습니다. 프로그램이 필요로하는 방식으로 구조화되지 않은 작업을 관리할 수 있는 유연함은 있지만, 정확성에 대해서 온전하게 책임져야 합니다.
현재 actor에 구조화되지 않은 작업을 생성하기 위해서, Task.init(priority:operation:) 초기자를 호출합니다.
현재 actor의 일부가 아닌 구조화되지 않은 작업을 생성하기 위해선, '떼어내는 작업'으로 잘 알려진 Task.detached(priority:operation:) 클래스 메소드를 호출합니다.
두 연산자 모두 작업과 상호작용을하는 작업핸들을 반환합니다.
예를들어 아래와 같이 결과를 기다리거나 취소합니다.
let newPhoto = // ...some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value
작업 취소 (Task Cancellation)
스위프트 동시성은 협동적인 취소 모델을 사용합니다. 각 작업은 실행시 적절한 시점에 취소된상태인지 아닌지 체크하고, 적절한 방법으로 취소에 응답 합니다.
작업하는거에 따라, 대개 의미하는바는 아래와 같습니다.
- CancellationError 와 같은 에러를 던집니다
- nil 또는 빈 콜렉션을 반환 합니다.
- 부분적으로 완료된 작업을 반환 합니다.
취소가 된걸 확인하기 위해선, 만약 작업이 취소가 됐다면 CancellationError를 던지는 Task.checkCancellation()을 호출하거나, Task.isCancelled 의 값을 체크하고, 자신의 코드에 취소 처리를 합니다.
예를들어, 갤러리에서 사진을 부분 다운로드를 삭제하고 네트워크 연결을 닫아야 할 수 있습니다.
행위자(Actors)
class와 같이 actors는 참조 타입 이며, 값 타입과 참조 타입의 비교는 class 뿐만 아니라 actors에도 적용 됩니다.
하지만 actors는 한번에 한개의 작업만 변경가능한 상태에 접근할 수 있습니다.
아래는 온도를 기록하는 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를 소개한다음, 중괄호{ }로 정의 합니다.
actor The TemperatureLogger 는 actor외부의 코드가 접근할 수 있는 프로퍼티들을 갖고 있고, max 프로퍼티를 actor내부의 코드만 최대값을 업데이트 할수 있도록 제한 합니다.
actor의 인스턴스 생성은 struct와 class와 동일한 초기자 구문을 사용합니다.
actor의 프로퍼티 또는 메소드에 접근할 때, await을 사용해 잠재적인 일시 중단점을 표시합니다.
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max) // 안찍힘..
// 25
logger.max에 접근이 일시중단가능 지점입니다. actor가 자신의 변경 가능 상태에 한번에 한 작업의 접근만 허용하기 때문에, 다른 작업 코드가 이미 logger와 상호작용중이면, 프로퍼티 접근을 기다리는동안 이 코드는 일시중단됩니다.
이와 대조적으로, actor의 일부(내부) 코드에서 actor의 프로퍼티에 접근할 때 await를 사용하지 않습니다.
예를들어, 새로운 온도로 TemperatureLogger 를 업데이트 하는 메소드가 있습니다.
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
update(with:) 메소드는 이미 actor에서 동작하고 있으므로, max 같은 프로퍼티 접근에 await 표시를 하지 않습니다.
이 메소드는 actor가 자신의 변경 가능 상태와 상호작용 할때 한번에 한 작업만 허용한다는 이유: actor 상태에 대한 일부 업데이트는 잠정적으로 불변함을 깨뜨리는걸 보여줍니다.
actor TemperatureLogger 는 온도 목록과 최대 온도를 계속 추적하고, 새 측정 값을 기록할 때 최대 온도를 갱신 합니다. 업데이트 중간에 새 측정값을 추가한 후 max값을 업데이트 하기 전에, temperature logger가 일시적으로 일치하지 않는 상태에 있습니다. 동시에 여러 작업이 동일한 인스턴스와 상호 작용하는걸 막으면 아래의 문제들을 방지해 줍니다.
- 코드가 update(with:) 메소드를 호출합니다. 첫번째로 measurements 배열을 업데이트 합니다.
- 코드가 max를 업데이트 하기전에, 다른 곳의 코드가 최대값과 temperatures의 배열을 읽을수 있습니다.
- max 값을 변경하여 업데이트를 종료합니다.
이 경우, 다른 곳에서 실행중인 코드는 데이터가 일시적으로 유효하지 않은 동안 updata(with:) 호출 중간에 actor에 대한 접근이 끼어들기 때문에 잘못된 정보를 읽을 수 있는데, Swift actor를 사용하면 이 문제를 방지할 수 있습니다.
update(with:)엔 어떤 일시중단지점도 없기 때문에, 업데이트 중간에 자료에 접근할 수 있는 코드는 없습니다.
클래스 인스턴스에서 하는것 처럼, 이러한 프로퍼티를 actor 밖에서 접근하려고 하면, 컴파일 타임 에러가 나오며
예를들면 아래와 같습니다.
print(logger.max) // 에러
actor의 속성이 해당 actor의 격리된 지역상태의 일부이기 때문에 await 작성 없이 logger.max 에 접근 하면 실패하게 됩니다. Swift는 actor 내부의 코드만 actor의 지역상태에 접근할 수 있도록 보장합니다. (actor isolation)
'iOS' 카테고리의 다른 글
[iOS] APNs (Apple Push Notification service) (0) | 2022.06.10 |
---|---|
[Swift] 타입 캐스팅 (Type Casting) (0) | 2022.06.08 |
[iOS] Swift Memory - Copy On Write (COW) (0) | 2022.06.03 |
[Swift] 에러처리 (Error Handling) (0) | 2022.06.01 |
[iOS] 앱 업데이트 (0) | 2022.05.29 |
- Total
- Today
- Yesterday
- Combine: Asynchronous Programming with Swift
- swift programmers
- Swift 알고리즘
- Class
- 원티드 프리온보딩
- Swift joined
- Swift Error Handling
- Swift 내림차순
- swift protocol
- Swift final
- RTCCameraVideoCapturer
- removeLast()
- Swift inout
- RIBs tutorial
- Swift RIBs
- Swift init
- Swift 프로퍼티
- iOS error
- swift property
- Swift 프로그래머스
- Swift joined()
- ios
- CS 네트워크
- 2023년 회고
- swift 고차함수
- swift reduce
- swift (programmers)
- Swift ModernRIBs
- Swift
- Swift Leetcode
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |