Search
Duplicate

GCD의 개념 및 종류

Created
2023/07/11 08:50
Tags
태그
Concurrency
Async

GCD의 개념 및 종류

 DispatchQueue (GCD) : 디스패치큐

 (글로벌) 메인큐 : DispatchQueue.main

메인큐 = 메인쓰레드 (1번 쓰레드를 의미한다)
UI 업데이트 내용 처리하는 큐
Serial (직렬)
// 메인큐 = 메인쓰레드("쓰레드1번"을 의미), 한개뿐이고 Serial큐 let mainQueue = DispatchQueue.main
Swift
복사

 글로벌큐 : DispatchQueue.global()

6가지 Qos(작업에 따라 Qos 상승가능)
종류가 여러 개, 기본설정 동시(Concurrent)
시스템이 우선순위에 따라 더 많은 쓰레드를 배치하고, 배터리를 더 집중해서 사용하도록 함
Concurrent (동시)
큐의 QoS (Quality Of Service)
→ iOS가 알아서 우선적으로 중요한 일임을 인지하고 쓰레드에 우선순위를 매겨 더 많은 쓰레드를 배치하고 CPU의 배터리를 더 집중해서 사용하도록 일을 빨리 끝내도록 하는 개념
// 6가지의 Qos를 가지고 있는 글로벌(전역) 대기열 let userInteractiveQueue = DispatchQueue.global(qos: .userInteractive) let userInitiatedQueue = DispatchQueue.global(qos: .userInitiated) let defaultQueue = DispatchQueue.global() // 디폴트 글로벌큐 // DispatchQueue.global(qos: .default) 와 동일 let utilityQueue = DispatchQueue.global(qos: .utility) let backgroundQueue = DispatchQueue.global(qos: .background) let unspecifiedQueue = DispatchQueue.global(qos: .unspecified)
Swift
복사
.userInteractive
유저와 직접적 인터렉티브
UI업데이트 관련(직접X), 애니메이션, UI 반응 관련 어떤 것이든
사용자와 직접 상호작용하는 작업에 권장, 작업이 빨리 처리되지 않으면 상황이 멈춘 것처럼 보일만함
소요 시간 : 거의 즉시
.userInitiated
유저가 즉시 필요하긴 하지만, 비동기적으로 처리된 작업
ex) 앱 내에서 PDF 파일을 여는 것과 같은, 로컬 데이터베이스 읽기
소요 시간 : 몇 초
.default
일반적인 작업
.utility
보통 Progress Indicator와 함께 길게 실행되는 작업, 계산
ex) IO, Networking, 지속적인 데이터 feeds
소요 시간 : 몇 초에서 몇 분
.background
유저가 직접적으로 인지하지 않고(시간이 안 중요함) 작업
ex) 데이터 미리 가져오기, 데이터베이스 유지보수, 원격 서버 동기화 및 백업 수행
소요 시간 : 몇 분 이상 (속도보다는 에너지 효율성 중시)
.unspecified
legacy API 지원 (스레드를 서비스 품질에서 제외시키는)

 프라이빗(Custom)큐 : DispatchQueue(label: “…”)

Qos 추론 / Qos 설정 가능
디폴트 : 직렬(Serial) (둘다 가능, attributes로 설정)
// 기본적인 설정은 Serial, 다만 Concurrent 설정도 가능 let privateQueue = DispatchQueue(label: "com.inflearn.serial")
Swift
복사

GCD 사용시 주의해야할 사항

1. 반드시 메인큐에서 처리해야하는 작업

화면을 다시 그리는 역할에서 UI 관련 일들은 다시 메인 쓰레드로 보내야 한다.
DispatchQueue.global(qos: .utility).async { ... ... ... self.textLabel.text = "New posts updated!" } // 에러!!
Swift
복사
에러 발생 : UI와 관련된 작업들은 메인 쓰레드에서 처리하지 않으면 에러가 발생한다 (메인쓰레드가 아닌 쓰레드는 그림을 다시 그리지 못하기 떄문)
DispatchQueue.global(qos: .utility).async { ... ... ... // UI 관련 일이기 때문에, 그림을 다시 그리는 작업은 메인 큐에서 DispatchQueue.main.async { self.textLabel.text = "New posts updated!" } }
Swift
복사
메인 쓰레드 : UI와 관련된 작업들을 메인 쓰레드에서 처리할 수 있도록 메인큐를 통해서, 작업을 다시 메인 쓰레드로 보냄
예시)
var imageView: UIImageView? = nil let url = URL(string: "https://bit.ly/32ps0DI")! // URL세션은 내부적으로 비동기로 처리된 함수임 -> URLSession 자체가 글로벌 큐에서 동작하고 있음 URLSession.shared.dataTask(with: url) { (data, response, error) in if error != nil { print("에러있음") } guard let imageData = data else { return } // 즉, 데이터를 가지고 이미지로 변형하는 코드 let photoImage = UIImage(data: imageData) // 🎾 이미지 표시는 DispatchQueue.main에서 🎾 DispatchQueue.main.async { imageView?.image = photoImage } }.resume()
Swift
복사
DispatchQueue.global().async { // 비동기적인 작업들 ===> 네트워크 통신 (데이터 다운로드) DispatchQueue.main.async { // UI와 관련된 작업은 } }
Swift
복사

2)  completionHandler의 존재 이유 - 올바른 콜백함수의 사용

잘못된 함수설계
비동기적인 작업을 해야하는 함수를 설계할 때, return을 통해서 데이터를 전달하려면 항상 nil이 반환되어야 한다.
func getImages(..., c..: String) -> UIImage? { ... URLSession.shared.dataTask(...) { ... }.resume() ... ... return photoImage } // 함수 내부의 일이 끝나기 전에 return 하므로 무조건 nil로 반환
Swift
복사
// 잘못된 함수 설계 func getImages(with urlString: String) -> (UIImage?) { let url = URL(string: urlString)! var photoImage: UIImage? = nil URLSession.shared.dataTask(with: url) { (data, response, error) in if error != nil { print("에러있음: \(error!)") } // 옵셔널 바인딩 guard let imageData = data else { return } // 데이터를 UIImage 타입으로 변형 photoImage = UIImage(data: imageData) }.resume() // 항상 nil 이 나옴 return photoImage } getImages(with: "https://bit.ly/32ps0DI") // 무조건 nil로 리턴함 ⭐️
Swift
복사
제대로 된 함수설계
비동기적인 작업을 해야하는 함수는 항상 클로저를 호출할 수 있도록 함수를 설계해야한다.
func getImages(..., c..: @escaping(UIImage?) -> Void) { ... ... ... URLSession.shared.dataTask(..) { ... ... completion(photoImage) // 함수 내부의 일이 끝나면 completion 클로저 호출 }.resume() }
Swift
복사
올바른 비동기함수의 설계
return이 아닌 콜백함수를 통해, 끝나는 시점을 알려줘야 한다.
// 올바른 함수 설계 (클로저 방식으로 설계!) func properlyGetImages(with urlString: String, completionHandler: @escaping (UIImage?) -> Void) { let url = URL(string: urlString)! var photoImage: UIImage? = nil URLSession.shared.dataTask(with: url) { (data, response, error) in if error != nil { print("에러있음: \(error!)") } // 옵셔널 바인딩 guard let imageData = data else { return } // 데이터를 UIImage 타입으로 변형 photoImage = UIImage(data: imageData) completionHandler(photoImage) }.resume() } // 올바르게 설계한 함수 실행 properlyGetImages(with: "https://bit.ly/32ps0DI") { (image) in // 처리 관련 코드 넣는 곳... DispatchQueue.main.async { // UI관련작업의 처리는 여기서 } }
Swift
복사
// 서버와 통신 =========================================================== struct MovieDataManager { let movieURL = "http://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?" let myKey = "7a526456eb8e084eb294715e006df16f" func fetchMovie(date: String, completion: @escaping ([Movie]?) -> Void) { let urlString = "\(movieURL)&key=\(myKey)&targetDt=\(date)" performRequest(with: urlString) { movies in completion(movies) } } func performRequest(with urlString: String, completion: @escaping ([Movie]?) -> Void) { print(#function) // 1. URL 구조체 만들기 guard let url = URL(string: urlString) else { return } // 2. URLSession 만들기 (네트워킹을 하는 객체 - 브라우저 같은 역할) let session = URLSession(configuration: .default) // 3. 세션에 작업 부여 let task = session.dataTask(with: url) { (data, response, error) in if error != nil { print(error!) completion(nil) return } guard let safeData = data else { completion(nil) return } // 데이터 분석하기 if let movies = self.parseJSON(safeData) { //print("parse") completion(movies) } else { completion(nil) } } // 4.Start the task task.resume() // 일시정지된 상태로 작업이 시작하기 때문 } func parseJSON(_ movieData: Data) -> [Movie]? { // 함수실행 확인 코드 print(#function) let decoder = JSONDecoder() do { let decodedData = try decoder.decode(MovieData.self, from: movieData) let dailyLists = decodedData.boxOfficeResult.dailyBoxOfficeList // 고차함수를 이용해 movie배열 생성하는 경우 ⭐️ let myMovielists = dailyLists.map { Movie(movieNm: $0.movieNm, rank: $0.rank, openDate: $0.openDt, audiCnt: $0.audiCnt, accAudi: $0.audiAcc) } return myMovielists } catch { //print(error.localizedDescription) // (파싱 실패 에러) print("파싱 실패") return nil } } }
Swift
복사

3. weak, strong 캡처의 주의 - 객체 내에서 비동기 코드 사용시

강한 참조
캡처 리스트 안에서 weak self로 선언하지 않으면 강한 참조(strong)
1) 서로를 가리키는 경우 메모리 누수(Memory Leak) 발생 가능
2) (메모리 누수가 발생하지 않아도) 클로저의 수명 주기가 길어지는 현상이 발생할 수 있음
// 캡처리스트 + 약한 참조 선언하지 않으면 기본적으로 강한 참조 DispatchQueue.global(qos: .utility).async { ... ... ... DispatchQueue.main.async { self.textLabel.text = "New posts updated!" } }
Swift
복사
// 강한 참조가 일어나고, (서로가 서로를 가르키는) 강한 참조 사이클은 일어나지 않지만 // 생각해볼 부분이 있음 class ViewController: UIViewController { var name: String = "뷰컨" func doSomething() { DispatchQueue.global().async { sleep(3) print("글로벌큐에서 출력하기: \(self.name)") } } deinit { print("\(name) 메모리 해제") } } func localScopeFunction() { let vc = ViewController() vc.doSomething() } //localScopeFunction() //글로벌큐에서 출력하기: 뷰컨 //뷰컨 메모리 해제
Swift
복사
(글로벌큐) 클로저가 강하게 캡쳐하기 때문에, 뷰컨트롤러의 RC가 유지되어 뷰컨트롤러가 해제되었음에도, 3초뒤에 출력하고 난 후 해제됨
강한 순환참조가 일어나진 않지만, 뷰컨트롤러가 필요 없음에도 오래 머무름
그리고 뷰컨트롤러가 사라졌음에도, 출력하는 일을 계속함
약한 참조
대부분의 경우, 캡처 리스트 안에서 weak self로 선언하는 것을 권장 → 불필요한 일이 생기지 않음
// 클로저이므로 캡처리스트 + 약한 참조 선언해야함 DispatchQueue.global(qos: .utility).async {[weak self] in guard let `self` = self else { return } ... ... DispatchQueue.main.async { self.textLabel.text = "New posts updated!" } }
Swift
복사
class ViewController1: UIViewController { var name: String = "뷰컨" func doSomething() { // 강한 참조 사이클이 일어나지 않지만, 굳이 뷰컨트롤러를 길게 잡아둘 필요가 없다면 // weak self로 선언 DispatchQueue.global().async { [weak self] in guard let `self` = self else { return } sleep(3) print("글로벌큐에서 출력하기: \(self.name)") } } deinit { print("\(name) 메모리 해제") } } func localScopeFunction1() { let vc = ViewController1() vc.doSomething() } localScopeFunction1() //뷰컨 메모리 해제 //글로벌큐에서 출력하기: nil
Swift
복사
뷰컨트롤러를 오래동안 잡아두지 않음
뷰컨트롤러가 사라지면 —> 출력하는 일을 계속하지 않도록 할 수 있음
(if let 바인딩 또는 guart let 바인딩까지 더해서 return 가능하도록)

4. 동기함수를 비동기적으로 동작하는 함수로 변형하는 방법

일반(동기) 함수
오래 걸리는 일반적인 함수를 단순히 동기함수로 만들면 메인쓰레드에 부하가 걸림
func doSomething() { print("프린트 시작") sleep(3) print("프린트 종료") } print("1") doSomething() print("2") // 1 // 프린트 시작 // 프린트 종료 // 2
Swift
복사
비동기 함수로
오래걸리는 일반적인 함수를 내부에 비동기적 처리를 하면 비동기로 동작하는 함수로 변형 가능
func doSomething(com: @excaping (Void) -> Void) { DispatchQueue.global().async { print("프린트 시작") sleep(3) print("프린트 종료") com() } } print("1") doSomething() print("2") // 1 // 2 // 프린트 시작 // 프린트 종료
Swift
복사
func longtimePrint(name: String) -> String { print("프린트 - 1") sleep(1) print("프린트 - 2") sleep(1) print("프린트 - 3 이름:\(name)") sleep(1) print("프린트 - 4") sleep(1) print("프린트 - 5") return "작업 종료" } // 요 함수를 // 작업을 오랫동안 실행하는데, 동기적으로 동작하는 함수를 // 비동기적으로 동작하도록 만들어, 반복적으로 사용하도록 만들기 // 내부적으로 다른 큐로 비동기적으로 보내서 처리 func asyncLongtimePrint(name: String, completionHandler: @escaping (String) -> Void) { DispatchQueue.global().async { let n = longtimePrint(name: name) completionHandler(n) } } //asyncLongtimePrint(name: "잡스", completion: <#T##(String) -> Void#>) asyncLongtimePrint(name: "잡스") { (result) in print(result) // 메인쓰레드에서 처리해야하는 일이라면, // DispatchQueue.main.async { // print(result) // } }
Swift
복사

5. 비동기 함수 / 메서드의 이해

URLSession.shraed.dataTask
→ 비동기적으로 설계되어 있다(Asynchronicity), 이 함수를 실행하면 메인쓰레드에서 실행되는 것이 아니라 다른 쓰레드로 보내서 일 처리를 한다(기다리지 않는다)
일반적으로 대부분의 네트워킹 등 오래 걸리는 API들은 따로 비동기처리를 하지 않아도 내부적으로 비동기적으로 구현 되어 있다.
URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in self.image = UIImage(data: data!) ... }.resume()
Swift
복사

URLSession은 비동기 메서드

API 중에는 내부적으로 비동기 처리가 된 메서들이 존재
let movieURL = "https://bit.ly/2QF3ID2" // 1. URL 구조체 만들기 let url = URL(string: movieURL)! // 2. URLSession 만들기 (네트워킹을 하는 객체 - 브라우저 같은 역할) let session = URLSession.shared // 3. 세션에 (일시정지 상태로)작업 부여 let task = session.dataTask(with: url) { (data, response, error) in if error != nil { print(error!) return } guard let safeData = data else { return } print(String(decoding: safeData, as: UTF8.self)) } // 4.작업시작 task.resume() // 일시정지된 상태로 작업이 시작하기 때문 print("출력 - 1") // 비동기 처리 된 메서드 URLSession.shared.dataTask(with: url) { (data, response, error) in if error != nil { print(error!) return } guard let safeData = data else { return } print(String(decoding: safeData, as: UTF8.self)) }.resume() print("출력 - 2")
Swift
복사