본문 바로가기

학습 노트/iOS (2021)

174. Dispatch Work Item & Dispatch Source Timer

Dispatch Work Item

GCD에서 Task 를 Capsule화 하는 Class이다.
이를 직접 실행할 수 있지만, 보통은 Queue나 Diaspatch Source에 추가하는 방식으로 사용한다.

취소에 관한 API를 제공하지만 효율적이지는 못하다.
만약 필요하다면 Operation Queue와 Operation을 활용해 사용하는 것이 좋다.

//
//  DispatchWorkItemViewController.swift
//  Concurrency Practice
//
//  Created by Martin.Q on 2021/12/23.
//

import UIKit

class DispatchWorkItemViewController: UIViewController {
	let workQueue = DispatchQueue(label: "WorkQueue")
	var currentWorkItem: DispatchWorkItem!
	
	
	@IBAction func submit(_ sender: Any) {
		
	}
	
	@IBAction func cancel(_ sender: Any) {
	}

}

사용할 코드는 위와 같다.

@IBAction func submit(_ sender: Any) {
	currentWorkItem = DispatchWorkItem(block: {
		for num in 0..<100 {
			print(num, separator: " ", terminator: " ")
			Thread.sleep(forTimeInterval: 0.1)
		}
	})
}

우선 Item을 생성하고, currentWorkItem에 저장한다.
이 Item은 0.1초 간격으로 100번 반복하며 숫자를 출력한다.

취소기능 구현하기

Dispatch는 취소 API를 제공하지 않기 때문에 Dispatch Work Item에 대한 참조를 저장했다가  cancel 메서드를 호출해야한다.
여부는 isCancelled 속성으로 확인하고, 이 때 currentWorkItem 에서 접근하도록 코드를 변경해야한다.

@IBAction func submit(_ sender: Any) {
	currentWorkItem = DispatchWorkItem(block: {
		for num in 0..<100 {
			guard !self.currentWorkItem.isCancelled else { return }
			print(num, separator: " ", terminator: " ")
			Thread.sleep(forTimeInterval: 0.1)
		}
	})
	workQueue.async(execute: currentWorkItem)
}

@IBAction func cancel(_ sender: Any) {
	currentWorkItem.cancel()
}

반복문 초입에 상태를 확인하도록 코드를 추가했다.
Dispatch Work Item은 Operation Queue에 비해 취소기능의 구현이 효율적이지 못하다.
지금과 같은 과정은 취소할 작업의 수가 늘어날 수록 SideEffect를 고려하여 모든 작업에 해 줘야 한다.

@IBAction func submit(_ sender: Any) {
	currentWorkItem = DispatchWorkItem(block: {
		for num in 0..<100 {
			guard !self.currentWorkItem.isCancelled else { return }
			print(num, separator: " ", terminator: " ")
			Thread.sleep(forTimeInterval: 0.1)
		}
	})
	workQueue.async(execute: currentWorkItem)
	
	currentWorkItem.notify(queue: workQueue) {
		print("Done")
	}
}

마지막 notify 메서드는 queue나 qos를 전달하고 실행할 코드를 전달할 수 있다.

취소 버튼을 누르면 즉시 작업이 취소되고, notify 메서드를 통해 추가한 코드가 실행된다.
이때, notify 메서드로 추가한 코드는 dispatchWorkItem에서 DeadLock이 발생하면 동작하지 않기 때문에
wait 메서드를 사용해 해결해야한다.

@IBAction func submit(_ sender: Any) {
	currentWorkItem = DispatchWorkItem(block: {
		for num in 0..<100 {
			guard !self.currentWorkItem.isCancelled else { return }
			print(num, separator: " ", terminator: " ")
			Thread.sleep(forTimeInterval: 0.1)
		}
	})
	workQueue.async(execute: currentWorkItem)
	
	currentWorkItem.notify(queue: workQueue) {
		print("Done")
	}
	
	let result = currentWorkItem.wait(timeout: .now() + 3)
	switch result {
	case .timedOut:
		print("time out")
	case .success:
		print("success")
	}

}

실행후 3초 내에 완료되지 못하면 timeOut 상태를 반환한다.
3초 내에 완료됐을 경우 success를 반환한다.

위와 같이 실행 후 3초가 지난 시점에 time out이 출력되는 것을 볼 수 있다.
또한 지금 상태에선 3초가 지나기 전까지 UI가 반응할 수 없다.
새로 추가한 wait 메서드가 Main Thread를 점유하고 있기 때문이다.
wait 메서드는 동기메서드에 해당하고, 이러한 결과가 나오지 않도록 주의해야한다.

이러한 패턴으로 네트워크에 접속하고, 지정된 시간 내에 응답이 없을 경우 취소하는 방식으로 많이 사용된다.
또한 같은 패턴으로 지정된 시간 내에 작업이 완료되지 않으면 취소하는 것도 가능하다.

 

Dispatch Source Timer

Dispatch Source는 다양한 시스템 이벤트를 감시하고, Queue를 통해 handler를 실행하는 API를 제공한다.

var timer: DispatchSourceTimer?

@IBAction func start(_ sender: Any) {
	if timer == nil {
		timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)
		timer?.schedule(deadline: .now(), repeating: 1)
		timer?.setEventHandler(handler: {
			self.timeLabel.text = self.formatter.string(from: Date())
			print(self.timeLabel.text ?? "")
		})
	}
	timer.resume()
}

timer를 저장할 인스턴스를 생성하고, DispatchSource의 makeTimerSource 메서드를 사용해 timer를 생성한다.
해당 타이머는 주기마다 UI를 업데이트 하게 되므로 main Queue에서 실행되도록 한다.
schedule로 Timer의 주기를 설정하고, setEventHandler를 사용해 실행할 코드를 작성한다.
이후 Timer를 시작할 수 있도록 resume 메서드를 호출한다.

@IBAction func suspend(_ sender: Any) {
	timer?.suspend()
}

@IBAction func stop(_ sender: Any) {
	timer?.cancel()
	timer = nil
}

suspend 메서드는 일시정지를 , cancel은 완전 정지를 의미한다.
cancel을 사용하는 경우 인스턴스에 nil을 저장해 메모리에서 비워준다.

다른 타이머와 마찬가지로, 화면을 이동해도 중지되지 않는다.
이렇게 되면 중지할 방법이 사라지기 때문에 화면을 벗어나거나 진입할 때 이를 제어해 줘야한다.

override func viewWillAppear(_ animated: Bool) {
	super.viewWillAppear(animated)
	timer?.resume()
}

override func viewWillDisappear(_ animated: Bool) {
	super.viewWillDisappear(animated)
	stop(self)
}

진입할 때는 기존의 Timer를 이어서 실행하고,
벗어날 때는 stop 메서드를 통해 완전히 종료한다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

176. GCD in Action  (0) 2022.01.10
175. Dispatch Group, Dispatch Semaphore  (0) 2022.01.10
173. GCD #1 (Grand Central Dispatch)  (0) 2022.01.07
172. Interoperation Dependencies  (0) 2022.01.07
171. Operation & Operation Queue  (0) 2022.01.06