동시성 프로그래밍

동시성 프로그래밍

동시성 프로그래밍 목차

용어 정리

동기와 비동기

정의

  1. 동기

    • task 1이 끝날 때까지 다른 태스크를 실행하지 않는다.

    • 작업을 시작시키고, 끝날 때까지 기다렸다가 다음 일을 진행한다.

  2. 비동기

    • 다른 스레드로 task 1을 보내고, task2를 수행한다.

      • 비동기는 task1이 끝날때까지 기다리지 않는다. (즉시 리턴)

      → 메인 스레드(1번 스레드)가 다른 일을 할 수 있다.

비동기처리가 필요한 이유

  • iPhone의 경우, 1초에 60번씩 화면을 그려 (60Hz) 화면이 움직이는 것처럼 보이게 만든다.

    그런데 네트워크 통신 등 시간이 오래 걸리는 작업을 동기로 처리하면, 그 작업의 결과를 기다리느라 화면을 그리는 매커니즘이 제대로 동작하지 않게 된다. → 화면이 뚝뚝 끊기는 현상이 발생한다.

  • 비동기처리를 통해 부하가 많이 걸리는 일을 동시에 수행하기 위해 비동기 처리가 필요하다.


직렬처리와 동시처리

정의

  1. 직렬 처리

    • Task를 2번 스레드에 모두 보내는 것이다.

    • 다른 하나의 스레드에서 처리하는 것

  2. 동시 처리

    • 여러개의 스레드에 보내는 것이다.

    • 스레드의 개수는 시스템이 결정한다.

직렬처리가 필요한 이유

  • 태스크가 순서가 중요할 때 사용한다.

병렬 처리가 필요한 이유

  • 독립적이지만, 유사한 일을 처리해야할 때 사용

    (게시글을 불러올 때)


결론

  • 비동기와 동시는 다른 말이다!

    • 기다릴지 말지에 의한 것 : 동기 / 비동기

    • 1번 스레드 이외의 스레드 개수에 의한 것 : 동시 / 직렬

동시성 프로그래밍이 필요한 이유

  1. 성능, 반응성을 다루기 위해서

  2. 최적화를 위해서

동시성 프로그래밍

복습

비동기 처리가 필요한 이유

  • 테이블을 스크롤할 때마다 버벅임이 발생한다.

  • 서버에 데이터를 요청하는 일이 부하가 많이 걸리는 일이기 때문이다.

왜 뚝뚝 끊기는 현상이 일어날까?

  • iPhone의 화면 주사율은 60Hz

    • 1초에 60번 화면을 다시 그린다.

    • 특정 메커니즘에 의해 화면을 계속 다시 그리면서, 움직이는 것처럼 보이게 만든다.

  • 비동기처리를 하지 않으면, 메커니즘이 제대로 동작할 수 없어서 버벅임이 발생한다.


CPU의 개념

코어 / 쓰레드 / 클럭의 개념

  • 1개의 CPU (1 Core, 1 Thread)

    • 물리적인 속도를 높이기 위해 Clock(진동)의 속도를 높였다.

    • 발열 문제 및 배터리 이슈 발생

  • CPU 개수를 늘리기 시작함

    • 2개의 CPU (2 Core, 2 Thread)

    • 4개의 CPU (4 Core, 4 Thread)

    • 8, 10 …

  • 4 Core 8 Thread

    • Core에 Thread 개수를 늘렸음 (하이퍼 쓰레딩 기술)

    • Thread : 실제로 일을 하는 부분임. → 1Core에 2개의 Thread를 붙이게 되면서 8개의 core가 있는 효과를 가져왔다.

  • 현대 PC 6Core, 12 Thread

    • 물리적으로 일을 처리하는 부분은 12개이다~!


앱의 시작 과정 및 동작 원리

  1. 앱의 시작 (Launch Time)

    • 사용자가 앱 아이콘을 클릭함

    • main()이 실행됨

    • UIApplicationMain()을 통해 앱 객체가 생성됨

    • 화면을 준비

    • … 초기화 완료 → Running 단계로 이동

  2. 실행 중 (Running Time)

    • 앱의 동작

    • event loop라는 런루프를 생성하고, 무한 반복문이 실행된다.

      • event: 사용자가 전달하는 값들 (터치, 화면 회전 등)

      • 런 루프 : 이벤트 핸들링 객체로 이벤트를 파악하고 적절한 함수를 실행시킨다.

    • 무한 반복문이 일처리를 하고, update cycle(1초에 60회)에 맞추어 업데이트 하게 된다.

      • 화면을 그리는 일 = main Thread에서 담당한다.

main Thread의 역할

  • (앱이 시작될 때 앱을 담당하는 메인 런 루프가 생성된다.)

  • 메인 쓰레드는 이벤트 처리를 담당한다.

    • 이벤트에 맞춰 어떤 함수를 실행시킬 것인지 선택하고 실행한다.

  • 실행 결과를 화면에 보여주며, 필요 시 화면을 다시 그리게 된다. (main Thread)

소프트웨어적인 Thread (NSThread 객체)

  • 메인 쓰레드는 1초에 60번 화면을 다시 그려야하는 역할을 가지고 있다.

    • 직접적으로 화면을 그리진 않지만, 렌더링 프로세스 역할을 한다는 것임.

    • 이에 맞는 업데이트 주기를 가지고 있다. (Update cycle)

  • 이런 과정에서, 메인 스레드에서 너무 오래 걸리는 작업을 한다면?

    • 1초에 60번 화면을 다시 그리는 역할도 수행해야하기 때문에 화면이 버벅거림

      • task 사이 비어있는 시간에만 화면을 다시 그릴 수 있기 때문이다.

  • 이에 1번 쓰레드 뿐만 아니라, 다른 쓰레드에서도 일을 시키는 방법으로 코딩을 해야함

    • 분산처리를 어떻게 하는지에 대한 코딩 방법

    • 비동기처리, 동시성 프로그래밍이라고 한다.


iOS의 동시성 처리

  • 작업을 대기행렬(Queue)에 보내기만하면, 운영체제(iOS)가 알아서 분산처리(동시적 처리)를 한다.

    • Queue (FIFO 구조)

    • Queue에 들어오는 즉시 다른 스레드에 바로 배치한다.

      • 즉, 작업을 쌓았다가 보내는 것이 아니라 작업을 즉시 스레드에 배치한다.

iOS 프로그래밍의 대기열 (Queue)의 종류

💡 iOS에서는 직접적으로 쓰레드를 관리하는 개념이 아니라, 대기열 개념을 이용해서 작업을 분산처리한다. 이후 iOS가 알아서 쓰레드의 개수를 관리하게 된다. 즉, 개발자들은 Task를 Queue에 보내기만 하면, iOS가 관리함을 의미한다.

  1. Dispatch Queue

    • GCD (Crand Central DispatchQueue)라고도 한다.

  2. Operation Queue


병렬 / 동시

병렬(Parallel)과 동시성(Concurrency)의 개념

💡 물리적인 스레드

  • 물리적인 스레드는 1개이더라도, S/W적 스레드는 여러 개의 객체로 나누어질 수 있다.

  • 물리적인 스레드가 1초에 35억번의 일을 할 수 있다고 가정한다.

    • S/W적 스레드 하나 당 1초에 10번의 연산을 할 수 있다는 것이다.

    • Thread pool이라고 한다. (OS에서 알아서 관리)

  1. 동시성 (Concurrency)

    • 소프트웨어적인 스레드의 관점

      • 개발자가 신경써야하는 영역이다.

    • 물리적인 Thread를 알아서 switching하며, 빠르게 일을 처리한다.

  2. 병렬 (Parallel)

    • 물리적인 스레드에서 실제 동시에 일을 하는 개념이다.

      • 내부적으로 동작하는 것으로 개발자가 신경쓰지 않아도 되는 부분이다.


분산처리

  • 비동기 처리 / 동시성 프로그래밍!!

    • 성능, 반응성, 최적화와 관련된다.

    • 즉, 화면의 버벅거림 문제를 해결하기 위한 프로그래밍 기법이다.

동기, 비동기의 개념

동기 (Sync)

  • 다른 스레드에 일을 시켰을 때, 해당 일이 끝날 때 까지 기다리는 것이다.

    • block 된 상태로, 다른 일 처리를 할 수 없다.

  • 작업이 긴 경우, task1이 끝날 때까지 기다려야 task 2가 실행될 수 있다.

비동기 (Async)

  • 다른 스레드에 일을 시킬 때, 해당 일이 끝나는 것을 기다리지 않고, 다음 일을 진행하는 것이다.

    • 큐가 알아서 스레드에 태스크를 보낸다.

  • task1이 매우 오래 걸려도, task2를 시작할 수 있다는 것이다.

코드레벨에서의 동기와 비동기

  • 내부적으로 비동기 처리가 되어있는 함수의 경우

    • 거의 같은 시간 내 모든 함수가 종료된다.

    • 실행 순서와 관계 없이 실행 시간이 짧은 함수의 결과를 먼저 출력한다.


Blocking, Non Blocking

기본 개념 : CPU 제어권과 관련된 개념이다.

Swift에 Blocking 관련 개념이 있다는 것이 아님 (동기, 비동기만 존재)

  1. Blocking (동기)

    • 제어권을 바로 반환하지 않는 것이다.

    • 즉, 2번 Thread에 제어권을 줘서, 1번 Thread는 멈춰있는 상태이다.

  2. Non Blocking (비동기)

    • 제어권을 바로 반환하는 것이다. → 다른 일을 할 수 있다.

    • 1번 Thread가 지속적으로 완료 여부를 확인하고, 2번 Thread는 지속적으로 완료 여부를 회신하는 방식이다.

직렬과 동시

큐의 종류

  1. 직렬 큐 (Serial)

    • 작업을 배치 보내면, 하나의 스레드를 생성한다.

    • 다른 하나의 스레드에서만 작업을 수행한다.

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

  2. 동시 큐 (Concurrent)

    • 작업을 배치 보내면, 여러개의 스레드를 생성하여 수행한다.

    • 독립적이지만, 유사한 작업을 처리할 때 사용한다. (중요도나 성격이 유사할 때)

GCD 개념 및 종류

Dispatch Queue

  • Default로 global 큐를 생성하고, 비동기적으로 작동한다.

  • 클로저는 작업을 하나로 묶는다.

    • 클로저 내 작업은 순차적으로 진행된다.

  • Dispatch Queue는 하나의 task로, 순서가 바뀔 수 있다.

    • Dispatch Queue 내부 closure는 순서가 바뀌지 않음을 의미한다.

  • 비동기적 함수를 만들기 위해서 함수 내부에 DispatchQueue를 정의하면 된다.

Queue 의 종류

  1. DispatchQueue (GCD)

    1. (글로벌) 메인 큐

    2. 글로벌 큐

    3. 프라이빗 큐

  2. OperationQueue

Dispatch Queue (GCD) 의 종류

  1. Global Main Queue

    • 1번 Thread를 의미한다.

    • 직렬(Serial)로 동작한다.

    • UI 업데이트 내용을 처리한다.

    • DispatchQueue.main

  2. Global Queue

    • 여러 개의 Thread를 사용한다.

    • 서비스 품질과 연관된다.

      • 서비스의 품질이 높을수록, Thread 사용 갯수가 많다.

      • iOS가 알아서 스레드를 배치하고, CPU의 배터리를 집중해서 사용하도록해서 일을 빨리 끝내도록 한다.

      • 서비스의 품질이 높을수록 신경을 많이 쓰기는 하지만, 먼저 끝난다고 보장할 수 없다.

      • 6가지 Qos 종류가 존재한다.

    • DispatchQueue.global(qos: )

      • .default == global

  3. Private Queue (custom)

    • Qos를 설정 가능하다.

    • DisparchQueue(label: “”)

GCD 사용시 주의 사항

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

  • Thread 2의 작업 결과를 반드시 Thread1에 보내줘야할 때

    • Main Thread가 화면을 다시 그리는 역할이다.

      • ⇒ UI와 관련된 일들은 다시 Main Thread에 보내주어야한다.

      • UI 관련 작업은 메인 쓰레드에서 하지 않으면, 에러가 발생한다.

    • URL session은 내부적으로 비동기로 처리된 함수이다.

    • 따라서 이미지 표시 관련 코드는 반드시 메인 스레드로 보내주어야 한다.


컴플리션 핸들러 존재 이유

콜백함수를 제대로 사용해야한다

  • main Thread는 일을 시킨 후, 작업의 종료를 기다리지 않기 때문이다.

  • 비동기 작업이 끝난 후, 원래 있던 위치에 리턴하기 때문에, 비동기 작업이 끝나는 시점을 파악해야한다.

    • 비동기적 함수는 return 타입으로 설계할 수 없어서 콜백함수로 설계해야한다.

      • 일반적으로 애플이 설계해놓은 클로저 이름이 completionHandler이다.

    • 즉시리턴하므로, 데이터 리턴이 아닌 클로저를 실행하는 것이 적절하다.

      • 함수 내부의 작업이 끝나면 클로저가 실행될 수 있도록 설계하는 것이 적절하다.

      • 즉, 함수 내부의 일이 끝나기 전에 return하므로, 항상 nil이 반환되기 때문이다. 이에 비동기적 작업을 할 때에는 클로저를 호출하는 것이 바람직한 것이다.

    // 올바른 비동기함수의 설계 - 콜백 함수의 사용, return형으로 설계하면 안 된다. (async 를 선언하면 가능하다)
    
    func pro(with wrulString: String, **completionhandler: @escaping(UIImage?) -> Void**) {
        
    
        // 어쩌고
    
            **completionHandler(photoImage)** //closure의 실행
        }.resume()
    }

weak, strong 캡처의 주의

  • 캡처 리스트 내에서 weak self로 선언하지 않으면 강한참조가 된다.

    • 일반적인 경우 weak self로 선언하는 것을 권장한다.

    • 서로를 가리키는 경우, 메모리 누수가 발생할 수 있다.

    • 메모리 누수가 발생하지 않아도, 클로저의 수명주기가 길어지는 현상이 발생할 수 있다.

예제

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()
  • Global Queue closure가 강하게 캡처되어 뷰 컨트롤러의 RC가 유지된다.

    • 뷰 컨트롤러가 해제되어도, 3초 후 출력한 후 해제된다.

    • 강한 순환 참조가 일어나지는 않았으나, 뷰컨트롤러가 필요 없음에도 불구하고 오래 머무르게 된다.

    • 뷰 컨트롤러가 사라졌음에도 출력하는 일을 계속 한다.

class ViewController1: UIViewController {
    
    var name: String = "뷰컨"
    
    func doSomething() {
        // 강한 참조 사이클이 일어나지 않지만, 굳이 뷰컨트롤러를 길게 잡아둘 필요가 없다면
        // weak self로 선언
        DispatchQueue.global().async { **[weak self]** in
            guard let weakSelf = self else { return }
            sleep(3)
            print("글로벌큐에서 출력하기: \(weakSelf.name)")
        }
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}

func localScopeFunction1() {
    let vc = ViewController1()
    vc.doSomething()
}

localScopeFunction1()
  • 뷰컨트롤러를 오래동안 잡아두지 않음

  • 뷰컨트롤러가 사라지면 ===> 출력하는 일을 계속하지 않도록 할 수 있음 (if let 바인딩 또는 guard let 바인딩까지 더해서 return 가능하도록)


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

  1. 작업을 오랫동안 실행하는 함수가 있을 때

    • 작업을 오랫동안 실행하는데, 동기적으로 동작하는 함수를 비동기적으로 만들어 반복적으로 사용하도록 만든다.

    • 내부적으로 다른 큐로 비동기적으로 보내서 처리한다.

    • DispatchQueue로 감싸서 처리하면 가능하다.

      • return이 있는 형태라면, closure로 감싸서 reuturn 할 수 있다.

비동기적으로 구현된 메서드

  • 일반적으로 대부분의 네트워킹 등 오래걸리는 API는 비동기적으로 구현 돼 있다.

  • 그러나, 그렇지 않아 DispatcQueue로 클로저를 보내 명시적으로 비동기처리가 필요한 경우도 있다.

Async/await

Async await

  • Swift 5.5부터 사용가능한 방법이다.

  • 비동기함수를 이어서 처리하는 것이다. (코드 상의 불편함을 해결한 것임)

  • 작업이 끝나는 시점에 Completion 블럭을 실행시키는 것이다.

    • Completion 블럭에서 Completion 블럭을 넣을 수 있다.

장점

  • 들여쓰기가 필요 없다

    • 죽음의 피라미드 (Pyramid of doom을 없애준다, 들여쓰기를 계속해야하는 것)

  • 리턴 시점을 기다리지 않아도 된다

  • 클로저를 통해 전달하지 않아도, return을 실행할 수 있다. (깔끔한 코드 작성이 가능)

  • 비동기적 코드가 이어져있을 때 코드의 들여쓰기가 연결되는 것을 방지할 수 있다.


동시성 프로그래밍의 문제와 해결방안

Async await

  • Swift 5.5부터 사용가능한 방법이다.

  • 비동기함수를 이어서 처리하는 것이다. (코드 상의 불편함을 해결한 것임)

  • 작업이 끝나는 시점에 Completion 블럭을 실행시키는 것이다.

    • Completion 블럭에서 Completion 블럭을 넣을 수 있다.

장점

  • 들여쓰기가 필요 없다

    • 죽음의 피라미드 (Pyramid of doom을 없애준다, 들여쓰기를 계속해야하는 것)

  • 리턴 시점을 기다리지 않아도 된다

  • 클로저를 통해 전달하지 않아도, return을 실행할 수 있다. (깔끔한 코드 작성이 가능)

  • 비동기적 코드가 이어져있을 때 코드의 들여쓰기가 연결되는 것을 방지할 수 있다.

Last updated