ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Swift Concurrency - Task (3)
    Concurrency 2023. 3. 2. 10:48

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 Task 세번째 이야기인 Task의 딜레이를 주는 방법을 알아보겠습니다🙌

     

    비동기 Swift Task 딜레이 주기

    때때로 비동기 Task의 실행에 있어 어느정도 지연을 주고 싶을때가 있죠!

    그럴때 우리가 익숙한 combine이라면 Debounce를 걸어 처리하곤 합니다.

    그런데 Task를 사용하는 Swift Concurrency에서는 어떻게 하면 좋을까요?

     

    아주 간단하게 아래와 같이 Task에 제공되는 sleep 메서드를 이용하면 됩니다.

    Task {
        // Delay the task by 1 second:
        try await Task.sleep(nanoseconds: 1_000_000_000)
        
        // Perform our operation
        ...
    }

    요렇게 말이죠!

    여기서 우리가 익숙한 시스템단 즉 스레드의 sleep 같은 경우는 블로킹합니다.

    즉 해당 sleep이 이뤄지는 동안 아무 동작이나 액션을 취할 수 없게 막힙니다.

    그렇지만 Task의 sleep은 non-blocking하죠.

    그렇기에 실제 비동기 내부에서의 처리에 대해 지연될 뿐이지 다른 Task의 액션을 취하거나 코드를 실행 시켜줄 수 있습니다.

     

    그럼 이 예를 볼까요?
    class VideoViewController: UIViewController {
        ...
        
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            let loadingSpinnerTask = Task {
                try await Task.sleep(nanoseconds: 150_000_000)
                showLoadingSpinner()
            }
    
            Task {
                await prepareVideo()
                loadingSpinnerTask.cancel()
                hideLoadingSpinner()
            }
        }
        
        ...
    }

    예시로 위 코드를 보시면 try로 Task를 지연시킵니다.

    try인 이유는 해당 시간 동안 작업이 취소된 경우 오류를 발생시키기에 sleep이여도 에러가 날 수 있음에 try로 처리합니다.

    그리고 해당 시간 이상이 걸릴때는 로딩 스피너가 노출될 수 있도록 showLoadingSpinner 메서드를 호출합니다.

     

    중요한건 그와 동시에 prepareVideo를 통해 비디오를 준비하도록 비동기로 처리합니다.

    그러면서 비디오 준비가 끝나면 스피너를 노출시키는 Task를 취소하고 노출을 숨깁니다.

    즉, 두 Task는 동기적이 아닌 비동기적으로 호출되어 각 Task로 처리되는데 만약 비디오를 준비하는 시간이 해당 sleep 시간보다 길게 되면 로딩 스피너가 도는것이고 그전에 준비가 완료되면 위 Task를 취소하게 되는것입니다.

     

    즉 지연을 시키면서 로딩 인디케이터를 적절히 잘 보여줄 수 있도록 구현하기 용이할것 같아요👍

     

    자 그런데 여기서 하나 불편한게 있습니다!

    바로 나노 세컨즈 단위로 sleep을 시켜주고 있죠?

    그러다보니 세컨즈 즉, 초 단위가 익숙한 저희한테는 굉장히 머리를 굴려야되는 상황이죠🥲

    그렇기에 아래와 같이 Task를 확장시켜 추상화해 조금 더 익숙한 초 단위로 사용할 수 있습니다.

    extension Task where Failure == Error {
        static func delayed(
            byTimeInterval delayInterval: TimeInterval,
            priority: TaskPriority? = nil,
            operation: @escaping @Sendable () async throws -> Success
        ) -> Task {
            Task(priority: priority) {
                let delay = UInt64(delayInterval * 1_000_000_000)
                try await Task<Never, Never>.sleep(nanoseconds: delay)
                return try await operation()
            }
        }
    }

    여기서 Task를 호출하는 부분을 보면 Task<Never, Never> 타입입니다.

    즉 반환 타입 자체가 없죠 sleep은!

    그런데 에러가 날 수 있는데도 불구하고 Never 타입을 갖는게 조금 신기하긴 합니다🤔

     

    아무튼! 이렇게 delayed라는 기능을 만들었다면 아래와 같이 사용할 수 있습니다.
    class VideoViewController: UIViewController {
        ...
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            let loadingSpinnerTask = Task.delayed(byTimeInterval: 0.15) {
                self.showLoadingSpinner()
            }
    
            Task {
                await prepareVideo()
                loadingSpinnerTask.cancel()
                hideLoadingSpinner()
            }
        }
        
        ...
    }

    훨씬 더 간결하고 쉬워졌죠?

    Task에 해당 메서드를 호출해서 시간만 넣어주고 그 시간 후에 어떤 동작을 할지 정의만 해주면되죠!

     

    그런데 또 하나 변경된게 보일거에요.

    바로 self.showLoadingSpinner 코드를 보면 self로 접근하죠?

    왜냐하면 해당 Task 클로저 내에서 수동으로 캡쳐해야 된다는 소리입니다.

     

    만약 이것도 귀찮다~ 원래대로 self 안붙이도록 하고 싶다!

    수동 캡쳐 처리 이런거 다 알아서 해줘라~ 한다면 @_implicitSelfCapture를 사용하면 되요.

    extension Task where Failure == Error {
        static func delayed(
            byTimeInterval delayInterval: TimeInterval,
            priority: TaskPriority? = nil,
            @_implicitSelfCapture operation: @escaping @Sendable () async throws -> Success
        ) -> Task {
            ...
        }
    }

    해당 프로퍼티 래퍼의 기능은 암묵적으로 셀프 캡쳐를 해주는건데 보시면 _ 언더바가 붙었잖아요?

    Swift에서 제공은 하지만 _가 붙은 기능들은 아직 베타 버전이라고 생각하면 됩니다.

    즉 공식적으로 사용해라! 하고 공표된게 아니기에 언제든 사라지거나 바뀔 수 있어 좋은 방법은 아닌것 같아요😲

     

    결론

    우리가 익숙한 GCD나 타이머 등 이런걸로 지연을 구현할 수도 있지만 Concurrency에서 Task에 조금 더 편리하게 붙일 수 있습니다.

    정리하자면 Task 참 편리하다~🙌

     

    마무리

    Task 지연에 대해 기존에는 내부에서 GCD를 사용했었는데 알고 나니 더 편리하고 깔끔하게 코드를 구현할 수 있겠네요👍

     

    [참고  자료]

    https://www.swiftbysundell.com/articles/delaying-an-async-swift-task/

     

    Delaying an asynchronous Swift Task | Swift by Sundell

    Most often, we want our various asynchronous tasks to start as soon as possible after they’ve been created, but sometimes we might want to add a slight delay to their execution — perhaps in order to give another task time to complete first, or to add s

    www.swiftbysundell.com

    'Concurrency' 카테고리의 다른 글

    Swift Concurrency - Parallel  (4) 2023.03.06
    Swift Concurrency - Task (4)  (8) 2023.03.02
    Swift Concurrency - Task (2)  (2) 2023.02.27
    Swift Concurrency - Task (1)  (6) 2023.02.24
    Swift 5.5 - async & await  (0) 2021.09.25
Designed by Tistory.