왜 멀티스레딩이 필요한가?
아이폰에는 여러 개의 스레드가 존재합니다. 그런데 왜 앱이 버벅일까요? 🤔
간단한 비유로 설명해보겠습니다. 회사에 일꾼이 여러 명 있는데, 한 사람에게만 "야 너 다해!"라고 모든 일을 맡기면 어떻게 될까요?
당연히 그 사람은 "아... 할 게 너무 많아..."하며 과부하가 걸리겠죠.
iOS에서도 마찬가지입니다. 특히 메인 스레드는 UI 업데이트와 사용자 이벤트 처리라는 핵심 업무를 담당하는데,
여기에 무거운 작업까지 몰아주면 앱이 버벅이게 됩니다.
저도 처음에는 "스레드가 여러 개면 괜찮지 않나?"라고 생각했는데, 실제로는 작업을 어떻게 분산하느냐가 핵심이더라고요 💡
해결책은 간단합니다: 다른 스레드로 작업을 분산하면 됩니다!
iOS에서는 직접 스레드를 관리할 필요가 없습니다. 단지 작업(Task)을 대기행렬(Queue)에 보내기만 하면 OS가 알아서 처리해줍니다.
메인 스레드
메인 스레드에서만 UI 작업을 해야 하는 이유는 Thread Safety 때문입니다.
Thread Safe란? 여러 스레드가 동시에 접근해도 프로그램 실행에 문제가 없는 상태를 말합니다.
하지만 Swift의 대부분 UI 타입들은 Thread Safe하지 않습니다. 만약 UI 업데이트를 여러 스레드에서 동시에 수행한다면:
- 각 스레드의 UI 변경 작업이 충돌할 수 있음
- 스레드 간 동기화가 이루어지지 않아 UI가 올바르게 업데이트되지 않음
- 예를 들어, 한 스레드가 버튼 색상을 바꾸는 동안 다른 스레드가 같은 버튼의 텍스트를 바꾸면 크래시 발생 가능
UI가 화면에 그려지는 과정: View Drawing Cycle
그럼 실제로 UI가 어떻게 그려지는지 살펴볼까요?
- 이벤트 처리: 사용자의 터치 제스처에 따른 코드 실행
- UI 업데이트: 1번의 결과를 UI 요소에 반영
- 화면 반영: 가장 복잡한 단계로, 다음 과정을 거칩니다
- Core Animation 트랜잭션 커밋: 변경된 UI를 트랜잭션으로 그룹화
- GPU 전달: Core Animation이 변경사항을 GPU로 전달
- 화면 렌더링: GPU가 디바이스 화면에 렌더링 (일반적으로 60Hz, iPhone Pro는 120Hz)
이 모든 과정이 Main Run Loop를 통해 디스플레이 주기와 동기화됩니다.
GCD(Grand Central Dispatch) 깊이 파보기
GCD는 iOS에서 제공하는 강력한 멀티스레딩 API입니다. 작업을 큐에 추가하면 iOS가 알아서 최적의 스레드로 분배해서 처리해줍니다.
GCD에는 크게 두 가지 큐가 있습니다
Main Queue vs Global Queue
1. Main Queue
DispatchQueue.main.async {
// UI 업데이트 코드
self.label.text = "업데이트 완료!"
}
특징:
- Serial Queue: 한 번에 하나의 작업만 처리
- 메인 스레드에서 실행
- UI 업데이트와 사용자 이벤트 처리 전용
2. Global Queue
DispatchQueue.global().async {
// 무거운 작업 (네트워크 통신, 데이터 처리 등)
let result = heavyTask()
// UI 업데이트는 메인 큐에서!
DispatchQueue.main.async {
self.updateUI(with: result)
}
}
특징:
- Concurrent Queue: 여러 작업을 동시에 여러 스레드에서 처리
- QoS(Quality of Service)에 따라 우선순위 조절 가능
- 백그라운드 작업에 적합
QoS(Quality of Service) 이해하기
// 높은 우선순위 (사용자 인터랙션과 직접 관련)
DispatchQueue.global(qos: .userInteractive).async { }
// 사용자가 시작한 작업
DispatchQueue.global(qos: .userInitiated).async { }
// 기본값
DispatchQueue.global(qos: .default).async { }
// 낮은 우선순위 (백그라운드 작업)
DispatchQueue.global(qos: .utility).async { }
DispatchQueue.global(qos: .background).async { }
동기(sync) vs 비동기(async)의 함정들
이 부분이 정말 중요한데, 처음에 이해하기 어려웠던 부분이기도 해요.
데드락의 위험성
절대 하면 안 되는 코드:
따로 메인쓰레드에 sync 작업을 넣어주게 되면 데드락이 걸리며 앱이 강제 종료된다.
DispatchQueue.main.sync {
DispatchQueue.main.sync {
print("이 코드는 실행되지 않음")
}
}
왜 데드락이 발생할까요?
- 현재 메인 스레드에서 실행 중
- 메인 큐에 sync 작업을 추가
- sync는 작업이 완료될 때까지 기다림
- 하지만 메인 큐는 현재 작업이 끝나야 다음 작업을 처리할 수 있음
- 현재 작업은 sync 작업이 끝나야 완료될 수 있음
- 무한 대기 상태 = 데드락!
언제 DispatchQueue.main.sync를 사용할까?
1. 백그라운드 쓰레드에서 메인 큐 작업이 필요할 때
• 예를 들어, 백그라운드 작업에서 데이터를 처리하고, 처리된 데이터를 메인 스레드(UI)에서 업데이트해야 할 때
DispatchQueue.global().async {
// 백그라운드에서 데이터 처리
let processedData = processData()
// UI 업데이트는 반드시 메인 큐에서 실행해야 함
DispatchQueue.main.sync {
updateUI(with: processedData)
}
}
왜 sync쓰는데? 데이터 처리가 끝난 후 UI가 즉시 업데이트 되어야 하기에.
근데 왜 이경우는 데드락이 아닌가? 글로벌 큐에 비동기로 넣었기에 즉시 쓰레드가 반환이 된다. 그렇기에 메인 큐는 비어있다.
2. UI 업데이트 작업의 순서를 보장할 때
DispatchQueue.global().async {
// 첫 번째 작업
DispatchQueue.main.sync {
updateLabel(with: "First Update")
}
// 두 번째 작업
DispatchQueue.main.sync {
updateLabel(with: "Second Update")
}
}
하지만 sync자체가 여러 글로벌큐에서 비동기적으로 코드를 실행하다 메인큐를 여러개에서 접근하면 또다시 데드락 발생 위기가 있기에 정말정말 순서가 중요하고 개발자가 잘 파악한경우 이외에는 DispatchQueue.main.async를 통해 UI를 업뎃해줄수 잇다.
2. Global Queue
global큐는 메인과 달리 concurrent(큐가 받아들이는 작업을 여러쓰레드에 보내는 )한 queue이다.
DispatchQueue.global().async : 글로벌 큐에 비동기적으로 보내꺼야~, 뒤의 클로저를(이 클로저 하나에 묶이는게 Task단위) 하나의 작업 뭉텅이이기에 여기서는 순서대로 된다. 비동기는 다른쓰레드를 보내고 즉시리턴을 받기에 다른 일을 할수 있다
DispatchQueue.global().sync{} : 메인쓰레드에서 다른 큐로 보낼때 이렇게 sync로 보내게 된다면 1번에서 했던 마찬가지 데드락이 발생한다. 또한 현재와 같은 큐에 sync를 보내면 안된다.
DispatchQueue.global().async{
//A
DispatchQueue.global().sync{
//B
}
}
위 코드에서 global 큐에 비동기적으로 A 작업을 보냈습니다. 그런데 그 안에서 sync로 동기적으로 B 작업을 보내고 있습니다. 이때 A 작업은 비동기적으로 실행되어 즉시 리턴되지만, B 작업은 동기적으로 실행되므로 A 작업이 끝나야 B 작업이 완료됩니다.
하지만 중요한 문제는 같은 큐에서 sync로 작업을 보낼 경우, B 작업이 실행되기 위해서는 global 큐에서 스레드가 비어야 하는데, global 큐의 스레드는 A 작업이 실행되는 동안 이미 사용되고 있습니다. 그래서 B 작업이 기다리게 되어, A 작업이 끝나지 않으면 B 작업이 실행될 수 없게 되는 데드락이 발생합니다.
그러면 또 애플이 왜 global.sync를 만들었는데;;
1. 빠르게 처리할 작업이 필요할때
2. 현재쓰레드에서만 작업을 해야할때
'면접준비' 카테고리의 다른 글
Swift Concurrency - Async/Await⭐️ (0) | 2023.08.08 |
---|---|
디스패치 그룹 (0) | 2023.07.02 |
Actor🕴🏻 (0) | 2023.06.29 |
Frame vs Bounds (1) | 2023.06.04 |
Socket 통신 개념+구현까지(서버는 nodejs) (1) | 2023.06.03 |