강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

구본성님의 프로필 이미지
구본성

작성한 질문수

앨런 Swift Concurrency for Swift 6 (Part-2)

Global Actor (전역 액터) (23강)

Actor에서 Task vs Task.detached 사용 시 재진입 문제 질문

해결된 질문

작성

·

18

·

수정됨

0

actor TaskImageDownloader {
    /// (다운로드) 상태를 저장하기 위한 열거형 정의
    enum DownloadState {
        case completed(UIImage)
        case loading(Task<UIImage, Error>)
        case failed
    }
    
    private(set) var cache: [String: DownloadState] = [:]
    
    
    func image(from url: String) async throws -> UIImage {
        /// 기존에 저장된 상태가 있는 경우 (캐시 먼저 확인)
        if let cachedState = cache[url] {
            switch cachedState {
            case .completed(let image):   /// 이미지 리턴
                return image
            case .loading(let task):      /// 작업(Task)을 기다렸다가 ===> 이미지 리턴
                return try await task.value
            case .failed:
                throw "이미지 다운로드 실패"
            }
        }
        
        /// 작업(Task)을 생성
        let task = Task.detached<UIImage, Error> {
            let image = try await downloadImage(from: url)
            return image
        }
        
        /// 일단 (완료되지 않은) 작업 상태를 보관
        cache[url] = .loading(task)
        
        do {
            /// 작업의 완료를 기다렸다가 ===> 완료되면 ===> 완료상태(이미지)로 바꿔서 보관
            let image = try await task.value
            cache[url] = .completed(image)
            return image
        } catch {
            cache[url] = .failed     // 에러 발생의 경우
            throw "이미지 다운로드 실패"
        }
    }
}

Actor에서 Task vs Task.detached 사용 시 재진입 문제 질문

안녕하세요.

위 코드에서 Task.detached 대신 일반 Task {}를 사용하면 재진입 문제가 발생할 수 있을 것 같아 질문드립니다.

제가 이해한 바로는:

  • Task {}를 사용하면 생성된 작업이 actor의 context를 상속받아 serial executor에서 실행됩니다

  • 다운로드 작업 중 await를 만나면 actor가 suspension되고, 이 시점에 다른 스레드에서 해당 actor에 재진입할 수 있습니다

  • 따라서 동일한 URL에 대해 중복으로 다운로드 작업이 생성될 가능성이 있습니다

반면 Task.detached를 사용하면 actor context와 독립적으로 실행되어 이러한 재진입 문제를 방지할 수 있는 것으로 이해했습니다.

제가 이해한 내용이 맞는지 확인 부탁드립니다. 감사합니다. 

답변 2

1

구본성님의 프로필 이미지
구본성
질문자

Task 생성과 이미지 캐싱 자체는 동기적으로 동작하니 detached로 바뀌어도 전혀 상관없는 문제였네요.
답변 감사합니다!

1

앨런(Allen)님의 프로필 이미지
앨런(Allen)
지식공유자

네 안녕하세요!

 

질문 내용에 대해, 2가지 포인트를 말씀드릴 수 있을 것 같은데요.

(1) 여기서 이렇게 Task.detached로 구현되나 그냥 Task로 구현되나, 전혀 상관없이 downloadImage 함수 자체는 액터가 아닌 외부 쓰레드에서 실행되게 됩니다.

let task = Task.detached<UIImage, Error> {
     let image = try await downloadImage(from: url)
     return image
}
let task = Task<UIImage, Error> {
     let image = try await downloadImage(from: url)
     return image
}

조금 의아하실 수도 있지만, 왜 그런지 설명드리면.. Task로 구현하시면 actor의 context를 상속받는 것은 맞지만, 실제 downloadImage(from: url) 함수 자체가 액터 내부의 메서드가 아닌 외부의 메서드이고, 해당 함수를 호출하면서, 앞에 await 이라는 키워드가 붙어있다는 것이, 결국엔 나의 쓰레드가 아닌 외부의 쓰레드를 사용한다는 의미라고 보시면 됩니다.

(쉽게 한마디로 정리해서 말씀드리면, 비동기 메서드(함수)로 구현되어 있으면, (액터에서 호출하더라도) 액터가 아닌 외부 쓰레드에서 동작합니다.)

그래서, 사실 Task 나 Task.detached나 구현하고, 실제 동작하는 것에는 전혀 차이가 없지만.. 여기서 Task.detached를 사용했다는 것은 "외부 쓰레드에서 실행된다는 의미를 조금 더 명확하게 해주기 위함"이라고 보시면 될 것 같습니다.

(네 그래서, 위의 내용을 정확하게 이해하시는 것이 먼저 중요한 것 같고요.)

 

(2) 만약에, 위의 (1)번 내용이 아니라고 가정하고, Task를 사용하는 것이 serial executor에서 실행이 된다고 가정을 해서 말씀을 드려도, 재진입 문제가 발생하지 않습니다.

let task = Task<UIImage, Error> {
      let image = try await downloadImage(from: url)
      return image
}
        
/// 일단 (완료되지 않은) 작업 상태를 보관
cache[url] = .loading(task)

왜냐하면, 바로 아래에 있는 코드인, cache[url] = .loading(task) 때문에 그렇습니다. 쉽게 말씀을 드리면 위의 코드는 바로 아래의 코드와 동일합니다. (Task 안의 비동기적인 작업을 제외하고 생각하시면 쉬워요.)

let task = Task<UIImage, Error>{ }
        
cache[url] = .loading(task)

작업을 생성하자마자, 동기적으로 일단 cache에 상태를 .loading라고 저장해 놓습니다. 작업 자체를 생성하자마자 저장해놓는 (동기적인) 방식이기 때문에 (물론 작업 안에서는 비동기적인 일이 일어나지만..) 일단 캐시 자체를 바꿔놓았기 때문에 await을 통해 외부에서 접근하더라도 재진입 문제가 발생하지 않습니다.

(그래서, "작업(Task)"자체를 생성하고 저장하는(중간에 await키워드가 전혀 들어가지 않는) 동기적인 방식으로 재진입 문제를 해결하는 것이라고 보시면 됩니다.)

 

위의 내용들을 잘 이해해 보시면 좋을 것 같고, 추가적으로 질문이 생기시면 또 질문주세요!

감사합니다. :)

구본성님의 프로필 이미지
구본성

작성한 질문수

질문하기