Multi-Threading?
짧은 시간에 복잡하고 많은 작업을 실행하여 사용자에게 좋은 경험을 제공하기 위해,
작업을 여러 프로세스에서 동시에 실행시키는 병렬 처리 방법!
iOS에서도 역시 Multi-Threading을 지원한다.
iOS에서 사용하는 Multi-Threading
대표적인 3 가지!
- GCD (Grand Central Dispatch)
- Operation, OperationQueue (NSOperation)
- Swift Concurrency
먼저 하나씩 알아보자!
GCD (Grand Central Dispatch)
https://developer.apple.com/documentation/dispatch
GCD는...
- iOS 8.0+
- 저수준 큐 기반
- 멀티코어 환경과 멀티스레드 환경에서 최적화된 프로그래밍을 할 수 있도록 애플이 개발한 기술
- 어떻게 사용할지 (일의 순서, 동기/비동기 처리) 정해주면 스레드를 관리해준다.
- GCD 사용은 Dispatch라는 프레임 워크를 사용하고 DispatchQueue라는 클래스를 주로 사용한다.
사용법
// 동기, sync
DispatchQueue.main.sync { ... }
DispatchQueue.global().sync { ... }
// 비동기, async
DispatchQueue.main.async { ... }
DispatchQueue.global().async { ... }
자세한 내용은 이전에 정리했던 노션 글 참조! (야곰 강의 정리본)
https://galvanized-jeep-eb7.notion.site/Concurrency-Programming-cc51f4320fd149acab2e44e20d3ba647
Operation, OperationQueue (NSOperation)
https://developer.apple.com/documentation/foundation/operation
Operation은...
- iOS 2.0+
- GCD를 객체지향적으로 재탄생 시킨것
- 동시성 프로그래밍에 관련한 작업을 Operation이라는 객체로 만들어 놓고 사용할 수 있다.
- Operation을 상속받아 커스텀 클래스로 직접 만들거나 BlockOperation이라는 하위 클래스를 사용해 사용가능
- GCD에도 작업을 객체로 만드는 DispatchWorkItem이 있다... 다른점은?
- 일단 Operation은 DispatchWorkItem 보다 먼저 나옴
- Operation은 객체지향적으로 설계되어 더 세부적인 스케쥴링 가능
- 세부적인것 중 대표적인 부분은 Operation 상태추적을 위한 다양한 프로퍼티가 존재함
사용법
let operation = BlockOperation { ... } // 생성
operation.addExecutionBlock { ... } // 첫번째로 초기화 한 위 작업이 끝나면 실행됨.
operation.completionBlock = { ... } // ExecutionBlock이 모두 실행된 뒤 호출되는 Property
operation.start() // 직접 실행하는 방식
let operationQueue = OperationQueue() //OperationQueue에 넣어 실행하는 방식
operationQueue.addOperation(operation)
자세한 내용은 이전에 정리했던 노션 글 참조! (야곰 강의 정리본)
https://galvanized-jeep-eb7.notion.site/Operation-3df4b450f95a4db985b5c193838c6b30
Swift Concurrency
https://developer.apple.com/documentation/swift/concurrency
Concurrency는...
- iOS 13.0+
- 구조적 동시성(Structured Concurrency)을 지향
- GCD의 복잡함을(콜백) 줄이고 가독성, 안전성을 보장한 비동기 코드를 작성할 수 있도록 만듦.
- 스레드, 큐를 직접 다루지 않아도 비동기 작업을 간단하고 명확하게 구현가능.
- 직관적인 문법제공 -
async,await,Task,actor등의 키워드
이것도 이전 스터디 노션 정리내용으로!
강의영상 저작권 때문에 링크는 X
잘 몰랐지만 직접 쓰레드를 생성하는 방법도 있었다!
let current = Thread.current
print("current thread", current, current.stackSize)
let newTread1 = Thread()
newTread1.name = "secondThread"
print("secondary thread with default size", newThread1, newThread1.stackSize)
let newTread2 = Thread()
newThread2.name = "thirdThread"
newThread2.stackSize = 4096 * 512
print("secondary thread with default size", newThread2, newThread2.stackSize)
/* Thread class also has...*/
Thread.isMainThread //Bool
DispatchQueue, OperationQueue의 차이
| DispatchQueue | OperationQueue | |
|---|---|---|
| 작업 간 의존성 설정 | 불가능 | addDependency 메서드 사용해 가능 |
| 작업 취소 기능 | 수동구현 | cancel로 명시적 취소 가능 |
| 우선순위 설정 | qos로 제한적 가능 | queuePriority qualityOfService 설정으로 상세하게 가능 |
| 재사용/재시작/중단 | 불가능 | Operation으로 가능 |
| 결론 | 간단한 작업에 적합 | 복잡한 작업에 적합 |
Race Condition
Race Condition은 두 개이상 스레드가 하나의 자원에 동시에 접근 및 수정하려 할 때 발생하는 문제
그럼 각 Multi-Threading 방식에서는 어떻게 방지할 수 있는지 알아보자
GCD
Serial Queue 사용
Serial Queue를 사용하면 같은 큐에 실행되는 작업은 순차적으로 실행되기 때문에 동시접근이 발생 안함
let serialQueue = DispatchQueue(label: "com.example.myQueue")
serialQueue.async {
// 공유 자원에 안전하게 접근
}
Dispatch Semaphore 사용
운영체제 과목을 공부하다보면 나오는 Semaphore와 같은 개념
특정 자원에 접근 가능한 스레드 수를 제한하는 방식
하나의 스레드가 접근하여 작업을 하는동안 다른 스레드는 대기하다가 들어갈 수 있게 되면 작업을 시작함.
let semaphore = DispatchSemaphore(value: 1) //접근 가능한 수 설정으로 이해하면 됌
func updateSharedResource() {
semaphore.wait() // 접근 시작
sharedResource += 1
semaphore.signal() // 접근 종료
}
Operation
Operation 의존성 사용
그냥 동시에 실행되지 않게 실행순서를 보장하는 방법
let op1 = BlockOperation { /* 공유 자원 수정 */ }
let op2 = BlockOperation { /* 이후 작업 */ }
op2.addDependency(op1)
queue.addOperations([op1, op2], waitUntilFinished: false)
NSLock 사용
Lock을 사용해 명시적으로 접근을 제어가능
let lock = NSLock()
let operation = BlockOperation {
lock.lock()
// 공유 자원 접근
lock.unlock()
}
Swift Concurrency
Actor 사용
actor는 내부 상태를 단일 태스크만 접근 가능하도록함.
actor Counter {
private var count = 0
func increment() {
count += 1
}
func value() -> Int {
return count
}
}
// 사용
let counter = Counter()
await counter.increment()
let current = await counter.value()
메인 스레드에서 UI 업데이트를 해야 하는 이유는 무엇일까?
일단 iOS에서 메인 스레드는 UI, event 처리를 모두 담당하고 있다.
좋은 사용자 경험 및 일관성을 위해,
프레임워크 자체가 메인스레드에서 안전하게 작동되도록 설계되었기 때문이다.
UI업데이트를 병렬로 처리한다면 Race Condition이 발생 할 수 있다.
물론 방지하는 방법을 쓰면 괜찮겠지만, 어쩔 수 없이 지연이 생기게 될 것이다.
그렇게되면 일관성 및 사용자 경험이 저해되는 상황이 발생하게 된다.
번외로 메인스레드에서 오래걸리고 복잡한 로직을 (UI아님) 실행하게 된다면
메인 스레드 블락 현상이 발생한다. 그럼 화면 터치도 안되고 앱 자체가 작업이 끝날때 까지 멈추게 된다.
이를 방지하기 위해 메인스레드에서 UI 또는 이벤트 작업 외에는 동시성을 사용하는게 좋다.