Queues & Threads

박성민·2021년 7월 5일
0

iOS

목록 보기
22/30

Thread

  • Thread는 thread of execution을 나타내는 말로, 실행중인 process가 시스템의 리소스 간에 작업을 분할하는 방식입니다. 디바이스 cpu의 코어 갯수만큼 한번에 여러 스레드를 실행할 수 있습니다.
  • Multiple thread의 장점
    • Faster execution: 다중 쓰레드로 여러 작업을 실행하면 동시에 작업할 수 있습니다. 따라서 모든 것을 직렬로 수행하는 것보다 빠르게 작업을 끝낼 수 있습니다.
    • Responsiveness: main UI thread에서 user-visible한 작업만 수행한다면, 사용자들은 다른 쓰레드에서 수행할 수 있는 작업으로 인해 앱이 느려지거나 멈추는 것을 인식하지 못할 것입니다.
    • Optimized resource comsumption: Thread는 OS에 의해 최적화되어있습니다.

Dispatch queues

  • thread로 작업하는 밥법은 바로 DispatchQueue를 생성하는 것입니다. queue를 생성하면 OS가 잠정적으로 한개 이상의 thread를 생성하여 queue에 할당합니다. 이미 존재하고 있는 thread들이 사용가능하다면 재사용 될 수 있습니다. 그렇지 않다면 OS가 필요에 따라 생성합니다.
  • dispatch queue를 만드는 것은 아래와 같이 간단합니다.
let label = "com.sungmin.study.serial"
let queue = DispatchQueue(label: label) // serial queue

let label2 = "com.sungmin.study.concurrent"
let queue2 = DispatchQueue(label: label2, attribures: .concurrent) // concurrent queue
  • label은 식별을 위해 고유한 값이어야 합니다. UUID를 사용하여 고유성을 보장할 수도 있지만 위에 표시된 것과 같이 reverse-DNS 스타일 이름(예: com.company.app)을 사용하는 것이 좋습니다. label은 디버깅할 때 보기 때문에, 의미 있는 텍스트를 할당하는 것이 유용합니다.

The main queue

  • 앱을 실행할 때, main dispatch queue는 자동적으로 생성됩니다. main queue는 사용자 UI를 담당하는 serial queue 입니다. 매우 자주 사용되기 때문에, 애플은 DispatchQueue.main을 통해 클래스 변수로 사용 할 수 있게 설정했습니다.
  • main queue에서 실제 UI작업과 관련되지않은 작업들은 synchronous하게 사용하면 안됩니다. UI를 block하여 잠재적으로 앱의 성능을 저하시킬 수 있습니다.

Quality of Service(QoS)

  • Concurrent queue를 사용할 때 iOS에 이 작업이 얼마나 중요한지 알려주어야 다른 resource를 사용하는 task들의 적절한 우선순위를 지정할 수 있습니다.
  • 애플에서는 QoS에 따른 6개의 global concurrent queue들을 제공합니다. Concurrent queue가 필요하지만 고유한 queue가 필요하지 않은 경우 미리 정의된 queue들을 사용합니다.

.userInteractive

  • 유저와 직접 interact하는 작업에 추천됩니다.
  • UI 업데이트 계산, animations, 빠르게 UI 반응을 해야하는 작업 등등 (빠르게 작업하지않으면 버벅이는 것처럼 보이는 작업들)
  • 이 queue에 제출된 task들은 사실상 즉시 완료되야합니다.

.userInitiated

  • 유저가 UI에서 작업을 즉시 시작해야하지만 async하게 수행될 수 있는 작업을 시작할 때 사용합니다.
  • 문서를 열거나 로컬 DB에서 읽어올 때 유저가 버튼을 클릭한 경우.
  • 이 queue에서 수행되는 작업은 완료하는데 몇초 또는 그 이하가 소요되야합니다.

.utility

  • 장기 실행 중인 연산, I/O, 네트워킹 또는 연속 데이터 피드와 같은 진행률 표시가 포함된 task에 사용할 수 있습니다.
  • 이 시스템은 에너지 효율과 반응성 및 성능 간의 균형을 유지하려고 합니다.
  • 이 queue에서는 작업이 몇 초에서 몇 분 정도 걸릴 수 있습니다.

.background

  • 사용자가 직접 인식하지 못하는 task의 경우 .background queue를 사용해야 합니다.
  • user interaction도 필요없고, 시간에 민감하지도 않은 prefetching, DB 유지관리, 원격서버 동기화 및 backup 수행이 좋은 예입니다.
  • OS는 속도보다 에너지 효율에 초점을 맞춥니다. 이 queue를 사용하여 상당한 시간이 걸리는 작업(몇분 그 이상)을 수행합니다.

.default, .unspecified

  • 이 두가지 선택사항은 명시적으로 사용하지 않습니다.
  • .default는 .userInitiated와 .utility사이에 있으며 QoS의 기본값입니다.
  • .unspecified는 QoS에서 직접 thread를 선택할 수 있는 legacy API들을 위해 존재합니다.

Note: Global queue들은 언제나 concurrent하고 FIFO입니다.

Inferring QoS

  • 아래와 같이 queue를 생성할 때 initializer에서 QoS를 지정할 수 있습니다.
let queue = DispatchQueue(label: label, qos: .userInitiated, attributes: .concuttent)

그러나 OS는 queue에 제출되는 task의 타입을 고려하여 필요에 따라 변경합니다.

  • queue보다 높은 QoS를 가진 task를 제출한다면 queue의 레벨이 상승합니다. 대기중인 queue안에 다른 작업들도 우선순위가 상승하게 됩니다.
  • 만약에 현재 context가 main thread라면, 추론된 QoS는 .userInitiated입니다. QoS를 직접 지정할 수 있지만 QoS가 높은 task를 추가하는 즉시 queue의 QoS서비스가 이에 맞게 상승합니다.

Adding task to queues

  • Dispatch Queue는 task를 queue에 추가하는 sync 및 Async 메서드를 제공합니다.
  • 예를 들어 앱이 시작되면 서버에 연결하여 앱 상태를 업데이트해야 할 수 있습니다. 이는 사용자가 시작하지 않으며 즉시 발생할 필요가 없으며 네트워킹 I/O에 의존하므로 global utitity queue로 전송해야 합니다.
DispatchQueue.global(qos: .utiltity).async { [weak self] in
	guard let self = self else { return }
	
	// Perform your work here
	// ...

	// Switch back to main queue to
	// update your UI
	DispatchQueue.main.async {
		self.textLabel.text = "New atricles available!"
	}
}
  • 위의 샘플 코드에서 주의해야할 두가지 중요한 점이 있습니다.

    • 첫째로, DispatchQueue에는 closure rule을 무효화하는 특별한 것이 없습니다. 따라서 closure안에 캡쳐된 변수(예를 들어 self)들이 적절히 처리되었는지 확인해야합니다.
      GCD안에 strong capture된 self는 closure가 해제될 때 같이 해제되므로 reference cycle(e.g. retain cycle)이 발생하지는 않습니다. 그러나 self의 lifetime이 연장됩니다.
      예를 들어, 잠시동안 해제된 viewController에서 network request를 요청하면 closure는 계속 불릴 것입니다. 만약에 viewController를 weak 캡쳐 했다면 nil일 것입니다. 그러나 strong 캡쳐했다면 viewController는 closure가 끝날 때까지 계속 살아있을 것입니다.

    • 둘째로, 어떻게 UI 를 업데이트하는 지 background queue안에 있는 main queue로 알려주어야 합니다. async type call을 내부에 중첩하는 것은 정상적이고 매우 일반적입니다.

      Note: main queue가 아닌 다른 queue에서 UI 업데이트를 하면 안됩니다. API 콜백이 어떤 queue를 사용하는지 적혀있지 않은 경우, main queue로 dipatch 하세요.

  • sync로 task를 queue에 보낼 때 주의해야합니다. Async 대신에 sync로 호출하는 경우 실제로 sync를 사용해야하는지 두세번 다시 생각해보세요. 현재 queue를 차단하는 작업을 제출하고 현재 queue에 있는 resource에 접근하려고 하면 앱이 교착상태(deadlock)에 빠지게 됩니다. 마찬가지로 main queue에서 sync를 호출하면 UI를 업데이트하는 thread가 차단되어 앱이 멈추게 될것입니다.

Note: main thread sync를 호출하지 마세요, main thread가 block되어 교착상태(deadlock)가 될 수 있습니다.

DispatchWorkItem

  • DispatchQueue에 작업을 제출하는 방법으로 DispatchWorkItem이 있습니다.
let queue = DispatchQueue(label: "abc")
let workItem = DispatchWorkItem {
	print("Dispatch work Item block ran")
}

queue.async(execute: workItem)

Canceling a work item

  • 명시적인 DispatchWorkItem을 사용하는 이유중 하나는 실행 전이나 실행중에 작업을 취소해야하는 경우입니다. cancel()을 호출하는 경우 다음 두가지중 하나가 실행됩니다.
    • queue에 task가 아직 시작되지 않은 경우, task가 삭제됩니다.
    • task가 실행 중인 경우, isCancelled 프로퍼티가 true가 됩니다.

→ isCancelled 프로퍼티를 주기적으로 확인하여 task를 취소하기 위한 적절한 행동을 취해야합니다.

Poor man's dependencies

  • DispatchWorkItem class는 현재 work item이 완료된 후에 실행되어야하는 또 다른 DispatchWorkItem들을 식별하기 위한 notify(queue:execute:) 메서드를 제공합니다.
  • 실행할 후속 작업을 지정할 때에 work item이 실행될 queue를 명시적으로 지정해야합니다.
let queue = DispatchQueue(label: "xyz")
let backgroundWorkItem = DispatchWorkItem { }
let updateUIWorkItem = DispatchWorkItem { }
backgroundWorkItem.notify(queue: DispatchQueue.main, execute: updateUIWorkItem)
queue.async(execute: backgroundWorkItem)

출처: Concurrency by Tutorials Multithreading in Swift with GCD and Operations by Scott Grosch

profile
iOS시작~

0개의 댓글