[Swift] 비동기 프로그래밍

임승섭·2023년 7월 3일
0

Swift

목록 보기
23/35

Warm-up

동기 vs 비동기

  • 비동기(Async) : 작업을 다른 쓰레드에서 시키고, 끝날 때까지 기다리지 않는다
    즉, 현재 쓰레드에서 다른 작업을 시작할 수 있다
  • 동기(Sync) : 기다린다. 다른 쓰레드로 보낸 작업이 끝날 때까지 기다린다
    다른 작업을 처리할 수 없다

직렬 vs 동시

  • 직렬 큐 vs 동시 큐
  • 직렬 : 하나의 쓰레드로만 작업들을 보낸다 (작업에 순서가 중요할 때)
  • 동시 : 여러 쓰레드로 작업을 분산시켜서 보낸다

동시성 프로그래밍

비동기 처리가 필요한 이유

  • 서버에 데이터를 요청하는 일(네트워크 통신)은 부하가 많이 걸린다
  • 만약 비동기 처리를 해주지 않으면, 화면을 스크롤 할 때마다 버벅이게 된다
    • 화면 주사율 120Hz : 1초에 화면을 120번 다시 그린다

동시성 처리

  • 작업을 대기행렬(큐)에 보내기만 하면, iOS가 알아서 여러 쓰레드로 나눠서 분산처리(동시적 처리)를 한다
  • 즉, 내가 해야 할 일은 작업을 큐에 보내는 것

DispatchQueue

  • GCD - Grand Central DispatchQueue
  • 직접적으로 쓰레드를 관리하는 개념x
    큐의 개념 이용해서 작업 분산처리하고 OS에서 알아서 쓰레드 숫자 관리
  • 쓰레드보다 더 높은 레벨에서 작업 처리
  • 메인 쓰레드가 아닌 다른 쓰레드에서 오래걸리는 작업들과 같은 작업들이 쉽게 비동기적으로 동작하도록 함

병렬 vs 동시성

  • 병렬 : 물리적인 쓰레드에서 동시에 일을 하는 개념 (내부적으로 알아서 동작. 하드웨어적인 내용)
  • 동시성 : 메인 쓰레드가 아닌 다른 소프트웨어적 쓰레드에서 동시에 일을 하는 개념 (개발자가 신경써야 하는 부분)

GCD의 개념 및 종류

메인 큐

DispatchQueue.main

  • 메인 쓰레드이자 메인 큐 (메인 쓰레드 == 메인 큐)
  • 유일한 한개, 직렬, 실제는 그냥 메인 쓰레드
let mainQueue = DispatchQueue.main

글로벌 큐

DispatchQueue.global()

  • 여러 종류, 동시
  • QoS(서비스 품질. Quality of Service)에 따라 6가지
    • iOS가 알아서 우선적으로 중요한 일인 걸 인지하고
      쓰레드에 우선순위를 매겨서 더 많은 쓰레드를 배치하고
      CPU의 배터리를 더 집중해서 사용하도록 해서 일을 빨리 끝내도록 함
  • 서비스 품질이 높을수록 여러 쓰레드 사용
  • 소요 시간이 각자 다르다
let userInteractiveQueue = DispatchQueue.global(qos: .userInteractive)
let defaultQueue = DispatchQueue.global()  // 디폴트 글로벌큐

프라이빗 큐

DispatchQueue(label: "...")

  • 커스텀으로 만드는 큐, 기본은 직렬 (attribute으로 동시 만들 수 있다)
let privateQueue = DispatchQueue(label: "com.inflearn.serial")

GCD 사용 시 주의사항

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

  • 1번 쓰레드에서 화면 그리는 작업을 하기 때문에,
    다른 쓰레드에서 화면 그리는 작업을 할 때는 반드시 메인 큐로 보내줘야 한다
/*Ex 1*/
// UI 관련 작업들은 메인 쓰레드에서 처리하지 않으면 에러가 발생한다 (메인 쓰레드가 아닌 쓰레드는 그림을 다시 그리지 못한다)
DispatchQueue.global(qos: .utility).async {
	...
    ...
    self.textLabel.text = "New post updated!"
} 	// -> 에러

// UI 관련 작업들을 메인쓰레드에서 처리할 수 있도록 메인큐를 통해 작업을 다시 메인쓰레드로 보내준다
DispatchQueue.global(qos: .utility).async {
	...
    ...
    DispatchQueue.main.async {
    	self.textLabel.text = "New post updated!"
    }
}


/*Ex 2*/
var imageView: UIImageView? = nil
let url = URL(string: "https://bit.ly/32ps0DI")!

// URL세션은 내부적으로 비동기로 처리된 함수임. -> 메인 쓰레드가 아닌 다른 쓰레드에서 작동하고 있다고 이해하기
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()

⭐️ 컴플리션핸들러의 존재 이유 - 올바른 콜백함수의 사용

  • 다른 쓰레드에서 작업을 시킬 때, 그게 언제 끝나는지에 대해 생각해야 한다

  • 또한, 다른 쓰레드에서의 작업이 끝나면, return으로 전달하는 게 아니고 클로저로 전달해야 한다

  • 콜백함수를 통해, 작업이 끝난 뒤의 결과를 받아야 한다. (작업을 시작하고 바로 리턴하기 때문에 return x)

  • return : 비동기 작업을 기다리지 않고 바로 반환 -> nil

  • return이 아닌 콜백함수를 통해, 끝나는 시점을 알려줘야 한다

// 잘못된 함수 설계
// 비동기적인 작업을 해야하는 함수를 설계할 때 return을 통해 데이터를 전달하려면 항상 nil이 return된다
func getImages(..., c..: String) -> UIImage? {
	...
    URLSession.shared.dataTask(..) {
    	...
    }.resume()
    ...
    ...
    return photoImage	// 함수 내부의 일이 끝나기 전에 return하기 때문에 무조건 nil이 return된다
}


// 올바른 함수 설계
// 비동기적인 작업을 해야하는 함수는 항상 클로저를 호출할 수 있도록 함수를 설계해야 한다
func getImages(..., completionHandler: @escaping (UImage?) -> Void) {
	...
    URLSession.shared.dataTask(..) {
    	...
    	...
    	completionHandler(photoImage)	// 함수 내부의 일이 끝나면 completionHandler 호출한다
    }.resume()
}

// full code는 자료에 있으니까 실행시켜보기

weak, strong 캡처의 주의 (강한 참조 주의)

  • 강한 참조가 일어나고, 강한 참조 사이클이 일어나지 않더라도 생각해볼 필요 있다
  • 코드는 자료 확인

동기함수를 비동기함수로 변형

  • 오래걸리는 함수를 단순히 동기함수로 만들면 메인쓰레드에 부하
    -> 내부에 비동기적 처리를 해서 비동기로 동작하는 함수로 변형
// 작업을 오랫동안 실행하는 함수
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: "잡스") { (result) in
    print(result)
    
    // 메인쓰레드에서 처리해야하는 일이라면,
//    DispatchQueue.main.async {
//        print(result)
//    }
}

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

  • URLSession : 이미 GCD 이용해서 여러 쓰레드에서 작동함 -> 굳이 DispatchQueue로 감쌀 필요 없다
    • 일반적으로 네트워킹 등 오래걸리는 API는 내부적으로 비동기적으로 구현되어 있다

Async/Await

  • 함수 설계할 때 return 말고 클로저 형태로 반환해야 한다고 위에서 공부했다
    • 하지만 이렇게 하면, 오랫동안 작업하고 그게 끝난 시점에 다시 클로저(completion)을 실행해
      • 또 그게 끝나는 시점에 클로저 실행해
        • ...
  • 비동기적인 실행을 여러 개 연결해야 할 때, 계속 코드를 들여써야 하는 단점 (pyramid boom?)
  • (비동기적으로 동작하고, 오래걸리는) 함수를 만들 때 리턴형 앞에다 async 키워드를 붙여준다
    -> 함수를 리턴 방식으로 설계해도 된다!!
  • 그 함수를 실행할 때, await를 붙여서, 그게 끝날 때까지 기다려준다
    (리턴을 기다림)
    -> 리턴을 받을 수 있기 때문에 들여쓰기할 필요도 없다는 장점. 클로저를 사용하지 않아도 된다는 장점

메모리

  • 코드, 데이터, 힙 영역은 공유
  • 스택이 여러 개로 나뉨 (여러 쓰레드)

⭐️ 동시성 프로그래밍의 문제점

경쟁 상황 / 경쟁 조건

  • 각자 쓰레드(스택)에서 데이터 + 힙 영역에 같은 시점에 동시에 접근한다. 경쟁 상황
    • 2번 쓰레드는 데이터 값을 바꾸려고 하고, 3번 쓰레드에서는 그걸 읽으려고 해
      -> Thread-Safe 하지 않다
  • 멀티 쓰레드 환경에서 같은 시점에 여러개의 쓰레드에서 하나의 메모리에 동시접근 하는 문제
  • 해결하는 여러 가지 방법 (Thread-Safe 처리)
    • Lock을 거는 코드 -> 동시에 여러개가 접근 못하도록

교착 상태

  • 2번 쓰레드에서 a 변수의 이름 바꾸고 있고, 3번 쓰레드는 b 변수 이름 바꾸고 있어.
    만약 2번 쓰레드에서 b 변수의 이름이 필요한 상황인데, 3번 쓰레드가 작업중이라 Lock이 걸림
    -> 메서드가 일을 종료하지 못함
    • 앱이 아예 멈춰버린다
  • 멀티 쓰레드 환경에서 배타적인 메모리 사용으로 일이 진행이 되지 않는 문제

동시성 프로그래밍 문제 해결

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

  • 여러 개의 쓰레드에서 같은 메모리에 접근하려 할 때,
    그걸 다시 Serial Queue로 보내면,
    거기서는 순서대로 하나씩밖에 접근하지 못하기 때문에
    서로 겹치지 않는다
var array = [String]()		// 빈 배열 -> 메모리 공간에 하나만 존재
let serialQueue = DispatchQueue(label: "serial")	// 직렬 큐 하나 생성

for i in 1...20 {	// 1부터 20까지 비동기적으로 실행 -> 여러 쓰레드에서 배열에 접근
    DispatchQueue.global().async {
        print("\(i)")
        //array.append("\(i)")    //  <===== 동시큐에서 실행하면 동시다발적으로 배열의 메모리에 접근 -> append가 제대로 작동이 되지 않는다. 중간중간 빠진 숫자가 존재
        
        // 직렬 큐를 통해 한 번에 하나의 쓰레드에서만 접근할 수 있도록 한다!!
        serialQueue.async {        // 올바른 처리 ⭐️
            array.append("\(i)")
        }
    }
}

0개의 댓글