XLOG

[Swift] Concurrency 본문

Swift

[Swift] Concurrency

X_PROFIT 2024. 5. 19. 20:09
  1. GCD VS Concurrency
    1. 동기화의 대한 처리, 같은 데이터에 접근을 위해 GCD 의 경우 뮤텍스, 세마포어 등을 이용해야 하나, Concurrency 는 컴파일 에러를 발생시킨다.
    2. 즉 안정성이 높다
    3. GCD
      1. workItem 당 하나의 스레드를 할당,
      2. 스레드는 결국 메모리에 할당, thread explosion., 메모리 오버헤드를 발생시킬 수 있다
      3. Context switching 이 발생하며, 블록된 스레드가 어떤 자원을 lock 하고 있을 때 데드락 발생
    4. Concurrency
      1. CPU 성능 이상의 스레드를 생성하지 않는다
      2. 또한 await으로 중단됐을 때 컨텍스트 스위칭을 하는 것이 아닌 같은 스레드에서 다음 함수를 실행
    5. 우선순위 역전
      1. 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

https://green1229.tistory.com/338

'Swift' 카테고리의 다른 글

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