관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 1. 타임아웃 처리 방법(스레드 락 thread lock, DispatchWorkItem, wait(timeout:)) 본문

iOS 응용 (swift)

[iOS - swift] 1. 타임아웃 처리 방법(스레드 락 thread lock, DispatchWorkItem, wait(timeout:))

jake-kim 2023. 3. 31. 02:31

1. 타임아웃 처리 방법 - DispatchWorkItem 사용 < 

2. 타임아웃 처리 방법 -  DispatchGroup 사용 (+ async를 순서대로 처리 방법)

(async 작업이 하나이면 DispatchWorkItem을 사용하고, async 작업이 두 개 이상이면 DispatchGroup 사용)

타임아웃 처리 아이디어

  • 시나리오) 아래와 같이 someDelayWork 함수 내부에서 몇초가 걸릴지 모르고 스레드 락을 거는 작업이 있을때 사용하는쪽에서 타임아웃을 주어 일정 시간안에 응답이 안오면 에러를 띄우고 싶은 경우?
let value = someDelayWork()

private func someDelayWork() -> Int {
    sleep(10)
    return 3
}
  • 타임아웃 추가 아이디어
    • 현재 메인스레드이므로, 스레드 락이 걸리는 지점에서 메인스레드도 락이 걸릴 위험이 있기 때문에 별도의 큐를 만들어 someDelayWork()를 호출하게끔 수행
    • DispatchWorkItem을 사용하면 타임아웃 거는게 매우 쉽기 때문에 DispatchWorkItem 인스턴스를 생성하여 위에서 만든 큐와 같이 사용

사전지식) Queue의 종류

  • main queue (serial queue): DispatchQueue.main
DispatchQueue.main
  • global queue (concurernt, qos 설정 가능)
// 애니메이션과 같은 UI 즉시 업데이트가 필요하며, 멈춘것처럼 보이지 않는 작업들 (유저의 반응)
DispatchQueue.global(qos: .userInteactive)

// 저장된 문서를 열거나, 유저가 무언가 클릭했을 때 작업을 수행하고 즉각 보여주어야 하는 것 (몇초)
DispatchQueue.global(qos: .userInitiated)

// 기본 서비스 (일반적인 경우 - userInitiated와 utility의 중간정도의 우선순위)
DispatchQueue.global(qos: .default)

// 보통 프로그레스바를 같이 사용하는 작업
DispatchQueue.global(qos: .utility)

// 유저에게 표시되지 않는 동기화, 안정화, 백업과 같은 일
DispatchQueue.global(qos: .background)
  • custom queue (디폴트는 serial이며 concurrent로 변경 가능)
let mySerialQueue = DispatchQueue(label: "myQueue")
let myConcurrentQueue = DispatchQueue(label: "myQueue", attributes: .concurrent)

DispatchWorkItem을 이용한 타임아웃 구현

  • 필요한 기능
    • 정해진 시간안에 안오면 타임아웃 에러 처리
    • 정해진 시간안에 오면 타임아웃을 끝내고 성공 처리
  • 적용 전
let value = someDelayWork()

private func someDelayWork() -> Int {
    sleep(10)
    return 3
}
  • someDelayWork를 호출하는쪽의 스레드 락이 걸리므로, 메인 스레드가 아닌 별도의 스레드 생성
    • global도 전역적으로 사용되는 것이므로 커스텀 큐로 사용
let queue = DispatchQueue(label: "work_queue")
let workItem = DispatchWorkItem { [weak self] in
    let value = self?.someDelayWork()
}
  • workItem 인스턴스의 wait(timeout:) 메소드를 사용하면 타임아웃을 거는게 쉽고, 리턴타입으로 타임아웃이 일어났는지도 쉽게 파악이 가능
queue.async(execute: workItem)
let result = workItem.wait(timeout: DispatchTime.now() + 10)

switch result {
case .success:
    print("success")
case .timedOut:
    print("timedOut")
}

(결과)

let queue = DispatchQueue(label: "work_queue")
let workItem = DispatchWorkItem { [weak self] in
    let value = self?.someDelayWork()
}

queue.async(execute: workItem)
let result = workItem.wait(timeout: DispatchTime.now() + 10)

switch result {
case .success:
    print("success")
case .timedOut:
    print("timedOut")
}

private func someDelayWork() -> Int {
    sleep(5)
    return 3
}

확인) 타임아웃전에 someDelayWork()가 끝난 경우 - success

let queue = DispatchQueue(label: "work_queue")
let workItem = DispatchWorkItem { [weak self] in
    print("start")
    let value = self?.someDelayWork()
    print("end")
}

queue.async(execute: workItem)
print("test>")
let result = workItem.wait(timeout: DispatchTime.now() + 10)

switch result {
case .success:
    print("success")
case .timedOut:
    print("timedOut")
}

private func someDelayWork() -> Int {
    sleep(5)
    return 3
}

/*
test>
start
end
success
*/

확인) someDelayWork()가 늦게 끝나서 타임아웃에 도달한 경우 - timedout

  • 주의할점: 타임아웃이 걸려도 someDelayWork()가 끝난 시점에 데이터를 받아오기 때문에 (end가 출력) 취소되었을때 작업을 중단하는 별도의 코드도 필요
let queue = DispatchQueue(label: "work_queue")
let workItem = DispatchWorkItem { [weak self] in
    print("start")
    let value = self?.someDelayWork()
    print("end")
}

queue.async(execute: workItem)
print("test>")
let result = workItem.wait(timeout: DispatchTime.now() + 5)

switch result {
case .success:
    print("success")
case .timedOut:
    print("timedOut")
}

private func someDelayWork() -> Int {
    sleep(10)
    return 3
}

/*
test>
start
timedOut
end
*/
  • workItem.cancel()을 timeout나는 곳에서 호출해도 안 클로저는 다 실행되는것을 주의 (위에서 end 출력)
    • cnacel()을 사용하면 아래처럼 isCanclled 프로퍼티가 true로 되어 이것을 사용할때 적용
let result = workItem.wait(timeout: DispatchTime.now() + 1)

switch result {
case .success:
    print("success")
case .timedOut:
    workItem.cancel()
    print("isCancelled?", workItem.isCancelled)
    print("timedOut")
}
  • timeout 핸들링이 필요한 경우 아래처럼 전역변수에 isCancelled를 두고 사용
class ViewController: UIViewController {
    var isCancelled = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        isCancelled = false
        let queue = DispatchQueue(label: "work_queue")
        let workItem = DispatchWorkItem { [weak self] in
            print("start")
            let value = self?.someDelayWork()
            guard self?.isCancelled == false else { return }
            print("end", value)
        }
        
        queue.async(execute: workItem)
        print("test>")
        let result = workItem.wait(timeout: DispatchTime.now() + 1)

        switch result {
        case .success:
            print("success")
        case .timedOut:
            workItem.cancel()
            self.isCancelled = workItem.isCancelled
            print("timedOut")
        }
    }
    
    private func someDelayWork() -> Int {
        sleep(3)
        return 3
    }
}

* 전체 코드: https://github.com/JK0369/ExTimeout
* 참고
https://ios-development.tistory.com/1201

Comments