iOS랑 친해지기

[iOS] GCD와 동기,비동기 처리

데브킹덕 2023. 8. 2. 17:25

 Words API를 이용해 랜덤하게 단어들을 불러와 그 중 한가지 단어를 다시 Naver 번역,이미지 API를 이용해 단어의 정의와 사진을 보여주는 앱을 설계하려고 했다.

 하지만 번역과 이미지 데이터를 파싱해 0번째 인덱스 있는 데이터를 UI에 보여주도록 하였지만 out of range에러가 생성되었다.

이유는 Label과 Image만 mainQueue에서 비동기로 처리하였기 떄문이다.

단어,번역,이미지 데이터를 파싱을 하는 과정에서 배열에 데이터가 담겨져 있지 않은데 Label과 Image의 값을 0번째 인덱스에서 가져오려 했기 때문이다. 

 그래서 방법을 찾아보다가 GCD에 대하여 찾아 볼 수 있었다.

 

먼저 GCD를 알기 전에 프로그램,프로세스,쓰레드의 개념을 알아야 했다.

 

프로그램

- 작업을 실행할 수 있는 파일

- 실행을 하지 않아 메모리에 올라가 있지 않은 상태

 

프로세스

- 운영체제로부터 시스템 자원을 할당받는 단위

- 프로그램을 실행하여 메모리에 올라간 상태

- 독립적인 메모리 공간(Code,Data,Heap,Stack)을 가지고 있기 떄문에, 다른 프로세스의 변수나 자료구조에 접근 불가능

- 만약 다른 프로세스의 자원에 접근하고 싶으면 프로세스간 통신(IPC)를 사용해야함

- 최소 하나 이상의 쓰레드를 포함하고 있음

 

멀티 프로세스

- 하나의 프로그램을 여러개의 프로세스로 나누어 각 프로세스마다 작업을 할당하는 것

- 독립된 구조이기 때문에 안정성이 높음

- 프로세스를 CPU에 왔다갔다 하는 과정에서 Context Switching이 일어남

단점: Context Switching 시 CPU 부담이 커지고 오버헤드가 발생됨 

 

 

쓰레드

- 한 프로세스 내에서 실행되는 여러 실행의 흐름

- 프로세스 내에서 동작되는 것이기 때문에 메모리 영역을 독립적으로 할당받지 못함

- Code,Data,Heap은 공유하지만 Stack영역은 독립적으로 할당 받을 수 있음

 

 

멀티 쓰레드 - iOS는 주로 멀티쓰레드 사용

- 하나의 프로세스를 여러개의 쓰레드로 구성하여 각 쓰레드마다 하나의 작업을 처리하는 것 

(하나의 쓰레드에서 이루어지던 작업을 다른 스레드에서도 작업할 수 있도록 분산 처리하는 방법)

- 하나의 프로세스 내에서 동시에 여러 작업을 수행. 즉 여러 쓰레드가 동시에 실행되는 것을 의미

- 동시에 여러 작업을 수행할 수 있기 때문에 빠르게 작업을 수행할 수 있지만, 어떤 작업이 먼저 끝날지

어떤 순서로 실행이 될지에 대한 순서를 알 수 없음

- 공유하기 때문에  통신 방법이 간단함 Context Switching이 빠름

- 설계가 까다로움

- A쓰레드가 접근하려는 힙영역의 자원을 B가 바꿔버리는 등 자원 공유의 문제가 생기기 떄문에 동기화 문제 발생

- 하나의 쓰레드에 문제 발생시 전체 쓰레드가 영향을 받음 

 

문제점 파악)

iOS에서 쓰레드를 여러개 제공해도 한가지 쓰레드만 사용하고 있었던 것 같다.

이미지를 보여줄때 압축한 이미지를 다운받고, 압축을 풀고, 이미지로 변경하는 시간 + 여러 일을 한쓰레드에 하고 있던것이다.

 

문제 해결 방법)

- iOS 에서는 대기행렬(Queue)에 보내면 알아서 쓰레드에 나누어서 처리해준다고 한다.

- 항상 선입선출(FirstInFirstOut)로 동작함

 

쓰레드의 종류

1. Main Thread

- iOS에서 오직 한개 존재

- 우리가 작성하는 코드들이 Cocoa에서 실행될때 , 메인스레드 에서 호출

- UI에 관련된 작업들은 Main에 , UI를 실행하는데 영향을 줄만한 코드는 Global Thread에서 실행시켜줘야함

 

2. Global Thread (Background Thread)

- iOS 모든 Framework는 Background Thread에서 구동됨

- 가끔 필요할때 Main Thread에 손을 뻗는 구조

* Delegate, Completion Handler 등을 통해 Main Thread에게 알려 통제

 

 

 

----------------------------------------------------------------------------------------------------------------------

 

Apple에서 Muti Threading을 위하여 API 지원해줌

 

 NSOperation

- GCD를 고급언어로 래핑

- GCD보다 다소 무겁고 약간의 오버헤드 발생시킴

- NSOperationQueue에 추가하여 실행시킴

- Concurrent Queue로 동작

- OperationQueue: 작업 취소, 일시 중지, 일시중지된 작업 재시작( 복잡한 기능 구현가능)

 

 

 

 

  GCD(Grand Central Dispatch)

- C로 구현되어 있어 가볍고 성능 좋음 

- 클로저로 구현되어 있어 코드 구독성 면에서 좋고 간단하게 사용가능

- DispatchQueue에 추가하여 실행시킴 (Dispatch: 보내다, Queue: 대기행렬)

- App실행과 동시에 구조적으로 Main,Global Queue가 자동생성됨

- 개발자가 작업을 정의해서 Dispatch Queue에 넣어주면 적절한 스레드에 각각 할당됨

 

 

DispatchQueue

- 비교적 간단하게 구현 가능하고, 클로저를 통해서 작업 단위를 표현할 수 있음

- 클로저 안에 작업별로 구성

 

1.DispatchQueue.global

- Concurrent Queue가 기본임 

-  작업 (Queue의 Task항목)을 여러 글로벌 스레드로 나눠 보냄 -> 동시에

 

2.DispathchQueue.main

- Serial Queue가 기본

- 작업을 여러 스레드로 분산하지 않고, 하나의 (main)스레드에서 처리

- UI 업데이트 시, 메인스레드 에 구현해야 함

 

 

 

Serial (직렬큐)

- (보통 메인에서) 분산처리 시킨 작업을 다른 한개의 쓰레드에서 처리하는 큐

- 큐에 들어온 작업들을 순차적으로 실행

- Sync와 Async처럼 단일 작업들을 하나씩 순차적으로 실행하는 것

- 주로 동기화 할때 사용

 

Concurrent (동시 큐)

- (보통 메인에서) 분산처리 시킨 작업을 다른 여러개의 쓰레드에서 처리하는 큐

- 큐에 들어온 작업들을 동시다발적으로 실행

- Sync와 Async처럼 단일 작업들을 하나씩 동시다발적으로 실행하는 것

 

Q) 그러면 무조건 Concurrent만 쓰면 되지 않냐?

A) 직렬큐는 한개의 쓰레드에서 처리하는 큐로 순서가 중요한 작업을 할때 사용하면 되고,

동시큐는 각자 독립적이지만 유사한 여러개의 작업([ex]테이블,콜렉션 뷰 셀 이미지)을 처리할때 사용

 

Sync

- 요청에 대한 응답이 동시에 발생해야함

- 내 작업이 끝나기 전까진 다른 작업을 수행하지 못함

- 큐에 작업을 등록한 이후에 해당 작업이 완료될떄까지 더 이상 코드를 진행하지 않고 기다리는 것

 

Async

- 요청에 대한 응답이 동시에 발생하지 않음

- 내 작업이 끝나기 전까진 다른 작업을 실행

- 큐에 작업을 등록하면 작업의 완료여부와 상관없이 계속 코드를 실행시키는 것

 

 

sync/async - DispatchQueue에 작업을 등록하는 주체에 대한 설정

concurrent/serial - 이미 큐에 들어온 작업을 어떻게 처리하냐

 

조합

1. Sync + Serial (메인스레드에서 실행됨)

- 큐에 작업을 등록한 이후 해당 작업이 완료될떄까지 더이상 코드를 진행하지 않고 기다림

- 먼저 들어온 작업이 완료 되어야 큐에 있는 다음 작업을 진행함

- 코드가 실행될때 Serial Dispatch Queue에 작업을 하나 등록하면 해당 작업이 끝날때까지 기다림

- Dispatch Queue쪽에서는 메인 스레드로 해당 작업을 보내 처리하도록 함

 

참고)

Sync + Main(Serial 특성을 가지는 Main큐)

- Serial Queue와 메인스레드로 인해 에러가 남 

- Main DispatchQueue에서 sync를 호출하면 메인스레드는 그대로 진행을 멈추고 등록한 작업이 끝날때까지 기다림

- 동시에 큐에 등록된 작업은 메인스레드로 할당됨

- 하지만 메인스레드는 이미 아무것도 할 수 없는 상태이므로 작업을 수행할 수 없음

- 큐에 등록된 작업은 시작이 되지 못하고 데드락 상태에 빠지게 됨

 

 

같은 큐에서 큐로 sync를 보내는 상황

DispatchQueue.global().async{
	DispatchQueue.global().sync{
    	print("")
    }
}

- 항상은 아니지만 잘못하면 같은 스레드에 다시한번 작업을 할당하게 될 수 있다.

- 외부 DispatchQueue가 스레드 1에 내부 DispatchQueue를 실행하는 작업을 등록했을 때,

해당 스레드 1에서 sync로 print를 큐에 등록하고, 운영체제가 등록된 출력작업을 다시 스레드 1에 할당하면

sync된 스레드 1은 아무작업도 하지 못하는 데드락이 발생하게 됨

 

2.Serial + Async

- 작업의 등록은 작업 완료여부와 상관없이 진행하고,

- 작업의 수행은 순차적으로 진행함

- 모든 작업들이 작업의 결과와 상관없이 등록되고

- 작업은 큐에서 하나씩 꺼내서 메인스레드에서 처리하게 됨

- 작업들의 등록순서와 출력순서가 일치함

 

 

3. Concurrent +Sync

- 작업이 끝날때까지 대기하면서 큐에 등록되는 작업들은 여러 스레드에 분산처리되어 처리

- 작업의 등록 순서와 출력 순서가 항상 일치함

 

4. Concurrent +Async

- 작업을 완료 여부와 상관없이 계속 등록

- 등록된 작업들은 스레드에 분산되어 동시에 실행

- 등록순서와 출력순서가 항상 일치하지 않음

 

 

 

3.custom Queue

- 사용자가 어떤 특성의 큐로 Dispatch Queue를 생성할지 결정할 수 있도록 해줌

- 기본 값은 Serial을 가짐

 

let dispatchQueue = DispatchQueue(lable:"custom", attributes. concurrent)

dispatchQueue.()(qos 옵션).(sync/async){
	//할일
}

 

 

QOS - Quality of Service

Cocurrent하게 작업이 분산되어 실행 될떄, 작업의 중요도를 설정해 실행의 우선순위를 부여할 수 있음

 

qos 종류

1. userInteractive

- 최고 

- 사용자와 직접 상호작용을 하는 작업을 의미하기 때문에 사용자의 요청에 곧바로 응답이 있어야함

- 애니메이션 처리, 이벤트 처리, UI업데이트 경우

 

2. userInitiated

- 사용자가 어떤 요청을 했을 때 그 결과를 곧바로 받아야 하거나 사용자가 앱을 사용하는 것을 순간적으로 막기 위한 경우

- 사용자에게 보여줄 이메일 내용을 로드할 때

 

3. default

- 기본값

 

4. utility

- 사용자가 앱을 계속 사용하지 않도록 막지 않는 작업에 사용할 수 있다.

- 사용자와 상호작용하지 않으면서 오랜시간동안 어떤 작업을 진행해야 할때 사용

 

5. background

- 가장 낮은 우선순위를 가지는 qos

- 앱이 백그라운드에서 실행 중일 떄 진행

 

 

 

수정한점

DispatchQueue.global(qos:.background).async{
	//API에서 데이터를 가져온다.
	//오래걸리는 네트워크 처리
    
    DispatchQueue.main.async{
    	//Image,Label 등 UI설정 작업
    }
}