[iOS] GCD (4) - Queue & Threads

전성훈·2023년 10월 31일
0

iOS/Concurrency

목록 보기
4/11
post-thumbnail

주제: Dispatch Queue의 기본


Threads

  • Threads는 threads of execution의 줄임말로 써, 시스템 내 리소스를 활용하여 프로세스가 작업을 분할하는 방법이다. iOS앱은 CPU 내 코어 수와 동일한 수의 스레드를 실행시켜 작업을 수행한다.
  • 앱 작업을 여러 스레드로 분할하는 것에는 많은 장점이 있다.
  • Faster execution(빠른 실행)
    - 스레드에서 작업을 실행하면 병렬로 작업이 수행될 수 있기 때문에 serial로 모든 작업을 실행하는 것보다 더 빠르게 완료할 수 있다.
  • Responsiveness(반응성)
    - UI 스레드에서 사용자가 볼 수 있는 작업만 수행하면, 사용자는 앱이 일시적으로 느려지거나 멈추는 것을 인식하지 못 할 수 있다.
  • Optimized resource consumption(최적화된 자원 소비)
    - 스레드는 OS에 매우 최적화되어있다.
  • 스레드는 명시적으로 생성할 필요는 없다. OS는 더 높은 추상화 수준을 활용하여 스레드를 생성한다.
  • 스레드를 직접 관리하면 성능이 저하될 수 있다.

Dispatch Queues

  • 스레드를 사용한느 방법은 DispatchQueue를 만드는 것이다. 큐를 만들 때, OS는 큐에 하나 이상의 스레드를 생성하고 할당할 수 있다.
  • 기존 스레드가 사용 가능하면 재사용할 수 있고, 그렇지 않으면 OS가 필요에 따라 스레드를 생성한다.
let queue = DispatchQueue(label: "testQueue")
  • label 인자는 식별을 위해 고유한 값이어야 한다. UUID를 사용하여 고유성을 보장할 수도 있지만, 유용하고 의미있는 텍스트를 할당하는 것이 좋다.

The main Queue

  • 앱이 시작될 때, main dispatch queue가 자동적으로 생성된다. 이는 UI를 담당하는 serial queue이다.
  • 이는 클래스 변수로 제공되며 DispatchQueue.main을 통해 액세스 할 수 있다.
  • UI 작업과 관련되지 않은 경우, 메인 큐에서 동기적으로 작업을 실행시키는 행동은 UI가 락 되기 때문에 앱 성능이 저하될 수 있다.

Quality of service

  • QoS(Quality of Service)는 iOS에서 작업에 우선순위를 부여하기 위해 사용되는 개념이다.
  • 동시 작업을 수행하는 Dispatch Queue를 사용할 때, iOS에게 작업의 중요도를 알려줌으로써 시스템 리소스를 효율적으로 사용할 수 있다.
  • 높은 우선순위의 작업은 더 빨리 실행되며, 일반적으로 더 많은 시스템 리소스와 전력을 사용하게 된다.
  • Dispatch Queue를 직접 custom하게 생성하지 않고도, 미리 지정된 global queue를 이용해서 사용할 수 있다.
let queue = DispatchQueue.global(qos: . userInteractive)
  • 글로벌 큐는 항상 concurrent이며 FIFO이다.
구분사용해야 하는 상황소요 시간
.userInteractive유저와 직접 상호작용 하는 작업에 권장: UI 업데이트 계산, 애니메이션 또는 UI를 반응적이고 빠르게 유지하는 모든 것거의 즉시
.userInitiated유저가 UI에서 즉시 발생해야하지만 비동기적으로 수행될 수 있는 작업들: 앱 내부에서 pdf 파일을 열겨나, 로컬 데이터베이스 읽기 등몇초
.default일반적인 작업-
.utility보통 Progress Indicator와 함께 길게 실행되는 작업: 오랜 시간 동안 실행되는 시간, I/O, 네트워킹 또는 연속 데이터 피드 등몇초에서 몇분
.background유저가 직접적으로 인지하지 않고(시간이 안 중요한)작업: 미리 가져 오기, 데이터베이스 유지 관리, 원격 서버 동기와 및 백업 등몇분이상
.unspecifiedlegacy API 지원-

Adding task to Queues

  • GCD의 비동기 메서드인asyncsync를 이용해 큐에 작업(task)을 추가할 수 있다.
  • 예를 들어, 앱을 시작할 때 서버에 연결해서 앱 상태를 업데이트 할 필요가 있을 경우, 이 작업은 사용자가 시작하는 것이 아니므로 즉시 실행되어야 하는 것이 아니며, 네트워크 I/O에 따라 달라지므로, global utility queue로 보내야 한다.
DispatchQueue.global(qos: .utility).async { [weak self] in 
	guard let self = self else { return }
	// 작업 수행 
	
	// UI 업데이트를 위해 다시 메인 큐로 전환
	DispatchQueue.main.async { 
		self.textLabel.text = "새로운 기사가 있습니다!"
	}
}
  • 위 코드 샘플을 보면 두 가지 핵심적인 포인트가 있다.
  1. DispatchQueue에는 클로저 규칙을 준수해야 한다. 따라서 클로저의 캡처된 변수를 올바르게 처리해야 한다.

    • GCD의 비동기 클로저에서 self를 강하게 캡처하면 참조 사이클이 발생하지는 않는다. 해당 클로저가 완료되면 전체적으로 해제되기 때문이다.
    • 하지만 self를 강하게 캡쳐하면 self의 수명이 연장된다. 예를 들어, 뷰 컨트롤러에서 네트워크 요청을 하고 이를 중간에 dismiss했을 경우, 해당 클로저가 호출된다. 뷰 컨트롤러를 약하게 캡쳐 할 경우 nil이 된다.
    • 강한 참조 예시
    class ViewController: UIViewController { 
    	var name: String = "뷰컨"
    	
    	func doSomething() { 
    		DispatchQueue.global().async { 
    			sleep(3)
    			print("글로벌큐에서 출력하기: \(self.name)")
    			DispatchQueue.main.async {
    				print("메인큐에서 출력하기: \(self.name)")
    			}
    		}
    	}
    	deinit { 
    		print("\(name) 메모리 해제")
    	}
    }
    
    func localScopeFunction() { 
    	let vc = ViewController()
    	vc.doSomething() 
    }
    //글로벌큐에서 출력하기: 뷰컨
    //메인큐에서 출력하기: 뷰컨
    //뷰컨 메모리 해제
    • 약한 참조 예시
    class ViewController: UIViewController { 
    	var name: String = "뷰컨"
    	
    	func doSomething() { 
    		DispatchQueue.global().async { [weak self] in 
    			sleep(3)
    			guard let self = self else { return }
    			print("글로벌큐에서 출력하기: \(self.name)")
    			DispatchQueue.main.async {
    				print("메인큐에서 출력하기: \(self.name)")
    			}
    		}
    	}
    	deinit { 
    		print("\(name) 메모리 해제")
    	}
    }
    
    func localScopeFunction() { 
    	let vc = ViewController()
    	vc.doSomething() 
    }
    //뷰컨 메모리 해제
  2. 백그라운드 큐로 전환된 내부에서 UI 업데이트가 메인 큐로 디스패치 된다. async 타입의 호출을 중첩하는 것이 일반적이다.

    • 현재 큐에 작업을 동기적으로 제출하면 현재 큐를 블록하고 작업이 현재 큐에서 리소스에 액세스하려고 시도하면 앱이 데드락 상태에 빠질 수 있다.
    • 마찬가지로 메인 큐에서 sync를 호출하면 UI를 업데이트하는 스레드가 차단되어 앱이 멈춘 것처럼 보일 수 있다.
    • 메인 스레드에서 sync를 호출하면 주 스레드를 차단하므로 데드락이 발생할 수 있으므로 절대로 호출해서는 안 된다.

Image loading example

  • 기존 cellForItemAt에서 데이터를 직접 뿌려주다보니 메인스레드에서 네트워크 작업을 실시해서 버벅거리는 현상이 있었다.
  • DispatchQueue를 활용해서 네트워크 작업과 UI작업을 분리해서 코드를 작성하면 버벅거리는 현상이 없어진다.
private func downloadWithGlobalQueue(at indexPath: IndexPath) { 
	DispatchQueue.global(qos: .utility).async { [weak self] in 
		guard let self = self else { 
			return 
		}
		let url = self.urls[indexPath.row]
		guard let data = try? Data(contentsOf: url), 
					let image = UIImage(data: data) else { 
						return 
					}
		DispatchQueue.main.async { 
			if let cell = self.collectionView.cellForItem(at: indexPath) as? PhotoCell { 
				cell.display(image: image)
			}
		}
	}
}

extension CollectionViewController {
  override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return self.urls.count
  }

  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "normal", for: indexPath) as! PhotoCell
    cell.display(image: nil)
    downloadWithGlobalQueue(at: indexPath)
	
    return cell
  }
}
  • 하지만 URLSession의 dataTask 메서드를 사용하면, 데이터 다운로드를 직접 처리하지 않아도 되기 때문에 dispatchQueue를 사용할 필요가 없다. 이는 DispatchQueue.global를 코드 내부에서 내포하기 때문이다.
  • 가능한 경우 시스템에서 제공하는 메서드를 사용하는 것이 좋다. 이렇게 하면 코드를 좀 더 안정적으로 만들 수 있으며, 가독성 또한 향상되기 때문이다.
private func downloadWithURLSession(at indexPath: IndexPath) { 
	URLSession.shared.dataTask(with: urls[indexPath.item]) { [weak self] data, response, error in 
		guard let self = self, 
					let data = data,
					let image = UIImage(data: data) else { 
			return 
		}
		DispatchQueue.main.async { 
			if let cell = self.collectionView.cellForItem(at: indexPath) as? PhtoCell { 
				cell.display(image: image)
			}
		}
	}.resume()
}

DispatchWorkItem

  • DispatchWorkItem은 DispatchQueue에 클로저로 전달하는 것 외에도 코드를 제공할 수 있는 객체를 제공하는 클래스이다.
  • 즉, 작업을 클래스화 한 객체이다.
  • 완벅하지는 않지만 취소기능과 순서기능을 내장하고 있다.
let queue = DispatchQueue(label: "xyz")
let workItem1 = DispatchWorkItem {
  print("The block of code ran!")
}

let workItem2 = DispatchWorkItem {
  print("The block of code ran!")
}

let workItem3 = DispatchWorkItem {
  print("The block of code ran!")
}

workItem2.cancel()

workItem1.notify(queue: DispatchQueue.global(), execute: workItem3)

queue.async(execute: workItem1)

Canceling a work item

  • DispatchWorkItem을 명시적으로 사용하는 이유 중 하나는 실행 전 또는 실행 중에 작업을 취소해야 하는 경우이다.
  • 작업 항목에 cancel()을 호출하면 다음 중 하나가 수행된다.
  1. 작업이 아직 큐에서 시작되지 않은 경우 제거된다.
  2. 작업이 현재 실행 중인 경우 isCancelled 속성이 true로 설정된다.
  • 주기적으로 isCancelled 속성을 확인하고 작업을 취소할 수 있는 경우 적절한 조치를 취해야 한다.

Poor man's dependencies

  • DispatchWorkItem 클래스는 현재 작업 항목이 완료된 후 실행될 다른 DispatchWorkItem을 식별하는 데 사용할 수 있는 notify(queue: execute: ) 메서드도 제공한다.

Conclusion

큐의 종류생성 코드특징직렬/동시
.mainDispatchQueue.main메인 큐 = 메인 쓰레드(UI 업데이트)Serial
.global()DispatchQueue.global(qos: )6가지 QosConcurrent
customDispatchQueue(label: " ")QoS추론 / 설정 가능default : Serial

출처(참고문헌)

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글