동시성 문제 및 쓰레드 관리에서 어떠한 차이가 있는지 중점으로 알아보겠습니다.
먼저, Swift Concurrency가 어떻게 쓰이는지 간단하게 알아보겠습니다.
func getlistPhotosURLS(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
let photo = await getlistPhotosURLS(inGallery name: String)
show(photo)
기존의 Completion Handler(escaping closure 이용한)를 이용해 비동기 코드를 다뤘던 부분을, async로 대체하고 해당 함수의 결과 값을 받는 부분에 await keyword를 사용함으로서 가독성을 높이고 비동기 코드를 마치 sync 코드처럼 읽을 수 있습니다.
await 키워드로 인해 데이터 흐름이 중지되면 이후에 사용해야 하는 데이터를 힙(heap) 영역에 저장해 두고, 이후에 다시 힙 영역에서 해당 데이터를 가져와 사용합니다.
나아가
1. 콜백 구조 안에 또 콜백 코드가 들어가는, Pyramid of doom 문제
2. 에러헨들링 문제
3. Data Race 문제를 컴파일 과정에서 발견
위와 같은 문제를 다음과 같이 간단하게 해결합니다.
1. Pyramid of doom
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)
쉬운 예시를 들기위해 3장의 사진이 모두 완료된 후에 이미지를 보여주는 코드를 가져왔습니다.
Dispatch Group을 이용해 3개의 비동기 통신이 완료되었을 때, 이미지를 보여주는 작업도 가능합니다.
하지만 Swift Concurrency는 await을 이용해 해당 문제를 해결합니다.
또한 실제 프로젝트에서, 연속적인 통신과정으로 특정 이미지를 불러오는 작업도
위와 같이 해결이 가능합니다.
2. 에러헨들링 문제
func getlistPhotosURLS(inGallery name: String) async throws -> UIImage {
let (data, response) = try await session.downloadImage(from: name)
guard let image = UIImage(data: data) else {
throw Error.invalidDataError
}
return image
}
let firstPhoto = try await getlistPhotosURLS(inGallery name: url1)
let secondPhoto = try await getlistPhotosURLS(inGallery name: url2)
let thirdPhoto = try await getlistPhotosURLS(inGallery name: url3)
위와 같이 반환 키워드 '->' 왼쪽에 throws 키워드를 붙이고, throw와 try 구문을 통해
에러 헨들링을 간편하게할 수 있습니다.
3. Data Race 문제
또한, 개발자의 실수로 기존의 completion handler 구문에서 공유자원에 동시에 접근하여 Data Race 문제가 생기는 코딩을 진행하면 문제가 생기고 나서야 해당 부분이 파악이 가능했습니다.
즉, 별도의 컴파일 에러는 발생하지 않았습니다.
하지만, Swift Concurrency를 사용한 코드 과정에서 Data Race 문제가 생기면, non-isolated 구문이 변할 수 있는 프로퍼티에 접근하는 것을 금지한다는 메시지와 함께 컴파일 에러가 발생하게 됩니다.
미처 생각치 못한 Side Effect를 줄일 수 있음에 매우 훌륭하다고 생각됩니다.
GCD vs Swift Concurrency
결론부터 말하자면, Swift Concurrency는 thread explosion과 우선순위 역전(Priority Inversion)
문제에서 GCD에 비교해 비교적 자유롭습니다.
(동시성 문제는 예민하기때문에, 개발환경 및 UI 업데이트와 관련된 비동기 호출인지 등 변동성이 많고 주의해야 할 점이 많기에 보수적으로 비교적 자유롭다고 표현했습니다.)
Swift Concurrency에서는 await으로 중단됐을 때, CPU가 컨텍스트 스위칭을 해서 다른 스레드를 불러오는 것이 아니라 같은 스레드에서 다음 함수를 실행시킵니다. 즉, 하나의 코어가 하나의 스레드를 실행하도록 유지하는 것을 보장합니다. 기존에 스레드의 컨텍스트 스위칭으로 진행되던 것이 같은 스레드 내의 함수 호출로 대체되는 것입니다.
(ref: https://engineering.linecorp.com/ko/blog/about-swift-concurrency/)
이러한 시스템은, 코어보다 많은 쓰레드 생성을 하지 않게 됩니다.
다만, UI 업데이트가 필요한 상황에서는 코어보다 많은 스레드가 생성될 수 있으며 GCD와 비교하여 적은 스레드와 적은 컨텍스트 스위칭이 일어나지만 엄청나게 의미있는 차이가 나지 않습니다.
(추후에 조금 더 Test를 진행하여 의미있는 차이가 나면 포스팅 하겠습니다.)
또한, UI 업데이트가 없는 사용자 정의 비동기 코드에선 극명하게 GCD와 Swift Concurrency가 차이가
나게 됩니다.
1. thread explosion
긴 연산을 필요로 하는 작업을, GCD concurrent queue에 50개의 DispatchWorkItem으로 작업을 수행하게 하고 Swift Concurrency로 수행하게 Test 합니다.
활성화된 스레드 수와 컨텍스트 스위칭 수는 모두 11배 차이가 나게됩니다.
(ref: https://engineering.linecorp.com/ko/blog/about-swift-concurrency/
다만, DispatchWorkItem을 사용하지 않고 작업을 수행하게 된다면 연관된 작업이 같은 스레드에서 실행할 수 있으므로 차이가 더욱 적어질 것으로 생각됩니다.
또한 개발환경에 따라 Swift Concurrency를 사용하지 못한다 하더라도, DispatchSemaphore와 NSOperation queue를 이용해서 최대 쓰레드의 작업 수를 제한하는 것도 좋은 방법입니다.
(하지만 각기 다른 기기에 효율적으로 코어를 사용하지 못하는 것은 아쉬움이 남겠죠)
2. 우선순위 역전(Priority Inversion)
이 문제 또한 간단합니다. GCD에선 FIFO 구조로 task들이 담겨지기 때문에 우선순위가 높은 task가 뒷 순서에 있게 된다면, 앞 순서의 task를 높여서 문제를 해결합니다.
Swift Concurrency에선 작업순서가 FIFO가 아니라서, 우선순위가 높은 작업이 추가되면 바로 해당 task를 실행합니다.
(Goooooood)
당연히 최신 기술인 만큼 장점 밖에 없어보이지만, Dispatch Group 및 Operation Queue를 이용해 GCD 환경에서도 최적화가 가능하다고 생각합니다.
iOS 13 이상에서도, 별도의 모듈을 설치하여 Swift Concurrency를 개발환경에 추가할 수 있다고 하지만
아직 컴파일 및 기타 문제가 여러 존재하는 듯 합니다. 하지만 최소 iOS 버전이 많이 높아졌기에 곧 실제 개발환경에서 자주 사용될 것 같습니다.
과연 1~2년안에 completion handler의 운명은 어떻게 될까요?
'Deep Dive iOS' 카테고리의 다른 글
Thread Safe한 Core Data 환경 구성하기 (0) | 2022.01.17 |
---|---|
iOS 계층구조? 4단계로 나누어 생각해보기 (iOS Structure) (0) | 2020.12.26 |
UIView? Custom View? (0) | 2020.11.12 |
앱이 inactive 상태가 되는 경우 (0) | 2020.11.11 |
iOS/Swift) UIKit 이란? (0) | 2020.11.10 |