Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Concurrency

DispatchQueue / GCD / multi threading / 스레드 / 비동기

by Toughie 2023. 5. 26.

 

코드를 짜다 보면 DispatchQueue를 되게 자주 만나고 사용한다.

그런데 이것이 무엇인지에 대한 개념과 이해가 부족했기 때문에 좀 정리하고 넘어가보고자 한다.

 

스레드(Thread)

스레드는 동시에 실행되는 작업의 단위이다.

각각의 스레드는 독립적으로 실행되고, '코드의 흐름을 병렬로 실행'할 수 있다.

-> 만약 스레드가 1개만 있다면 코드의 순서대로 하나씩 위에서 아래로 쫙 실행해야 하는데,
이걸 스레드에 쫙 나눠줘서 동시에 병렬적으로 실행할 수 있다는 것이다. 

 

메인 스레드(Main Thread)

iOS 앱은 기본적으로는 메인 스레드(Main Thread)라는 단일 스레드 환경에서 동작한다.

메인 스레드의 역할은 UI 업데이트, 사용자 입력 처리, 애니메이션, 레이아웃 계산 등이 있다.

즉 사용자 인터페이스에 관련된 작업을 담당하는 스레드인 것이다.

 

메인 스레드에서 막힘이 없어야, 즉각적인 반응과 부드러운 동작을 보장할 수 있다.

-> 메인 스레드에 너무 많은 일을 주면 위험하다.(UI가 버벅이거나 멈춰버리는 문제가 발생 할 수도 있기 때문)

 

그러면 처리해야 하는 작업의 양이 많거나 오래 걸리는 경우 어떻게 해야 할까?

메인스레드에서만 이 작업을 하면 메인스레드가 힘들어 할텐데..

 

백그라운드 스레드(Background Thread)

다행히 앱이 항상 단일 스레드로만 동작하는 것이 아니다.

메인 스레드 이외의 추가적인 다른 스레드들이 작업을 나눠서 처리해줄 수 있다.

이러한 스레드들을 백그라운드 스레드라고 한다. 

백그라운드 스레드는 오래 걸리는 작업이나 네트워킹, 데이터 처리 등에 사용된다.

백그라운드 스레드를 생성하고 관리하기 위해 가장 대표적인 방법이 바로 DispatchQueue이다.

 

그런데 만들어진 스레드들이 어디에서 어떻게 관리되는지 궁금하지 않은가?
나는 궁금해서 찾아봤다..ㅋㅋㅋ

 

스레드풀(Thread Pool)

스레드들이 모여있는 곳을 스레드풀이라고 한다.

스레드풀은 작업을 처리하기 위해 여러 개의 스레드를 미리 생성해두고 관리하는 메커니즘을 가지고 있다.

세부적인 동작이나 정의는 iOS에서 관리되는 부분이 많기에, 기본적인 부분만 짚고 넘어가려 한다.

 

스레드풀은 미리 정의된 개수의 스레드를 생성하고, 이 스레드들에 작업을 할당해서 실행한다.
(보통 CPU 코어나 시스템 리소스를 고려해 최적의 스레드 개수를 설정한다고 한다.)

할당한 작업이 끝나면, 스레드풀은 작업을 끝낸 스레드를 재사용해서 또 다른 작업을 할당한다.

 

스레드를 스레드풀을 통해 관리하는 이유는 무엇일까?

새로운 작업이 있을 때마다 스레드를 생성하고 소멸시키는 것은 비효율적이기 때문이다.(오버헤드)

(테이블뷰 셀도 재사용 하는 것처럼 프로그래밍에서 효율을 위해 재사용 하는 경우가 많은 것 같다.)
또한 스레드풀은 스레드의 생성 및 관리를 추상화해서 개발자가 직접 스레드를 만들고 관리하는 수고를 덜어준다.

 

이제 이를 위한 DispatchQueue에 대해 알아보자.

DispatchQueue.main.async { code }

이 코드는 새로운 스레드를 생성하는 것이 아니다.

'메인 스레드'를 나타내는 것이다. 

메인 스레드에서 클로저(코드블럭)를 비동기적(async)으로 실행하겠다는 말이다.

-> 클로저가 메인 스레드에서 실행되지만 다른 작업들과 병렬로 처리된다는 의미이다.

 

위에 작성한 메인스레드에서 언급했듯이, 메인 스레드는 UI작업을 해야하는 스레드이기 때문에 연산이 많이 필요하거나
오래 걸리는 작업을 메인스레드에서 동기적으로 실행하면 버벅이거나 화면이 멈추는 문제가 생길 수 있다.

 

비유적으로 설명해보면..

메인 스레드는 계속 1개씩 푸쉬업을 하고 있는 상황이다.(푸쉬업이 화면을 그리는 일이라 치자.)

그런데 갑자기 너 버피테스트 100개 지금 당장 해! 하면 버피테스트가 끝날 때까지 푸쉬업을 하지 못한다.(동기적 실행)

메인 스레드의 본업은 푸쉬업이다.

그래서 버피테스트 지금 당장 연속으로 100개 다 할 필요 없고, '푸쉬업 하면서 버피테스트도 하나씩 해' 라고 말하는 것이다.

-> 화면이 버벅이거나 멈추는 것을 방지할 수 있다. (버피테스트도 하지만 푸쉬업을 멈추지 않고 있기 때문!)

 

하지만 이것도 한계가 있는법.. 여기서 갑자기 너 윗몸 일으키기도 100개 같이해! 하면 메인 스레드가 지칠 것이다.

파업 할 수도 있다.(메인스레드에 부하가 심하면 앱 크래시가 날 수도 있음)

 

그렇기 때문에 윗몸 일으키기는 다른 스레드에 시키자.

 

+ 더 정확히 이해하기 위해서는, 이벤트 루프 개념이 필요하다.

이벤트 루프(Event Loop)

메인 스레드에는 이벤트 루프라는 개념이 있다.

이벤트 루프는 사용자의 입력이나 다른 이벤트들을 기다리고 처리하는 역할을 수행한다. (무한 반복문임)

DispatchQueue.main.async를 사용하면 작업이 메인 스레드의 이벤트 루프에 비동기적으로 등록된다. 

이때 등록된 작업은 이벤트 루프가 유휴 상태일 때 실행된다.

이벤트 루프는 무한 반복문으로 동작하며, 이벤트가 발생할 때까지 대기한다.

ex. 사용자가 버튼을 클릭하거나 다른 UI 이벤트가 발생하면 이벤트 루프는 해당 이벤트를 감지하고 처리함.

 

DispatchQueue.main.async로 등록된 작업은 이벤트 루프에서 순차적으로 실행되며, 메인 스레드에서 처리된다.

이벤트가 발생했을 때는 이벤트를 우선적으로 처리하고, 이벤트가 없는 경우에 할당된 작업을 처리한다는 .
(작업들이 정말 동시에 처리되는 것이 아니라 루프 안에서 순차적으로 효율적으로 처리 됨)

 

DispatchQueue.global().async { code }

윗몸 일으키기를 대신 해줄 스레드를 만들어보자.

새로운 백그라운드 스레드를 생성하는 것이다. (이 스레드에 빡센 작업을 할당해주자_ex. 윗몸 일으키기)

DispatchQueue.global()은 메인 스레드 이외의 글로벌 디스패치 큐에 대한 참조를 반환한다.

 

글로벌 디스패치 큐는 위에서 언급한 스레드풀에서 작업을 실행하는데 사용된다.

DispatchQueue.global().async로 작업을 예약해두고 작업이 완료되면 해당 스레드는 다른 작업 처리를 위해
재사용될 수 있다.

 

이제 메인 스레드에서 비동기적으로 코드를 실행하고, 새로운 백그라운드 스레드에 작업을 할당하는 방법은 알았는데

그럼 DispatchQueue는 뭘까?

https://developer.apple.com/documentation/dispatch/dispatchqueue

 

DispatchQueue | Apple Developer Documentation

An object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.

developer.apple.com

* GCD와 동시성 프로그래밍은 추후 더 심화적으로 공부할 예정으로 간단하게 정리만 하고 넘어감.

 

DispatchQueue는 다중 스레드 작업을 쉽게 관리하게 해주는 기술인 
Grand Central Dispatch(GCD)를 사용해서 비동기적으로 작업을 실행하기 위한 클래스이다.

 

DispatchQueue를 사용해서 '작업을 예약'하면 GCD가 작업을 실행할 스레드를 자동으로 선택하고 작업을 관리한다.

DispatchQueue는 작업을 대기열에 추가하고, 이를 기반으로 스레드를 선택하고 작업을 실행한다.

(DispatchQueue를 사용한 코드에서Debug Navigator에서 스레드 현황을 보면 스레드 넘버가 계속 바뀌던데
이것이 위 메커니즘에 의한 것으로 생각된다.)

 

DispatchQueue의 유용성

1. 비동기 작업 관리

비동기 작업을 예약해서 작업들을 백그라운드에서 병렬적으로 실행할 수 있다.

-> 앱의 성능과 응답성 향상

2. 작업 우선순위 및 순서 조정

작업의 중요도에 따라 더 높은 우선순위를 부여하거나 원하는 순서로 실행할 수 있다.

 

3. 스레드 안정성

DispatchQueue는 내부적으로 스레드 안정성을 보장한다.

여러 스레드에서 동시에 DispatchQueue에 접근해서 작업을 예약해도 안전하게 처리된다.

*Race Condition을 방지하기 위해 Thread Safety를 보장함.

How?

DispatchQueue는 작업 실행을 위해 내부적으로 큐(Queue)를 사용함.

큐는 작업을 순서대로 보관하고 작업을 실행할 스레드를 관리한다.

큐에 있는 작업은 FIFO(First-In-First_Out)순서로 실행되기 때문에 작업 간 경합현상이 발생하지 않는 것

(but 작업 내부에서 공유된 자원에 대한 접근의 경우, 스레드 안정성을 추가적으로 고려해야할 수도 있음)

 

DispatchQueue의 종류

위에서 소개했듯

1. main

메인 스레드와 연결된 디스패치 큐

주로 UI업데이트와 사용자 이벤트 처리에 사용됨.

2. global

백그라운드에서 비동기적인 작업을 실행하는 데 사용됨.

다수의 글로벌 디스패치 큐가 존재하며, 각 큐는 우선순위에 따라 다른 스레드에서 작업을 실행함.

 

+ QOS(Quality of Service)_ 우선순위 설정

qos는 작업을 예약하는 데 사용되며, 각각 다른 우선순위로 작업을 처리하는 글로벌 디스패치 큐를 생성한다.

*우선 순위가 높은 순으로 정리

1. userInteractive

유저 상호작용을 위한 작업. 가장 높은 우선순위 부여.

즉각적 응답이 필요하거나 UI 업데이트나 애니메이션 등이 해당될 수 있음.

2. userInitiated

사용자가 시작한 작업에 대한 우선순위

직접적인 사용자 입력에 응답하거나 작업이 시작될 떄 적합함.

(사용자가 인지하고 시작하겠지만, 자유로운 앱 사용에 순간적인 제한이 생길 수 있으므로 신중히 사용하는 것이 좋아보임)

 

3. default

기본 우선순위. userInitiated보다 낮은 우선순위.

DispatchQueue.global()로 사용하는 경우와 동일

 

4. utility

보조작업에 대한 우선순위

오래 걸리는 작업이나 네트워크 요청 등

 

5. background

UI 업데이트와 상호작용이 필요없는 작업. 가장 낮은 우선순위.

 

GCD는 위 우선순위를 고려해서 작업을 처리하고 자원을 할당함.

더 높은 qos를 가진 작업은 더 높은 우선순위의 스레드에서 실행될 수 있음.

 

 

마지막으로 Xcode에서 시각적으로 CPU나 메모리 사용량, 그리고 스레드 현황을 확인할 수 있는

Debug Navigator에 대해 살펴보자

일반적으로 비동기 처리를 하지 않으면 Thread 1, 즉 메인 스레드에서만 변화를 관찰할 수 있다.

(특정 함수를 실행, 스크롤링 등을 할 때)

 

DispatchQueue를 활용해서 멀티 스레딩 비동기 처리를 해주면 아래와 같은 그래프를 볼 수 있다.

 

오랜만에 포스팅이 정말 길었다..😅

그래도 평소에 궁금했던 의문증들을 꽤 많이 해결한 시간이라 뿌듯하다.