iOS 비동기처리(async) 동시성 프로그래밍(Concurrent) (1)

 

iOS 메인스레드 (Thread 1) 의 역할

iOS 애플리케이션은 메인 스레드를 생성하고 메인 런 루프(Main Run Loop)를 실행하여 앱의 이벤트를 처리한다.

이벤트는 주로 사용자의 상호작용(터치, 제스처)이나 시스템에서 발생하는 알림(앱의 상태변화, 백그라운드 작업 등) 등을 포함한다.

사용자 인터페이스(UI) 처리

  • 메인 스레드는 앱의 UI를 처리하는 주된 스레드이다. 사용자의 모든 상호 작용은 메인 스레드에서 처리된다.
  • 화면에 보이는 요소들의 업데이트인 레이아웃 변경, 텍스트 업데이트, 이미지 로드 등은 메인 스레드에서 수행된다.

이벤트 처리

  • 사용자의 터치, 제스처 등의 입력은 이벤트로 변환되어 메인스레드로 전달된다.
  • 메인 스레드는 이러한 이벤트를 받아들여 적절한 UI 업데이트나 작업을 수행한다.

메인 런 루프(Main Run Loop)

  • 메인 런 루프는 메인 스레드에서 실행되며, 이벤트를 받아들이고 처리하는 역할을 한다.
  • 앱의 생명 주기 동안 계속 실행되며, 이벤트를 처리할 때마다 이벤트를 Queue에서 꺼내와 처리한다.

비동기 작업 관리

  • 메인 스레드에서는 비동기적으로 수행되는 작업들도 관리된다. 이러한 작업은 보통 백그라운드 스레드에서 실행되지만, 결과를 UI에 반영할 때에는 메인 스레드에서 처리된다.
  • Grand Central Dispatch ( GCD ) 나 Operation Queue를 사용하여 비동기 작업을 메인 스레드로 보낼 수 있다.

사용자 상호 작용 감지

  • 메인 스레드는 사용자의 상호 작용을 감지하고 즉각적으로 앱의 상태를 업데이트 한다. 이를 통해 이용자는 앱이 빠르게 반응한다고 느낄 수 있다.
  • 이러한 상호 작용은 메인 런 루프를 통해 처리되며, 앱이 새로운 이벤트를 기다리고 있는 동안에도 지속적으로 실행된다.

앱의 반응성과 성능 향상

  • 메인 스레드의 역할은 앱의 반응성과 성능 향상에 큰 영향을 미친다. 적절한 UI 업데이트와 빠른 이벤트 처리는 사용자가 앱을 더욱 쾌적하게 사용할 수 있도록 한다.
  • 메인 스를 효율적으로 관리하면 앱의 성능을 최적화 할 수 있다.

 

메인 스레드(Main Thread) 조금 더 알아보기

iOS 에서 Main Thread는 오직 한개만 존재한다. 나머지는 모두 Background Thread.

 

개발자가 일반적으로 작성한 코드는 Main Thread에서 동작한다. 그 이유는 작성된 코드가 Cocoa에서 실행되는데, 이 Cocoa가 코드를 Main Thread에서 호출하기 때문.

Global Thread ( Background Thread )

iOS의 framework들은 background에서 구동된다.

 

화면에 나타나지 않는 작업은 모두 background Thread에 맡기고 delegate method 나 callback 함수로 Main Thread에서 호출하여 이벤트를 컨트롤 하는 것이 일반적이다.

 

Global Thread는 Global Queue에서 실행되며, 이 Global Queue는 Concurrent Queue( 동시성 큐 ) 이다. Global Queue에서는 Task의 우선도를 선택할 수 있으며, 직접 선택이 아닌, QosClass에서 설정한다.

 

GCD (Grand Central DispatchQueue)

(* Dispatch : 보내다)

이번 네트워킹 및 코어데이터 관련 과제를 진행하면서 가장 궁금했던 DispatchQueue에 대해서 조금 더 자세하게 알아보자.

 

GCD는 개발자가 Queue에 작업을 보내면 그에 따른 스레드를 적절히 생성해서 분배해주는 방법이다.

 

Queue에 작업을 보내야 하니까 GCD 방법에서 쓰이는 Queue의 이름이 ‘DispatchQueue’ 이다.

 

따라서 DispatchQueue에 작업을 추가하면 GCD는 작업에 맞는 스레드를 자동으로 생성해서 실행하고, 작업이 종료되면 스레드를 제거한다.

 

 

DispatchQueue에는 두가지 타입이 존재한다.

 

Main Queue

  • 오직 한개만 존재한다.
  • 이곳에 할당된 task는 Main Thread에서 처리된다.
  • Main Queue에는 sync task를 추가할 수 없다.
DispatchQueue.main.async {
	// task
}

 

Global Queue

  • Concurrent 특성을 갖는 Queue
  • Qos(Quality Of Service) 를 결정할 수 있음
DispatchQueue.global().sync {
	// task
}

DispatchQueue.global().async {
	// task
}

 

Private Queue (사용자 지정 큐)

let myQueue = DispatchQueue.init(label: "my", qos: .background, attributes: .concurrent) myQueue.async { task }

 

 

GCD를 사용할 때 주의할 점

  • UI 는 Main Thread에서 처리한다.

유저인터페이스와 관련된 작업은 메인쓰레드에서 진행할 수 있도록 해야한다.

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

URLSession 코드는 이미 비동기처리가 되어있는 상태이므로 이상태에서 특정 이미지 즉 유저인터페이스에 관련된 내용은 따로 메인 스레드에 보내는 과정이 포함된 코드이다.

 

이와 같이 UI와 관련된 일은 메인으로 갈 수 있도록 꼭 설정

DispatchQueue.global().async {
    
    // 비동기적인 작업들 ===> 네트워크 통신 (데이터 다운로드)
    
    DispatchQueue.main.async {
        // UI와 관련된 작업은 
    }
}

 

  • 비동기 처리된 함수는 return이 아닌 callback 함수를 통해 끝나는 시점을 알려줘야 한다.⭐️ ⭐️ ⭐️

일반적으로 함수는 return 형식으로 아래와 같이 구성되는 경우가 많다.

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
}

이 방법은 사용하면 안되고 올바르게 비동기 함수를 사용하려면 아래와 같이 콜백형태의 함수를 이용해야한다.

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

completionHandler를 사용하여 클로저 형태로 구성한 뒤 사용 할 수 있도록 숙지하자.

 

  • 현재와 같은 큐에 sync로 작업을 보내면 안된다.
  • 메인 스레드에서 DispatchQueue.main.sync를 사용하면 안된다.
  •  

그렇다면 왜 유저인터페이스(UI)는 메인 스레드로 가야하는 걸까?

  • UIKit의 모든 속성을 Thread-safe하게 설계하면, 느려짐과 같은 성능 저하가 발생할 수 있기 때문에 그렇게 설계할 수 없다.
  • 메인 런루프가 뷰의 업데이트를 관리하는 View Drawing Cycle을 통해 뷰를 동시에 업데이트하는 그런 설계를 통해 동작하고 있는데 백그라운드 스레드가 각자의 런루프로 UI를 업데이트하는 동작을 하면 뷰가 제멋대로 동작할 수 있다.
  • iOS가 그림 그리는 렌더링 프로세스가 있는데, 여러 스레드에서 각자의 뷰의 변경사항을 GPU로 보내면 GPU는 각각의 정보를 다 해석해야하니 느려지거나, 비효율적이 될 수 있다.

출처 : https://hyunndyblog.tistory.com/178