섹션0. GCD/Operation에 앞서
대기행렬(큐)
GCD(=DispatchQueue)와 Operation Queue 2가지 종류가 있다.
GCD는 간단한 일을 하고, 함수를 사용하는 메소드 위주의 작업을, Operation은 복잡한 일을 위주로 한다.
GCD를 기반으로 해서 여러가지 기능이 추가되어 Operation이 나온 것이다.
그러면 스레드는 뭘까? 스레드는 일하는 녀석이다. 그동안 우리는 스레드가 여러개인데도 프로그래밍을 잘못해서 하나의 스레드만 사용한 것이다. 그치만 우리가 짠 앱 수준에서는 충분히 스레드 하나로 처리가 가능한 정도였다.
그러면 어떻게 다른 스레드로 작업을 분산시켜서 동시에 일을 하게 할 수 있을까? 답은, 대기행렬(큐)에 보내기만 하면 된다.
보내면 알아서 대기행렬을 사용해 시스템이 스레드 숫자 관리를 할 것이다.
결론, 우리가 할 일은 작업을 대기행렬! 큐!로 보내자~!
1번 스레들에서 하고 있던 작업을 2번 스레드로 보내는 것이 우리가 할 일이라는 것이다.~
그러면 어떻게 대기행렬로 보낼 수 있을까?
<클로저에 있는 작업을 global queue/main queue로 비동기적으로/동기적으로 보낼 거야~>
섹션1. sync/async 그리고 serial/concurrency
sync/async
쉽게 말하자면,
- sync는 다른 스레드에서 처리되고 있는 일이 다 끝날 때까지 기다렸다가 처리하는 것이고,
- async는 다른 스레들에서 처리되고 있는 작업이 완료되는 것을 기다리지 않고 일을 동시에 처리하는 것이다.
serial은 분산해서 처리하는데 다른 하나의 스레드에 분산하는 것이고, concurrency는 여러개의 스레드로 분산해서 처리하는 것이다.
<비동기란 개념이 필요한 이유? -> 네트워크 작업 때문>
실생활 예시로 빗대어서 설명하면,
카페 알바생이 sync로 일하면 한 명의 알바생이 하나의 음료를 다 만들고 그 다음 음료를 만들었다.
async는 카페 알바생이 주문이 여러개 들어오면 스무디가 만들어지고 있는 동안에 다른 아메리카노를 만드는 것이다.
serial/concurrency
직렬과 동시는 큐의 특성에 대해 말하는 것이다.
직렬큐 : 단 하나의 다른 스레드로만 보내는 특성을 가진 대기열 (순서가 중요한 작업)
동시큐 : 여러 스레드를 사용해서 작업을 분산 처리하는 큐 (독립적이지만 유사한 여러개의 작업을 처리)
serial은 알바생이 한 명 더 있는데 딱 한 명 더 있는 것이다.
concurrent는 다른 알바생이 여럿 더 있어서 동시에 여러 알바생이 만들 수 있는 것이다.
섹션2. 디스패치큐(GCD)의 종류와 특성
대기열의 종류가 여러개로 작업의 특성에 따라서 원하는 큐로 작업을 보내면 된다.
(1). main queue : serial, 메인 스레드, 1번을 의미
(2). global queue : concurrent(6가지), Quality of Service - 중요도가 존재
- DispatchQueue.global(qos: .userInteractive)
- 사용자와 상호작용하는 작업들
- 거의 즉시 처리해야 하는 작업들
- UI업데이트, 애니메이션, UI반응과 같은 것
- DispatchQueue.global(qos: .userInitiated)
- 사용자가 필요하긴 하지만 1번과 다르게 약간의 시간이 좀 더 걸리는 것
- pdf를 여는 몇 초가 걸리는 작업 → 비동기적으로 처리가 필요
- DispatchQueue.global()
- 디폴트
- 일반적인 작업 → 잘 모르겠으면 여기로 보내
- DispatchQueue.global(qos: .utility)
- progress indicator와 함께 실행되는 작업들
- ex) 연산, 네트워킹 작업들
- 몇 초에서 몇 분의 작업 처리 시간이 걸림
- DispatchQueue.global(qos: .background)
- 속도보다는 에너지 효율성을 중시하는 작업들
- 데이터를 미리 가져오거나 유지하는 것들
- 몇 분 이상
- DispatchQueue.global(qos: .unspecified)
- 명시적으로 사용하면 안된다고 함 → legacy API
func task1() {
print("Task 1 시작")
sleep(2)
print("Task 1 완료★")
}
func task2() {
print("Task 2 시작")
print("Task 2 완료★")
}
func task3() {
print("Task 3 시작")
sleep(1)
print("Task 3 완료★")
}
let userInteractiveQueue = DispatchQueue.global(qos: .userInteractive)
let userInitiatedQueue = DispatchQueue.global(qos: .userInitiated)
let defaultQueue = DispatchQueue.global() // 디폴트 글로벌큐
let utilityQueue = DispatchQueue.global(qos: .utility)
let backgroundQueue = DispatchQueue.global(qos: .background)
let unspecifiedQueue = DispatchQueue.global(qos: .unspecified)
timeCheck {
defaultQueue.async {
task1()
}
defaultQueue.async(qos: .userInitiated) {
task2()
}
defaultQueue.async {
task3()
}
}
- 2번째의 작업의 우선순위가 다른 것보다 높아서 먼저 출력됨
- 그리고 async이기 때문에 0.0003019571304321289초 정도 걸림
- 순서대로 시작했지만 끝나는 순서는 정확하게 알 수 없는데 이유는 defaultQueue이기 때문에 여러개의 스레드로 만들어서 어떤 것이 먼저 끝나는지 정확하게 장담할 수 없음
큐의 종류보다는 6개의 다른 종류로 작업을 보내서 일을 빨리 빨리 처리하려는 개념이 중요한 것
그리고 OS가 QoS를 굳이 설정 안해도 알아서 작업의 서비스 품질을 추론함
(3). custom queue : serial, 원하면 concurrent로 바꿀 수 있음, QoS 설정도 가능
섹션3. 디스패치 사용 시에 주의해야 할 사항
1. UI 관련된 일은 무조건 main queue에서 작업해야 한다.
(단, playground에서 UI작업은 main이 아니라 global에 작업을 해야 한다.)
하나의 스레드로 해야 간섭이 일어나지 않는다.
이미지를 다운 받는 것 후에 이미지를 넣는 작업은 DispatchQueue.main에서 한다는 것을 볼 수 있지!
2. sync 메서드 주의사항
2-1. 메인큐에서 다른큐로 보낼 때는 sync메서드를 부르면 안된다. = 메인큐에서는 항상 비동기적으로 보내야 한다.
왜냐하면, 메인큐에서 동기적으로 보낼 경우에는 예를 들어, 네트워크 같은 작업을 기다리기 때문에 하던 일을 멈추고 UI가 멈춰서 버벅이는 일이 발생할 수 있기 때문이다.
해당 코드 금지
DispatchQueue.main.async {
DispatchQueue.global().sync {
}
}
2-2. 현재의 큐에서 현재의 큐로 동기적으로 보내면 안된다. -> 교착상황이 발생할 수 있기 때문
(이 부분 이해가 완벽히 가지 않음)
3. 작업을 보내는 내용이 클로저이고, 캡처현상이 발생하기 때문에 주의해야 한다.
let viewController = ViewController()
객체를 생성하면 가리키는 녀석이 하나라서 ARC가 하나 올라갈 것임.
- weak self로 한 경우, ARC가 올라가지 않을 것임,
왜냐하면 weak이기 때문에, 그래서 뷰컨을 dismiss를 해주면 내부 작업도 중단이 되고, 뷰컨에 속하고 있던 작업도 없어질 것이다.
- 근데 weak self로 해주지 않을 시에 strong 캡처가 발생하고 ARC가 하나 올라간 상태이고 뷰컨이 없어져도 클로저 내부에서는 뷰컨을 가리키고 있고, 뷰컨이 dismiss됐음에도 여전히 클로저 안에서 뷰컨이 살아서 동작하고 있는 상태가 된다.
그래서 꼭 weak self로 비동기적 작업을 처리해주는 것이 좋다.
4. 비동기 작업에서 completion handler 존재 이유
어떤 작업이 끝났음을 알리는 클로저 작업이다.
왜 필요한가?
비동기 작업을 처리해주기 위한 것인데, task1이 끝나는 시점을 알아야 하고, 실제로 그 시점을 알아서 써먹어야 한다.
그래서 completion handler를 통해서 비동기작업이 명확하게 끝나는 시점을 활용하기 위함이다.
따라서, 비동기 작업이 있으면 모두 completion handler가 있다.
5. 동기적 함수를 비동기함수처럼 만드는 방법은?
원래 동기적인 함수인데 너무 오래 걸리다보니, 비동기적으로 처리할 수 있게 해주고 싶은 것이다.
URLSession과 같은 비동기 함수는 GCD/Operation이 이미 필요 없다. 왜냐하면 이미 내부적으로 비동기처리된 함수이다.
'⭐️ 개발 > iOS Concurrency' 카테고리의 다른 글
[GCD] Swift Concurrency - GCD (1) | 2022.09.02 |
---|---|
[GCD] Dispatch Group (4) | 2022.08.29 |
[GCD] weak self (1) | 2022.08.20 |