본문 바로가기

iOS/Swift

Concurrency - 기초

동시성(Concurrency) 프로그래밍

  • 비동기 프로그래밍 이해

1. 비동기 처리가 필요한 이유

  • 네트워크 통신과 비동기 처리

네트워크 통신(서버와 통신)은 부하가 많이 걸리는 일임.

        예를 들어 서버로부터 데이터를 가져와 테이블 뷰로 표현한다고 가정을 해보자.

        만약 비동기 처리가 되어있지 않다면 테이블뷰를 스크롤 할 때마다 버벅이게 될 거임!

        🤔왜 그럴까?

        비동기 처리를 하지 않으면 UI관련 메커니즘이 제대로 동작하지 않기 때문??

        비동기 처리를 하지 않으면 메인 쓰레드에서 모든 작업을 처리함.

        그러나 작업의 길이가 길어지게 되면서(네트워크 통신) 과부하가 걸리게 되는거임.

                그래서 메인 쓰레드는 UI작업만 남기고 네트워크 등 시간이 오래 걸리는 작업은 비동기 처리해야함.

앱의 시작과정과 동작 원리(참고)

About the app launch sequence | Apple Developer Documentation

 

About the app launch sequence | Apple Developer Documentation

Learn the order in which the system executes your code at app launch time.

developer.apple.com

동시성 처리

iOS에서는 작업을 Queue로 보내기만 하면 운영체제가 알아서 여러 쓰레드로 분산 처리를 해줌.

그리고 iOS에서 이 Queue는 크게 두가지로 나뉨

1. DispatchQueue 

(GCD - Grand Central DispatchQueue)

  • 직접적으로 쓰레드 관리 X, Queue의 개념을 이용해 작업 분산처리, OS가 쓰레드 관리
  • 쓰레드 객체 직접 생성 X, 쓰레드보다 높은 레벨에서 작업을 처리
  • UI이외의 작업을 메인이 아닌 다른 쓰레드에서 비동기적으로 쉽게 동작하도록 도와줌

2. OperationQueue

2. 비동기(async), 동시(Concurrent)의 개념

Synchronous(동기) VS Asynchronous(비동기)

  • 비동기(Async)처리

        다른 쓰레드에서 일을 시작 시키고 작업을 기다리지 않음

                일을 동시에 할 수 있게되는거임!

                긴 작업이 있을 경우 이를 비동기로 처리하여 다른 쓰레드에서 작업하고 메인 쓰레드는 다른 작업 가능.

  • 동기(Sync)처리

        다른 쓰레드에 일을 시작 시키고 작업이 끝날때까지 기다림.

                작업을 다른 쓰레드에 보내더라도 메인 쓰레드에서 다른 작업을 할 수 없음.

Serial(직렬) VS Concurrent(동시)

  • 직렬(Serial) Queue

        Queue에 들어온 작업을 메인 쓰레드 이외의 단 하나의 쓰레드로만 보냄.

        순서가 중요한 작업을 처리할 때 사용

  • 동시(Concurrent) Queue

        Queue에 들어온 작업을 메인 쓰레드 이외의 여러 개의 쓰레드로 보냄.

        유사한 여러개의 작업을 처리할 때 사용

        🤔그럼 분산처리를 할땐 동시 큐가 무조건 이득아님?? 직렬 큐 왜 있는데?

                직렬 큐는 순서가 중요한 작업을 처리할 때 사용

                동시 큐는 각자 독립적이지만 유사한 여러개의 작업을 처리할 때 사용

                        유사한 작업은 중요도, 작업의 성격 등 여러가지로 나눌 수 있음

3. GCD의 개념 및 종류

DispatchQueue(GCD)

  • 메인 큐
    • DispatchQueue.main
    • 메인큐 = 메인쓰레드(UI 업데이트)
    • Serial(직렬)로 동작(글로벌) 메인 큐

        사실 특별한 경우가 아니면 거의 사용할 일이 없음.

        그냥 실행하면 메인 쓰레드에서 동작하기 때문임.

  • 글로벌 큐
    • DispatchQueue.global()
    • 6가지 Qos(작업에 따라 선택)
      • 6가지 Qos 코드
//메인큐 = 메인쓰레드("쓰레드1번"을 의미), 한개뿐이고 Serial큐
let mainQueue = DispatchQueue.main


// 글로벌큐
// 6가지의 Qos를 가지고 있는 글로벌(전역) 대기열
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)
  • 시스템이 우선순위에 따라 더많은 쓰레드 배치
  • Concurrent(동시)로 동작
  • 프라이빗(Custom) 큐
    • DispatchQueue(label: "...")
    • Qos 추론 / Qos 설정 가능
    • 기본은 Serial, 그러나 둘 다 설정가능
//기본적인 설정은 Serial, 다만 Concurrent설정도 가능
let privateQueue = DispatchQueue(label: "com.inflearn.serial")

4. GCD 사용 시 주의해야 할 사항

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

  • UI 관련 작업은 반드시 메인 쓰레드에서 동작해야 함.
  • UI 관련 작업은 다른 쓰레드에서 작업을 하더라도 다시 메인 쓰레드로 보내야 한다.
  • 메인 이외의 쓰레드에서 UI 작업을 할 경우 에러가 발생함.
DispatchQueue.global().async {
    
    // 비동기적인 작업들 ===> 네트워크 통신 (데이터 다운로드)
    
    DispatchQueue.main.async {
        // UI와 관련된 작업은 
    }
}

컴플리션 핸들러의 존재 이유 - 올바른 콜백함수의 사용 (중요)

지금까지 본 비동기 함수의 경우 다른 쓰레드에 작업을 시키고 그 작업을 기다리지 않음.

근데 네트워크 처리 등을 할 때는 해당 비동기 작업의 종료 시점을 정확히 알고, 다음 작업을 시켜야 함.

따라서 함수를 설계할 때 return으로 설계하면 안됨

@escaping (데이터) → Void 이와 같은 콜백함수를 통해 작업이 끝난 후 데이터를 넘겨줘야 함.

⭐️⭐️⭐️ 리턴(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관련작업의 처리는 여기서
    }
    
}

weak, strong 캡처의 주의

  • 객체 내에서 비동기코드 사용 시

강한 참조

캡쳐리스트 안에서 weak self로 선언하지 않으면 강한 참조임(strong)

  1. 서로를 가리키게 되면 메모리 누수(Memory Leak)발생 가능
  2. 누수가 발생하지 않더라도 클로저의 수명주기가 길어짐
// 강한 참조가 일어나고, (서로가 서로를 가르키는) 강한 참조 사이클은 일어나지 않지만
// 생각해볼 부분이 있음

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()

//글로벌큐에서 출력하기: 뷰컨
//뷰컨 메모리 해제
/**=======================================================
 - (글로벌큐)클로저가 강하게 캡처하기 때문에, 뷰컨트롤러의 RC가 유지되어
 - 뷰컨트롤러가 해제되었음에도, 3초뒤에 출력하고 난 후 해제됨
 - (강한 순환 참조가 일어나진 않지만, 뷰컨트롤러가 필요없음에도 오래 머무름)

 - 그리고 뷰컨트롤러가 사라졌음에도, 출력하는 일을 계속함
=========================================================**/

약한 참조

대부분, 캡쳐리스트 내부에서는 weak self로 선언하는 것을 권장함.

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
/**=======================================================
 - 뷰컨트롤러를 오래동안 잡아두지 않음
 - 뷰컨트롤러가 사라지면 ===> 출력하는 일을 계속하지 않도록 할 수 있음
   (if let 바인딩 또는 guard let 바인딩까지 더해서 return 가능하도록)
=========================================================**/

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

// 작업을 오랫동안 실행하는 함수가 있다고 가정
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 "작업 종료"
}

longtimePrint(name: "잡스")

//: # 동기함수를 비동기함수로 만들기
// 작업을 오랫동안 실행하는데, 동기적으로 동작하는 함수를
// 비동기적으로 동작하도록 만들어, 반복적으로 사용하도록 만들기
// 내부적으로 다른 큐로 비동기적으로 보내서 처리

func asyncLongtimePrint(name: String, completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        let n = longtimePrint(name: name)
        completion(n)
    }
}

//asyncLongtimePrint(name: "잡스", completion: (String) -> Void)

asyncLongtimePrint(name: "잡스") { (result) in
    print(result)
    
    // 메인쓰레드에서 처리해야하는 일이라면,
//    DispatchQueue.main.async {
//        print(result)
//    }
}

 

 

앨런 선생님의 강의를 참고하여 작성했습니다.

async, await 같은 함수는 정리 후 다음 글로 작성하겠습니다!

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

Type Erasure, Opaque Type  (0) 2024.07.03
Access Control(접근제어자)  (0) 2024.06.23
[weak self] 왜 쓸까!!  (1) 2024.01.15
Metatype(.self, .Type, .Protocol)  (1) 2023.11.08
Swift Xcode 프로젝트명 바꾸기  (0) 2023.08.01