iOS/동시성 프로그래밍

디스패치 큐(GCD) 사용시 주의해야 할 점

소재훈 2022. 4. 6. 04:24

디스패치 큐를 그럼 마음대로만 쓰고싶은대로 사용하면 되는 것일까?🤔

분명 그런 것은 아닐 것이다.

어떤 경우에는 이건 쓰면 안되고...저걸써야하고...이런것은 하면안되고

이러한 규칙이 반드시 있을 것이다.

이번에는 디스패치 큐를 사용해야 할 때 주의해야할 점에 대해서 알아보자

 

📝 UI관련 작업은 반드시 메인 큐에서 처리한다

UI관련작업은 반드시 메인큐(DispatchQueue.main)에서 처리해야한다.

1번쓰레드는 계산등 다른 작업도 하지만 주로 화면 출력을 담당하고 있다.

모든 OS가 거의 그렇지만 화면을 표시하는 작업은 하나의 쓰레드에서 이루어진다.

만약 화면을 표시하는 작업이 여러쓰레드에 분산되어 이루어진다면 화면간에 간섭이 일어나거나 깜빡이는 현상이 발생할 수 있으니 주의하자

 

또한 작업 전체가 UI관련 작업이 아니더라도 작업 UI작업이 포함되어 있다면 그 부분만 main큐로 보내어 작업해줄 수 있다.

위의 그림에서 볼 수 있듯이 작업의 끝에 UI관련 작업이 있으면 다시 main큐로 작업을 보내주어야한다.

여기서는 작업안에서 또다른 작업을 다른 큐로 보낼 수 있을에 주목하고 기억하자!!

 

코드로 표현한 것을 살펴보자.

전체 코드는 오래 걸리는 작업이니 비동기로 분산해서 작업을 처리하고 싶은 의도이다.

하지만 UI관련 작업, 화면과 관련된 작업은 다시 메인큐로 내보내는 모습이다.

DispatchQueue.global().async {
    //작업
    //작업
    //작업
    
    DispatchQueue.main.async {
    	self.imageView.image = image
    }
}

 

⚠ playground VS 실제 앱
실제 앱에서 UI관련 동작은 main큐에서 동작한다.
하지만 playground에서는 global default큐에서 동작함에 주의하자.

 

📝 메인큐에서 다른 큐로 보낼 때 sync메서드를 부르면 안된다

메인큐에서는 항상 비동기적으로 작업을 보내야한다

메인 큐에서는 UI와 관련된 작업을 한다고 했었다. 그런데 메인큐에서 sync메서드를 통해서 작업을 수행하게 되면 메인큐가 보낸 작업이 끝날 때까지 대기하게되고, 그러면 UI가 멈추고 유저와의 반응이 느려져 화면이 버벅이는 현상이 발생할 수 있다.

 

현재 메인에서 작업하고 있따면 다음과 코드는 절대로 작성하면 안된다

//절대로 안돼!!
DispatchQueue.global().sync {

}

다음과 같이 작성해주도록 하자.

DispatchQueue.global().async {

}

 

📝 현재 큐에서 현재의 큐로 동기적으로 보내서는 안된다

현재 큐에서 작업을 쓰레드로 보내준 다음, 작업을 다시 현재 큐로 동기적으로 보내게되면

쓰레드는 큐로보낸 작업이 끝나기를 기다려야하기 때문에 block된다.

그런데 만약에 큐가 큐로 작업을 보낸 쓰레드로 다시 작업을 보내게되면 이미 block된 쓰레드를 사용할 수 없기 때문에 교착상태(DeadLock, 데드락)이 발생하게 되어 앱이 종료된다. 😥

 

작업을 sync로 보내서 2번 쓰레드가 block되었는데 2번 쓰레드로 작업을 또 sync로 보내면 교챡상태가 발생하는 모습이다.

작업을 기다리느라 사용할 수 없는 쓰레드에 작업을 보내게 되는 것이다.

 

사실 큐에서 언제나 지금 block되어있는 쓰레드로 작업을 보내는 것은 아니다. block되어 있지 않은 쓰레드로 작업을 보내게되면 아무문제도 발생하지 않는다. 그러나 block되어있는 쓰레드로 작업을 보내 데드락을 발생시킬 수 있는 가능성을 내포하고 있는것이다.

 

코드로 보면 다음과 같은 코드는 절대로 작성하면 안된다는 것이다.

DispatchQueue.global().async {
    DispatchQueue.global().sync {
    
    }
}

global default 큐에서 작업을 비동기적으로 보내고, 똑같은 큐에 작업을 동기적으로 보내고있다. 이러면 안된다는 것이다.

하지만 저런 경우는 코드로 명확히 보이기 때문에 저런실수를 잘하지는 않는다

정말로 실수하는 경우는 객체에서 객체로 접근할 경우이다.

뷰 컨트롤러에서 작업을 

DispatchQueue.global().async

의 클로저 내부에서 수행하고,

클로저 내부의 객체에서 작업을 또 

DispatchQueue.global().sync

를 통해서 수행하는 실수를 범할 수 있다.

 

눈에 바로 보이지 않기때문에 실수할 수 있다.

 

📝 weak, strong 캡쳐를 주의하자

큐에 작업을 보내는 것은 클로저(Closure)이기 때문에 객체의 캡쳐현상이 발생할 수 있다.

 

DispatchQueue.global(qos: .utility).async { [weak self] in
    guard let self = self else { return }
    
    DispatchQueue.main.async {
        self.textLabel.text = "Hello"
    }
}

그냥 객체를 보내게 되면 객체의 캡쳐현상이 발생해서 뷰 컨트롤러가 dismiss되어도 여전히 동작하지만

weak와 함께 사용되면 뷰 컨트롤러가 dismiss되면 큐로 보낸 클로저도 중단되기 때문에

뷰 컨트롤러나 객체를 사용할 때는 weak self, weak 객체 와 함께 사용한다고 한다.

 

📝 (비동기 작업에서) completionHandler의 존재이유

기본적으로 비동기로 작업을 시킨 직후에!

작업에 해당하는 값을 바로 사용해서는 안된다.

작업이 아직 종료되지 않아 잘못된 값을 사용할 확률이 높기 때문이다😭

 

따라서 비동기 작업이 끝났다는 것을 알려주는 시점이 필요하고, 그 시점이

바로 completionHandler이다.

 

비동기 함수와 관련된 작업들은 모두 컴플리션 핸들러(completionHandler)를 가지고 있다고 생각하자.

비동기작업이 명확하게 끝나는 시점을 알려주는 것이다.

위 그림처럼 작업1이 생각보다 길어졌을 때, 작업2에서 작업1에서 결정되는 값을 사용한다면

잘못된 값을 사용하게 될 것이다.

 

📝 동기함수를 비동기 함수처럼 만드는 방법

오래걸리는 작업을 기다려야 할 때 내부적으로 비동기 처리를 해서

단순히 함수를 사용할 때마다 비동기 처리되도록 만들 수 있다.

 

public func tiltShift(image: UIImage?) -> UIImage? {
    guard let image = image else { return nil }
    sleep(1)
    let mask = topAndBottomGradient(size: image.size)
    return image.applyBlur(radius: 6, maskImage: mask)
}

func asyncTiltShift(_ inputImage: UIImage?,
                    runQueue: DispatchQueue,
                    completionQueue: DispatchQueue,
                    completion: @escaping (UIImage?, Error?) -> ()) {
                    
    runQueue.async {
    	var error: Error?
        error = .none
        
        let outputImage = tiltShift(image: inputImage)
        
        completionQueue.async {
            completion(outputImage, error)
        }
    }
}

비동기적인 처리를 할것이기 때문에

비동기적인 처리를 하기 위한 큐가 필요할 것이고 outputImage 에 담긴 동기적인 함수를

비동기로 처리할 큐로 보내야 할 것이다.

 

따라서 비동기적으로 처리할 큐인 runQueue(실행할 큐)

그리고 그 안에서 tiltShift함수를 수행한다.

그리고 작업이 끝났을 때 작업이 끝난 처리를 수행할 큐인 completionQueue가 필요하다.

작업의 끝난 내용을 async처리를 통해 completionQueue로 보내는 것을 볼 수 있다.

 

그리고 실제로 completion에서 어떤 작업을 처리할 지 클로저를 매개변수로 볼 수 있다.

 

비동기적인 함수로 만들기 위해서 inputImage만 필요했던 함수에서

실행할 큐

작업을 완료 후 실행할 큐

컴플리션 핸들러

세가지를 추가한 것에 주목하자.

 

실제로 사용할 때는 다음과 같이 사용하면 동기적인 함수를 비동기적으로

작업할 수 있을 것이다.

let workingQueue = DispatchQueue()
let resultQueue = DispatchQueue.global()

asyncTiltShift(image,
               runQueue: workingQueue,
               completionQueue: resultQueue) { image, error in
    print("비동기 작업 종료")
}