동시성(Concurrency)을 대하는 Swift 의 자세 1 - DispatchQueue

2024. 2. 2. 05:54Mobile/Swift

요즘 Rx 에 대해 조금씩 곰튀김님 유튜브 강의를 통해 살펴보고, 동시성 글을 읽게 되면서, RxSwift 와 Combine 은 왜 쓰는걸까? 하는 의문이 들었다. 저 두 라이브러리 또한 비동기로 수행할 작업들을 기다리지 않고 적절한 시기에 동시적으로 수행할 수 있고, 코드의 가독성도 좋아져서일 것 같다.  동시성의 사용과 경계에 대해서는 클린코드에서 알아봤다.

 

(클린코드 동시성 글이 궁금하시다면 링크 클릭 하세요)

 

애플 또한 동시성을 사용함으로써 얻게 되는 성능적인 측면과 그로 인해 발생할 수 있는 경쟁 상태와 시스템 부하 외 콜백 함수를 사용할 때 발생하는 들여쓰기로 인해 보이는 예쁘지 않은 코드에 대해서도 대응을 했다. (그 방법은 async, await 와 연관이 있다😅, 여기서 언급하게 될 부분은 GCD 이기 때문에 다음에 다뤄봐야겠다.) 

 

동시성과 연관된 키워드들이 있기에 아래 나열해봤다. 이 글에서 이걸 모두 언급하거나 살펴볼 수는 없다. 그만큼 동시성 프로그래밍은 깊은 영역인 것 같다. 그래서 나눠서 살펴봐야 할 것이다. 

 

 

동시성(Concurrency) 에 관한 글을 작성하면서  앨런 강의,  문서, 다른 분들의 글, 예시 코드 등을 쳐보고 실행하면서  살펴봤다. 한번에 이렇게 동시성을 사용하고, 여기서 이것을 조심하면 될 것이다. 라고 주는 명쾌한 글은 아니다.

 

예전부터 들어는 왔지만, 짚고 넘어가면 좋을 것 같아서, 동시성 관련 강의도 먼저 들어보고 공식 문서글과 블로그 글들을 여럿 읽어봤다.  나의 개인적인 iOS 에 대한 갈증을 해소해주는 글이 될 것 같다. 

형태는 보이지만, 명확하게 잡기는 어려운 듯하다. 차근차근 타이틀을 던져보고 참고해서 적어나가는 그런 글이 될 것이다..ㅎㅎ 

 

내가 iOS 동시성 프로그래밍과 연관된 첫 키워드로 잡은 녀석은 바로 DispatchQueue 다. 애플에는 동시성 프로그래밍을 접근할때 현명하게 개발자가 조절하는 부분, 애플에서 설정해주는 부분 등이 있다. 그 부분들에 대해 앞으로 문서와 다양한 의견, 코드 등을 통해 살펴보려고 한다.

 

사전적 정의

Dispatch + Queue 가 합쳐진 이 녀석은  앱의 main thread(메인스레드) 또는 background thread 에서 순차적 또는 동시에 작업의 실행을 관리하는 개체 라고 공식문서에 정의되어 있다.  DispatchQueue 는 GCD, Grand Central DispatchQueue  라고도 한다.

 

Dispatch(디스패치)는 운영체제적 접근에서 준비상태에서 프로세스 중 실행될 프로세스를 선정하는 역할을 한다.

Queue 는 선입선출(FIFO), 문자 그대로 먼저 들어온 작업은 먼저 처리되고, 처리가 끝날 때까지 다음 것은 대기 상태에 놓인다. 

애플은 DispatchQueue 를 만들어 순차적으로도 작업을 수행하지만 동시에도 작업을 수행할 수 있도록 해놓았다. 

 

class DispatchQueue: DispatchObject

 

DispatchObject 타입은 DispatchQueue, DispatchGroup, DispatchSource 등 다양한 유형이 잇다. 기본 디스패치 객체 인터페이스를 사용하면 메모리 관리, 실행(activate),일시중지(suspend), 재개(resume), 객체 컨텍스트 정의, 작업 데이터 기록 등의 작업을 수행할 수 있다. 

 

디스패치 큐는 애플리케이션이 블록 객체 형태로 작업을 제출할 수 있는 FIFO 큐다. 디스패치 큐에 제출된 작업은 시스템에서 관리하는 스레드 풀에서 실행된다. 앱의 메인 스레드를 나타내는 디스패치 대기열을 제외하고는 시스템에서 작업을 실행하는 데 사용하는 스레드에 대해 보장하지 않는다.

 

스레드 풀(Thread Pool): 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리하는 것


작업 항목을 동기식 또는 비동기식으로 예약(스케쥴링)할 수 있다. 작업 항목을 동기식으로 예약하면 해당 항목이 실행을 완료할 때까지 코드가 대기하고, 작업 항목을 비동기적으로 예약하면 작업 항목이 다른 곳에서 실행되는 동안 코드가 계속 실행된다.

간단한 코드 예시는 아래에 적어뒀다. 

func executeFunc() {
    print("1")
    DispatchQueue.main.async {
        print("2")
        print("===비동기 실행===")
    }
    print("3")
}


executeFunc()

 

위에 함수를 실행하면 찍히게 되는 코드는 어떻게 될까? 

더보기

결과는 
1

3

2

비동기 코드는 순차적으로 실행되는게 아니라,  알 수 없는 시간에 호출될 수 있는 코드라서 바로 실행하지 않는다. 

위에서 말했듯, 일시중지, 실행, 재개가 자유롭기 때문에 마지막 라인의 코드가 먼저 실행될 수 있다.





 

동시성이 불러올 수 있는 문제점 

 

동시성을 고려하다보면 main queue 에서 작업 항목들을 동기적으로 실행하려고 하면 교착상태(DeadLock) 이 발생할 수 있다. 

교착상태: 여러 스레드가 서로 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더이상 진행하지 못함

과도한 스레드 생성 방지 

1. 동시 실행을 위한 작업을 설계할 때 현재 실행 스레드를 차단하는 메서드를 호출하지 마라.

동시 발송 대기열에서 예약된 작업이 스레드를 차단하면 시스템은 대기 중인 다른 동시 작업을 실행하기 위해 추가 스레드를 생성한다. 너무 많은 작업이 차단되면 시스템에 앱의 스레드가 부족할 수 있다. 

 

2. private concurrent dispatch queues 를 많이 만드는 앱이 많은 스레드를 소비하게 된다. 

각각의 디스패치 대기열은 스레드 리소스를 사용하므로 동시 디스패치 대기열을 추가로 만들면 스레드 소비 문제가 악화된다. 


Swift 는 구조화된 방식으로 비동기 및 병렬 코드를 작성하기 위한 지원 기능이 내장되어있다. 병렬 코드는 여러 코드 조각이 동시에 실행된다는 의미다.

그러나 동시성을 적용하면 발생하게 되는 애로사항들도 있다. 병렬 또는 비동기 코드를 통한 추가적인 스케쥴링 유연성으로 인해 복잡성도 증가한다. 또한 느리거나 버그가 있는 코드에 동시성을 추가한다고 코드 실행 속도가 빨라진다거나 정확해진다는 보장도 없다

 

Swift 에서 동시성 모델은  스레드 위에서 구축되지만 스레드와 직접 상호 작용하지 않는다.
📌 NOTE
비동기 함수는 실행 중인 스레드를 포기할 수 있으며, 이를 통해 첫번째 함수가 차단되는 동안 해당 스레드에서 다른 비동기 함수가 실행될 수 있다. 비동기 함수가 다시 재개되면 Swift 는 해당 함수가 어떤 스레드에서 실행될지는 보장하지 않는다.

 

방안 

private concurrent queues 를 만드는 대신 global concurrent dispatch queues(글로벌 혹은 전역 동시 발송 대기열) 중 하나에 작업을 제출 한다.

serial tasks(직렬 작업)의 경우 serial queue 를 전역 동시 대기열(global concurrent dispatch queues) 중 하나로 설정한다

이렇게 하면, 스레드를 만드는 개별 대기열(queues) 를 최소화하면서 대기열의 직렬화된 동작을 유지할 수 있다.


Dispatch Queue (디스패치 대기열 만드는 것)   - main VS global 

main  - 현재 프로세스의 메인 스레드와 연결된 디스패치 대기열

class var main: DispatchQueue { get }


디스패치큐 타입의 타입 속성으로 변화 가능한 프로퍼티(속성)이고, 값을 읽어오는 것(불러오는 것만) 가능하다. 

DispatchQueue.main.async {
	// UI 업데이트 등의 작업을 수행
    // 
}

func downloadImage(
	blobName:String, 
    imageView:UIImageView, 
    handler: @escaping (UIImage?)->()
    ){
    let blockBlob = blobContainer.blockBlobReference(fromName: blobName)
    blockBlob.downloadToData { error, data in
        if let error = error {
            print(error.localizedDescription)
        }
        print(error?.localizedDescription)
        if let data = data{
            let image = UIImage(data: data)
            
            DispatchQueue.main.async {
                imageView.image = UIImage(data: data)
            }
            
            handler(image)
        }
    }
}

위에는 image를  사용자가 선택한 이미지뷰에 이미지를 나타내는 계산속성에 UIImage(data: 사용자가 선택한 이미지 데이터) 업데이트 된 값을 가져올 때 위 처럼 DispatchQueue.main.async 를 사용한다. 

UIKit 의 모든 요소들은 대체적으로 Main Queue 에서 수행한다. Serial(직렬) 사용방식이다.

global() - 지정된 서비스 품질 클래스가 있는 전역 시스템 큐를 반환한다.

class func global(qos: DispatchQoS.QoSClass = .default) -> DispatchQueue

 

main 과 달리 global() 은 상속한다면 재정의가 가능한 타입 메소드로 여러 상황과 목적에 따라 설정할 수 있고 그 기준은 서비스의 품질(Quality of Service, QoS ) 에 따라 선택하여 사용한다. 또한, 전역적으로 공유되는 concurrent queue(동시적 큐) 이다.  여러 개의 쓰레드로 분산 처리 해준다.

 

이런 성향때문에 DispatchQueue.global().async  는 비동기적으로 접근해야 하는 네트워킹 작업, 이미지 다운이나 api 요청 후 응답값을 받아와야 하는 데이터 등을 가지고 올 때 쓴다.

프린트되는 순서는 매번 달라진다. 비동기적임으로..

 

sync(동기) 로 설정했을 때는 1부터 5까지 순차적으로 프린트 되고 그다음 작업덩어리인 DispatchQueue.global().async 안에 코드가 실행되어야 하는데..

async 는 말 그대로 비동기이기 때문에 DispatchQueue.global().async 안의 코드가 언제 실행될지 모르니까 그 뒤의 작업(task)들이 기다리지 않아도 된다. 비동기적으로 일반적인 for in loop 프린트들과 뒤섞여서 동시적으로 순서 상관없이 실행되는 것을 볼 수 있다.

비동기적으로 작업 실행

func async(execute: DispatchWorkItem)

작업 항목을 즉시 실행하도록 예약하고 즉시 반환한다.

 

func asyncAfter(deadline: DispatchTime, execute: DispatchWorkItem)

지정된 시간에 작업 항목이 실행되도록 예약하고 즉시 반환한다.

 

그 외 동기적으로 실행하는 방법, 병렬적으로 Task(작업) 실행하는 함수 등을 제공해준다.

 

DispatchQueue 를 주로 쓰는 상황은 api 에서 여러 개의 이미지를 호출해 로딩할 때나 많은 테이블뷰 셀 리스트들을 스크롤 하면서 내릴때도 쓰일 수 있다. 

 

 

다음에는 async, await 에 대해 알아보고, 활용하는 방법을 공부해봐야겠다. 

아직도 갈길이 먼 Swift 공부,,

비동기와 동기 

동시성과 병렬성, 직렬

나중에는 꼭 Actor 까지 한번 코드로 짜봐야겠다. 

 

 

 

 

참고문서

앨런 스위프트 마스터 클래스  - 동시성, 비동기, 동기 프로그래밍 강의 설명 일부 참고 

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

https://zeddios.tistory.com/516

https://developer.apple.com/documentation/dispatch/dispatchqueue/1781006-main

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

 

 

'Mobile > Swift' 카테고리의 다른 글

Swift 기본 가이드라인 - part 1  (0) 2023.12.14
Macro 에 대해서 알아보자. ( 수정중 )  (0) 2023.11.12
[ 구조체와 클래스 ]  (0) 2022.12.02
[Swift ] Escape Sequence - 문자열처리  (0) 2022.05.21