일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- GCD
- authentication
- view
- toolbarvisibility
- Network
- state
- 최적화
- gesture
- auth
- Performance
- arkit
- WWDC
- dataflow
- Animation
- firebase
- avsession
- swift
- CS
- 접근성제어
- ios
- iphone
- UIKit
- Concurrency
- RxSwift
- 달력
- withAnimation
- SwiftUI
- stateobject
- combine
- 네트워크
- Today
- Total
XLOG
[Swift] Concurrency 본문
- GCD VS Concurrency
- 동기화의 대한 처리, 같은 데이터에 접근을 위해 GCD 의 경우 뮤텍스, 세마포어 등을 이용해야 하나, Concurrency 는 컴파일 에러를 발생시킨다.
- 즉 안정성이 높다
- GCD
- workItem 당 하나의 스레드를 할당,
- 스레드는 결국 메모리에 할당, thread explosion., 메모리 오버헤드를 발생시킬 수 있다
- Context switching 이 발생하며, 블록된 스레드가 어떤 자원을 lock 하고 있을 때 데드락 발생
- Concurrency
- CPU 성능 이상의 스레드를 생성하지 않는다
- 또한 await으로 중단됐을 때 컨텍스트 스위칭을 하는 것이 아닌 같은 스레드에서 다음 함수를 실행
- 우선순위 역전
- Concurrency는 FIFO 가 아니기 때문에 우선순위가 높은 작업이 들어오게 되면 해당 작업을 먼저 수행시킨다.
MainActor
자동으로 UI 관련 API가 메인 스레드에서 적절하게 디스패치 되도록 제공해주는 속성
하지만 Task 를 Handling 해줘야한다. 할당이 헤제되더라도 비동기 작업이 자동으로 취소되지는 않고 백그라운드에서 계속 실행이 된다.
Task 참조 및 취소 -> Task 를 변수로 할당하여 view의 life cycle을 이용하여 취소를 해줄 수 있다.
private var loadinTasK: Task<Void, Never>?
viewWillAppear() {
loadingTask = Task {
do {
let user = try await loader.loadUser(withID: userID)
userDidLoad(user)
} catch {
handleError(error)
}
}
viewDidDisappear() {
loadingTask?.cancel()
loadingTask = nil
}
}
하지만 Task 를 MainActor 가 붙은 view, vc 에서 실행하게 되면 같은 컨텍스트 내이기 때문에 메인스레드에서 실행이 되어 성능적이 효율성을 얻지 못할 수 있다.
이럴때 detached Task 즉 분리된 Task 를 사용할 수 있다.
이렇게 되면 vc 의 메서드를 다시 호출할 때도 await을 사용해야 합니다.
class ProfileViewController: UIViewController {
private let userID: User.ID
private let database: Database
private let imageLoader: ImageLoader
private var user: User?
private var loadingTask: Task<Void, Never>?
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard loadingTask == nil else {
return
}
loadingTask = Task {
let databaseTask = Task.detached(
priority: .userInitiated,
operation: { [database, userID] in
try database.loadModel(withID: userID)
}
)
do {
let user = try await databaseTask.value
let image = try await imageLoader.loadImage(from: user.imageURL)
userDidLoad(user, image: image)
} catch {
handleError(error)
}
loadingTask = nil
}
}
...
private func userDidLoad(_ user: User, image: UIImage) {
// Render the user's profile
...
}
}
메모리 관리
Task 는 escaping closer 에서 실행이 되기 때문에 Local 의 함수나 변수를 사용할 때 self 를 사용하게 된다. 그렇기에 reference count 가 증가하는 것을 막을 수 없고, vc 가 해제되더라도 background에서 작업이 지속될 수 있기 때문에 메모리 누수가 발생할 수 있다.
그렇기에 약한참조를 걸어주는 것이 좋다. 혹은 viewWillDisappear 에서 미리 task 를 cancel 해주는 것 해결법이다. deinit 에서 실행을 해주게 되면 계속해서 메모리 해제가 되지 않기 때문에 cancel을 시켜줄 수 없다.
자동 재시도
Thread 의 sleep 은 스레드 동작을 멈추지만 Task 의 Sleep 은 해당 Task 만 sleep 시킨다. (non blocking)
Combine 은 retry 를 해줄 수 있으나 그게 안되기 때문에
for _ in 0..<3 {
do {
return try await performLoading()
} catch {
continue
}
}
return try await performLoading
병렬 처리
// featured 실행 후 favorites 실행 후 latest tlfgod
extension ProductLoader {
func loadRecommendations() async throws -> Product.Recommendations {
let featured = try await loadFeatured()
let favorites = try await loadFavorites()
let latest = try await loadLatest()
return Product.Recommendations(
featured: featured,
favorites: favorites,
latest: latest
)
}
}
// 같은 코드!!
extension ProductLoader {
func loadRecommendations() async throws -> Product.Recommendations {
try await Product.Recommendations(
featured: loadFeatured(),
favorites: loadFavorites(),
latest: loadLatest()
)
}
}
이렇게 되면 Feature, favorites, latest 를 순차적으로 실행하기 때문에 효율적이지 못하다.
extension ProductLoader {
func loadRecommendations() async throws -> Product.Recommendations {
async let featured = loadFeatured()
async let favorites = loadFavorites()
async let latest = loadLatest()
return try await Product.Recommendations(
featured: featured,
favorites: favorites,
latest: latest
)
}
}
asyn let구문을 사용하게 되면 완료될 때까지 기다리는 것이 아닌 각자 백그라운드에서 비동기 작업을 시작한다.
Task Group
태스크 그룹은 Task 내 오류를 발생시키는 옵션을 사용할지 여부에 따라 withTaksGroup 혹은 withThrowingTaskGroup 을 사용할 수 있다.
extension ImageLoader {
func loadImages(from urls: [URL]) async throws -> [URL: UIImage] {
try await withThrowingTaskGroup(of: (URL, UIImage).self) { group in
for url in urls {
group.addTask{
let image = try await self.loadImage(from: url)
return (url, image)
}
}
var images = [URL: UIImage]()
for try await (url, image) in group {
images[url] = image
}
return images
}
}
}
Map, forEach
extension Sequence {
func asyncMap<T>(
_ transform: (Element) async throws -> T
) async rethrows -> [T] {
var values = [T]()
for element in self {
try await values.append(transform(element))
}
return values
}
}e
extension Sequence {
func concurrentForEach(
_ operation: @escaping (Element) async -> Void
) async {
// A task group automatically waits for all of its
// sub-tasks to complete, while also performing those
// tasks in parallel:
await withTaskGroup(of: Void.self) { group in
for element in self {
group.addTask {
await operation(element)
}
}
}
}
}
참고자료
https://engineering.linecorp.com/ko/blog/about-swift-concurrency
'Swift' 카테고리의 다른 글
실시간 STT (Sound to Text) 구현하기 (1) | 2024.12.22 |
---|---|
Fatal error: Duplicate keys of type 'Node' were found in a Dictionary 해결 (0) | 2024.08.16 |
[Swift] CoreData (0) | 2024.06.08 |
[Swift] IAP, 인앱결제 StoreKit2 (2) | 2024.05.19 |