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

iOS 앱에서 동기와 비동기는 아주 자주사용되는 내용이고 사실 강의를 들으면서도 좀 어려운 개념이었다. 여러번 반복해서 들으면서 이 개념을 조금 더 익힐 수 있도록 해야겠다.

 

오늘 공부한 비동기 , 동시성 프로그래밍 관련 2탄 시작해보자


동기(sync) vs 비동기(async)

동기(sync)

  • 동기적인 작업은 순차적으로 실행되며, 한 작업이 끝나야 다음 작업이 실행된다.
  • 동기적인 작업은 보통 현재 실행 중인 스레드에서 처리되며, 작업이 완료될 때 까지 해당 스레드는 차단된다.

비동기(async)

  • 비동기적인 작업은 순차적으로 실행되지 않고 별도의 스레드에서 바로 새로운 작업이 시작된다.
  • 비동기적 작업은 보통 백그라운드 스레드에서 처리되며, 작업이 완료되면 메인 스레드로 결과를 반환하거나 콜백 함수를 호출하여 결과를 처리한다.

동기적 작업은 주로 간단하고 짧은 작업에 사용되고, 작업의 순서가 중요한 경우에 사용된다. 반대로 비동기 작업은 네트워킹과 같은 주로 오래 걸리는 작업이나 대량의 데이터 처리에서 사용된다.

 

동기 → 비동기로 변형하는 방법

위에서 설명한 내용과 같이 순차적으로 실행하는 동기적 처리를 그럼 비동기 처리로 바꾸는 방법은 무엇일까?

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: "잡스", completion: <#T##(String) -> Void#>)

asyncLongtimePrint(name: "잡스") { (result) in
    print(result)

그냥 비동기 처리로 한번 감싸주면 원래 동기 였던 함수 혹은 메서드는 비동기 처리로 변경되게 된다.

 

직렬(Serial) vs 동시(Concurrent)

동시성 프로그래밍 직렬(Serial) , 동시(Concurrent)의 개념

직렬(Serial)은 보통 메인에서 분산처리 시킨 작업을 다른 한개의 쓰레드에서 순차적으로 처리하는 Queue이다.

동시(Concurrent)는 보통 메인에서 분산처리 시킨 작업을 다른 여러개의 쓰레드에서 동시다발적으로 처리하는 Queue이다.

 

 

그렇다면 동시성 프로그래밍을 분산처리하는게 더 좋으니 동시(Concurrent) 만 사용하면 될텐데 직렬 Queue가 필요한 이유는 무엇일까?

 

직렬은 순서가 중요한 작업을 처리할 때 사용될 수 있으며, 멀티 스레드와 같은 상황에서 동시다발적으로 하나의 데이터에 접근할 때 Thread-safe 하게 처리할 수 있도록 하는 역할을 담당할 수 있다.

var array = [String]()

let serialQueue = DispatchQueue(label: "serial")

for i in 1...20 {
    DispatchQueue.global().async {
        print("\\(i)")
        //array.append("\\(i)")    //  <===== 동시큐에서 실행하면 동시다발적으로 배열의 메모리에 접근
        
        serialQueue.async {        // 올바른 처리 ⭐️
            array.append("\\(i)")
        }
    }
}

// 5초후에 배열 확인하고 싶은 코드 일뿐...

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    print(array)
    //PlaygroundPage.current.finishExecution()
}

이 코드는 하나의 반복문에 동시다발적으로 접근하여 데이터가 정상적으로 출력되지 않는 오류가 있는 코드이나, 직렬 Queue 를 사용하여 순차적으로 데이터가 접근할 수 있도록 한 뒤 데이터가 오류 없이 출력될 수 있도록 구성된 코드이다.

이러한 상태를 Thread-safe 한 상태라고 한다.

 

weak , strong 참조 오류 주의

iOS앱 개발 중 메모리가 제대로 해제 되지 않아 앱의 성능을 떨어트릴 수 있는 메모리 누수가 일어날 수 있는 오류로 이를 강한 참조로 구성되어있는 내용이 그대로 동시적 처리를 통해 처리 된다면 비교적 느린 데이터 처리를 하는 특성 상 그만큼 참조하지 않아도 될 대상의 참조 시간이 길어지고 메모리 누수가 발생할 수 있다.

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

현재 이 코드는 이미 밑에 있는 doSomething 함수가 출력이 완료되어 실행이 종료되어도 비교적 느린 데이터를 처리하는 과정에서 상단의 doSomething 함수가 참조되어 있어 해당 처리가 완료될 때 까지 메모리가 해제 되지 못하는 문제를 가지고 있다.

 

이를 해결 하기 위해 해당 참조된 부분을 그냥 self 로 강한 참조를 하는 것이 아닌

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

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

위와 같이 캡처리스트 + weak 를 사용하여 참조한다면 메모리가 낭비되는 시간 없이 데이터 처리가 가능해진다.

 

이렇게 2편에 걸처 iOS 동시성프로그래밍 , 비동기처리 등 알아보는 시간을 가졌다 생각보다 어려운 개념인 것 같아 여러번 반복해서 이해하도록 하자 🙂