1. 동기/ 비동기 개념에 대한 이해

쓰레드

연산을 수행하는 연산 장치 ( 코어 하나당 대부분 2개의 쓰레드 )

  • 소프트웨어적인 Thread - NSThread라고 불리는 객체임
  • 메인쓰레드 (1번 쓰레드)는 앱의 시작과 화면을 다시 그리는 역할도 한다 (60Hz 1초에 60번)
    • 여러 역할을 수행하므로 너무 오래 걸리는 작업 시키면 x
    • 다른 thread로 분산처리를 해야함!

병렬 vs 동시

  • 병렬물리적인 쓰레드에서 실제 동시에 일을 하는 개념이고
  • 동시성은 메인 쓰레드가 아닌 다른 소프트웨어적인 쓰레드에서 동시에 일을 하는 개념이다!

개발자는 동시성 부분은 신경쓰면 된다.

여러 개의 쓰레드에서 작업을 해도 내부 물리적인 쓰레드는 1개만 사용할 수도 있음

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

  • 네트워크 통신 - 서버에 데이터 (이미지 등) 를 요청하는 일을 부하가 많이 걸리고 시간이 오래 걸림!
  • 비동기 처리를 하지 않으면 → 네트워크 통신과 같은 실행이 제대로 동작하지 않아 화면이 버벅거림
  • 비동기 처리와 동시성 프로그래밍 → 성능/ 반응성/ 최적화와 관련되어 있다!

3. 비동기, 동기의 개념

동기 처리 (Synchronous)

  • 하나의 작업을 마칠 때까지 다음 작업들이 대기하는 것
  • 서버에 요청을 보냈을 때 응답이 돌아올 때까지 다른 작업들을 멈추고 대기
func task1() {
    print("Task 1 시작")
    sleep(2)
    print("Task 1 완료★")
}

func task2() {
    print("Task 2 시작")
    sleep(2)
    print("Task 2 완료★")
}

func task3() {
    print("Task 3 시작")
    sleep(2)
    print("Task 3 완료★")
}

task1()
task2()
task3()
// 각자의 task가 수행이 완료되어야만 다음 줄의 함수를 실행한다!

비동기 처리 (Asynchronous)

  • 여러 개의 쓰레드에서 각자 작업들을 수행한다.
  • 서버에 요청을 보내고 응답이 돌아오기 전에도 다른 작업들을 수행할 수 있음
  • 일을 시작 시키고, 작업이 끝날 때까지 기다리지 않는다 !!
func task4() {
    DispatchQueue.global().async {
        print("Task 4 시작")
        sleep(2)
        print("Task 4 완료★")
    }
}

func task5() {
    DispatchQueue.global().async {
        print("Task 5 시작")
        sleep(2)
        print("Task 5 완료★")
    }
}

func task6() {
    DispatchQueue.global().async {
        print("Task 6 시작")
        sleep(2)
        print("Task 6 완료★")
    }
}

task4()
task5()
task6()
// 동시 다발적으로 queue에서 여러 개의 쓰레드로 보냄 -> 일이 끝나지 않아도 다음 줄 수행
// (기다리지 않는다!)

// 출력:
// Task 6 시작
// Task 4 시작
// Task 5 시작
// 2초 쉬고,
// Task 5 완료★
// Task 4 완료★
// Task 6 완료★
// -> 각자 다른 쓰레드에서 수행되므로 순서도 바뀐다

직렬(serial) vs 동시(concurrent)

  • 큐가 thread를 처리하는 방식
  • 직렬은 한 개의 쓰레드에서 처리한다
  • 동시는 서로 다른 여러 개의 쓰레드에서 처리한다

직렬큐가 필요한 이유?

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

→ 동시성 프로그래밍의 문제를 해결할 때 사용한다


4. GCD의 개념 및 종류

큐의 종류

Queue : 작업들을 여러 쓰레드로 보내 분산처리한다@

  • 직접적으로 쓰레드를 관리하지 않고 큐로 보내 작업은 분산처리 한다.
  • OS가 알아서 쓰레드 개수 관리
    1. DispatchQueue ( GCD - Grand Central DispatchQueue)
    • (글로벌) 메인 큐 DispatchQueue.main
    • 글로벌 큐 DispatchQueue.global()
    • 프라이빗 큐 (커스텀 큐) DispatchQueue(label:"...")
      1. OpeationQueue
    • 메인 큐 OperationQueue.main
    • 프리이빗 큐 (커스텀 큐) OperationQueue()

1) 메인 큐

  • 메인쓰레드 == 메인 큐
  • 유일하게 한 개이고, Serial 큐!
  • 실제로 그냥 메인 쓰레드 (1번 쓰레드)임
  • 별도의 코드를 작성하지 않으면 모든 작업은 메인 큐에서

2) 글로벌 큐 & Qos

  • 종류가 여러 개이고, Concurrent 큐이다!
  • Qos (Quality of Service)는 6개의 종류가 있다

  • Qos의 개념
    • iOS가 업무의 중요도에 따라 우선순위를 매겨서 더 많은 쓰레드를 배치하고, CPU의 배터리 집중해서 일을 빨리 끝내도록 함

3) 프리이빗(커스텀) 큐

  • 커스텀으로 개발자가 직접 만드는 큐
  • Serial Queue (기본 설정) Concurrent도 가능
  • Qos도 설정 가능!

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

1) UI 업데이트는 메인 쓰레드에서 이뤄져야 된다!!!!

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

2) 올바른 비동기 함수의 설계! - 컴플리션 핸들러 (콜백함수) 사용

  • 네트워크 통신 비동기 프로그래밍시 → 함수 return 형식 x 클로저 형식
  • 다른 쓰레드로 작업을 시키기 때문에 메인 쓰레드 그 작업이 끝나는 시점을 알 수 없음!
  • → 끝나는 시점에 completion 함수 실행 (파라미터가 클로저)
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()

    
    return photoImage    // 항상 nil 이 나옴
}

getImages(with: "https://bit.ly/32ps0DI")    // 무조건 nil로 리턴함 ⭐️

********** 콜백함수 활용 **********

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

3) weak, strong 사용 시 주의

[객체 내에서 비동기 코드 사용시 주의!]

  • 강한 참조
    • 클로저 내부에서 객체의 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()은 이미 종료가 되어서 vc가 사라졌지만,
// 클로저가 vc를 3초동안 붙잡아둠 (강한 참조) RC 증가

// 약한 참조 예시 (weak self 사용)
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()
}
// 뷰컨 메모리 해제
// 글로벌큐에서 출력하기: nil

// 
// self가 없어졌으므로 self에 nil값이 들어가있음!

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

  • 일반 (동기) 함수 : 오래 걸리는 일반적인 함수를 단순히 동기 함수로 만들면 메인쓰레드에 부하
  • 비동기 함수 : 오래 걸리는 일반적인 함수를 내부에 비동기적 처리를 통해 비동기로 동작하는 함수로 변형 가능
// 오래 걸리는 함수!
func doSomething() {
	print("프린트 시작")
	sleep(3)
	print("프린트 종료")
}

doSomething()

// 비동기로 동작하도록 변형
func doSomething(completion: @escaping (Void) -> Void) {
	DispatchQueue.gloabl().async {
		print("프린트 시작")
		sleep(3)
		print("프린트 종료")
		completion()
	}
}

doSomething {
	print("비동기 동작으로")
}

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

  • 애플은 일반적으로 대부분의 오래 걸리는 API들을 내부적으로 비동기적으로 구현해놓음
  • URLSession.shared.dataTask 수행 시 → 무조건 다른 Thread로 보내서 일처리 (기다리지 않음)
  • 이미 내부적으로 GCD를 이용해서, 비동기적으로 처리하는 메서드(함수)
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")

// 출력 결과
// 출력 - 1
// 출력 - 2
// 데이터들~~~

7. Async/wait의 도입 (Swift 5.5~)

  • 비동기 처리 : 다른 thread로 일을 보내고 수행을 마칠 때까지 기다리지 않고 즉시 리턴!]
  • 함수 마칠 때 return 형 x 클로저(컴플리션 블럭)을 실행한다

💡 클로저 방식 사용 시 단점 !!

completion task가 끝날 때 또 비동기식 작업을 completion task를 실행한다면 ??

비동기 함수가 여러 개 뭉쳐있으면 ??

→ 단점 : 끊임없는 콜백함수의 연결, 들여쓰기,,

func linkedPrint(completion: @escaping (Int) -> Void) {
    longtimePrint { num in
        // 코드처리
        longtimePrint { num in
            // 코드처리
            longtimePrint { num in
                // 코드처리
                longtimePrint { num in
                    // 코드처리
                    completion(num)    // 모든 비동기함수의 종료시점을 알려줌
                }
            }
        }
    }
}

async/await 키워드를 붙이면 비동기 작업이 return할 때까지 기다려줘서 리턴 타입을 사용할 수 있게 됨

→ 장점 : 들여쓰지 않고 어디서 코드가 멈추는 지 한눈에 들어온다!

코드가 깔끔해짐~

func linkedPrint2() async -> Int {
    _ = await longtimeAsyncAwait()
    _ = await longtimeAsyncAwait()
    _ = await longtimeAsyncAwait()
    _ = await longtimeAsyncAwait()
    return 7
}

8. 동시성 프로그래밍과 관련된 문제점

하나의 앱은 코/데/힙/스 영역으로 이루어짐.

대부분의 함수는 stack에 쌓았다 없어지는 구조로 메모리가 움직임!

쓰레드 하나당 Stack 하나 사용 → 각자 쓰레드마다 수행하는 작업과 사용하는 메모리 다름!

코드, 데이터, 힙 영역의 메모리는 공유한다!

  • 메인 쓰레드 stack에 main() 함수
  • global등 비동기적 함수는 다른 Stack에서 실행

1) 경쟁상황/ 경쟁조건 (Race Condition)

  • = Thread-safe 하지 않음
  • 멀티 쓰레드 환경에서, 같은 시점에 여러 개의 쓰레드에서 하나의 메모리에 동시 접근하는 문제 발생!
  • 클래스의 인스턴스와 같은 힙 영역의 메모리에 여러 쓰레드가 동시 접근

    (이름을 쓰는 와중에 다른 쓰레드에서 이름을 읽을 경우 ???)

  • 한 쓰레드에 하나만 접근할 수 있도록 Lock을 거는 방식으로 해결 (Thread-Safe 처리)

2) 교착상태 (Deadlocks)

  • 멀티 쓰레드 환경에서, 베타적인 메모리 사용으로 일이 진행이 안되는 문제
  • 쓰레드의 작업이 종료되지 않아 앱이 멈추는 상황!
  • 앱이 여러 개일 경우도 같은 함수를 사용하다 설정이 바뀌면 모든 앱이 중단된다.

9. 동시성 프로그래밍 문제점의 해결

⭐️ 동시큐에서 직렬큐로 보내기

여러 쓰레드에서 동시 접근하는 작업은 다시 Serial Queue로 보낸다

→ 순차적인 처리 가능 & 하나의 쓰레드에서만 메모리에 접근