UIKit의 대부분의 요소는 thread-safe 하지 않다.(원자성이 보장되지 않는다) 모든 요소를 thread-safe하게 설계하기엔 UIKit이 너무 방대한 프레임워크이기 때문이다. 이를 억지로 보장하는 것은 오히려 성능상 좋지 않다.
예를 들어, UITableViewCell의 셀이 백그라운드 스레드에서 제거 되었는데 다른 백그라운드 스레드가 해당 셀이 접근하려고 하면 크래시가 발생한다. 또한 백그라운드 스레드의 Runloop에 특정 뷰에 대한 제거가 진행중이에 있는데, 사용자가 해당 뷰를 탭하면 응답을 처리하기도 애매하다
View Drawing Cycle을 생각해보면 하나의 Runloop에서 이를 확인하고 관리하게 된다. 하지만 만약 각 스레드의 Runloop에서 View의 life cycle을 관리하게 되면 화면의 UI가 변경되었을때 모든 변경사항을 화면에 존재하는 여러 View들이 확인할 수 없게 된다. 따라서 View가 깨진다.
ios는 그림을 표현할 때 특정한 렌더링 프로세스(코어애니메이션 -> 렌더서버-> GPU-> 표시) 를 거치는데 여러 스레드에서 각 뷰의 변경사항을 GPU로 보내면 각 정보를 해석하기 위해 많은 오버헤드가 발생할 수 있다.
UIApplication은 main thread에서 Main Run Loop라고 불리는 런루프를 생성한다. 앱 내에서 발생하는 대부분의 이벤트를 관장한다. 이 Main Run Loop를 통해 스크린의 내용이 refresh 될 수 있다.
view의 변경은 즉시 일어나지 않는다. 이번 RunLoop의 마지막에 redraw하여 view가 변하게 되는데 이러한 변경을 View Drawing Cycle이라고 부른다.
이러한 Run Loop는 thread마다 가지고 있기 때문에 만약 background thread에서 view의 변경이 가능하다면 view가 동시에 변해야 하는 상황(화면의 회전)에서 view들이 동시에 변하지 않는 문제가 발생할 것이다.
ios에서 업데이트 사이클(이벤트 결과를 화면에 출력) 작업을 수행할 수 있는 스레드는 오직 1번쓰레드 (main thread) 뿐입니다.
때문에 다른 스레드에서 작업을 수행하던 중 UI 관련 작업이 시작되면 , 해당 작업을 메인 큐를 통해 1번 쓰레드로 보내야 한다.
즉 UI관련 작업은 1번 쓰레드에서만 할 수 있으며 다른쓰레드에서 UI 작업을 만나면 1번 쓰레드로 보내야 한다.
DispatchQueue.global().async {
// 다양한 작업(UI 제외)
DispatchQueue.main.async {
//UI 작업
}
}
//main.sync가 안되는 이유 : 경합 조건때문에 데드락 발생 가능성 있기 때문
ios에서 비동기(Async) 처리 방식은 "해당 작업을 기다리지 않고 다음 작업을 진행하는 방식"
이러한 방식은 작업을 분산처리하여 성능을 높인다는 장점을 가지고 있지만 특정 작업의 함수 결과물을 의존/ 사용하는 다른 작업이 존재할 경우 에러가 발생할 수 있다.
var resultName: String?
func myName(name: String) -> String?{ // n번 쓰레드에서 작업
DispatchQueue.global().async {
sleep(2)
resultName = name
}
return resultName
}
print(myName(name: "김철수")) // print() 함수는 n번 쓰레드의 함수 결과를 사용
/* 출력 결과
nil // myName() 함수의 작업이 끝나기 전에 print() 함수의 작업이 진행하기 때문에 nil(에러) 출력
*/
비동기적인 작업을 해야 하는 함수는 항상 클로저를 호출할 수 있도록 함수를 설계해야 합니다.
var resultName: String?
func myName(name: String, completionHandler: @escaping (String?) -> Void){ // n번 쓰레드에서 작업
DispatchQueue.global().async {
sleep(2)
resultName = name
completionHandler(resultName)
}
}
myName(name: "김철수") { XXX in
print(XXX)
}
/* 출력 결과
Optional("김철수")
*/
객체 내에서 비동기 코드를 사용할 때는 약한/ 강한 참조를 생각하면서 코드를 작성해야 한다.
class Man{
var name: String
var run: (()->Void)?
init(name: String){
self.name = name
}
func runClosure(){
DispatchQueue.global().async {
self.run = {
print("\(self.name)이 달리고 있습니다.")
}
}
}
deinit{
print("\(self.name) 메모리에서 제거되었습니다.")
}
}
func doSomething(){
var kim: Man? = Man(name: "김철수") // kim 인스턴스 생성 (kim RC 1증가)
kim?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성
}
doSomething() // 아무 출력 없음
대부분의 경우 캡처리스트 안에서 weak self로 선언하여 약한 참조 하게끔 하는 것을 권장한다.
class Man{
var name: String
var run: (()->Void)?
init(name: String){
self.name = name
}
func runClosure(){
DispatchQueue.global().async {
self.run = { [weak self] in
print("\(self?.name)이 달리고 있습니다.")
}
}
}
deinit{
print("\(self.name) 메모리에서 제거되었습니다.")
}
}
func doSomething(){
let kim: Man? = Man(name: "김철수") // kim 인스턴스 생성 (kim RC 1증가)
kim?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성
}
doSomething() // 김철수 메모리에서 제거되었습니다.
작업 시간이 긴 함수들을 동기 함수로 만들면 1번쓰레드에 과부하가 걸립니다. 이러한 이유로 작업시간이 긴 함수를 내부에 비동기적 처리를 하여 비동기로 동작하는 함수로 변형해야 합니다.
func myTask(programmingLanguage: String) -> String{
print("수학 숙제 시작")
sleep(2)
print("수학 숙제 종료")
print("\(programmingLanguage) 코딩 공부 시작")
sleep(2)
print("코딩 공부 종료")
print("영어 숙제 시작")
sleep(2)
print("영어 숙제 종료")
return "공부 종료"
}
myTask(programmingLanguage: "Swift")
동기 함수를 비동기적으로 동작하는 함수로 변형하는 함수를 생성하여 만들면 버벅임과 같은 현상을 예방할 수 있습니다.
func myTask(programmingLanguage: String) -> String{
print("수학 숙제 시작")
sleep(2)
print("수학 숙제 종료")
print("\(programmingLanguage) 코딩 공부 시작")
sleep(2)
print("코딩 공부 종료")
print("영어 숙제 시작")
sleep(2)
print("영어 숙제 종료")
return "공부 종료"
}
func asyncMyTask(programmingLanguage: String, completionHandler: @escaping (String) -> Void){
DispatchQueue.global().async {
let function = myTask(programmingLanguage: programmingLanguage)
completionHandler(function)
}
}
asyncMyTask(programmingLanguage: "Swift") { XXX in
print(XXX)
}