[iOS] 차근차근 시작하는 GCD — 6

GCD 사용시 주의 사항에 대해 알아봅시다

Previously on GCD…

어휴 오랜만이어도 너무 오랜만이네요 정말..! 이것저것 바빠서 글을 못쓴지 어언.. ㅎㅎ 그래도 한달에 한개는 쓰고 싶어서 클스마스 연휴 맞이 GCD 시리즈로 돌아왔슴다.

저번 시간에는 메인 큐, 글로벌 큐, 커스텀 큐와 같은 GCD Dispatch Queue의 종류와 특성에 대해 알아본 후 로또 당첨을 기원하며 사라졌군요.

오늘은 지금까지의 내용을 총 망라한 GCD 사용시 주의 사항에 대해 살펴볼텐데요, 그럼 바로 시작해보죠! ㄱㄱ

1. UI는 main 스레드에서 처리한다

화가 스레드! 메인 스레드!

이 시리즈를 쭉 봐온 분들이라면 익숙한 그림, 바로 우리의 메인 스레드 입니다. 얘가 이렇게 화가 모자를 쓰고 있는 이유는 바로 UI 그리는 일을 담당 하기 때문이었죠!

메인 스레드가 UI 를 담당 것은 비단 iOS 에만 국한된 것이 아니라, 모든 OS 에 적용되는 사항이라고 해요.

하나의 스레드(메인)에서 UI를 잘 처리하고 있는데 다른 스레드에서 나도 할래!! 하고 끼어들면 UI 가 의도치 않게 보일 수 있으니까.. 생각해보면 당연한 얘기일까요?

어쨌든 요지는 이미지 등을 global 에서 다운 받아오더라도, 해당 데이터를 UIImageView에 넣어서 UI 업데이트 시켜주는 작업은 main에서 해야한다는 것입니다.

뭐 대략 이런 코드들을 많이 봐오셨을텐데요! URLSession 은 opertation queue이고, 모든것이 background thread에서 동작하기 때문에

이렇게 UI 업데이트를 main 에서 해준다를 명시해주지 않으면 아래와 같은 경고가 뜹니다.

DispatchQueue.main.async 주석 처리

경고를 더블 클릭하면 Main Thread Checker 에 대한 글이 뜨는군요.

background thread에서 유효하지 않은 동작들을 잡아낸다고 합니다.

2. sync 메소드에 대한 주의 사항 2가지

2-1. 메인 큐에서 다른 큐로 작업을 보낼 때 sync를 사용하면 안된다

이런 식으로 메인 스레드에서는 다른 큐로 보낼때 .sync 를 사용하지 말라는 건데요!

음.. 왜? 라는 생각이 든다면 다시 차근차근 왜 그런지 이해해 보아요.

sync 로 보낸다는 것은 해당 작업이 끝날때까지 “기다린다” 라는 의미였죠? 그런데 우리의 메인 스레드는 UI를 업데이트 해줘야하는 친구인데 여기서 다른 작업들이 끝날때까지 기다린다?🤔 이는 곧 해당 작업이 끝날때까지 UI 업데이트가 지연된다는 의미이고 결국 화면이 버벅여 보일거예요.

그러니 메인 스레드에서는 항상 async (비동기) 로 작업을 보내도록 합시다.

2-2. 현재와 같은 큐에 sync로 작업을 보내면 안된다

같은 큐에 동기적으로 작업을 보낸다는건 바로 이런 코드인데요, 보다시피 global queue 안에서 같은 global queue에 sync 로 작업을 보내고 있죠? 왜 이게 문제되는 상황인지 그림으로 알아봅시다.

우선 task A를 global queue로 보냈기 때문에, 해당 작업은 적당한 스레드에 할당 후 수행 될 것입니다.

근데 task A를 수행하다 보니 이 안에는 task B를 global queue에 보내는 작업이 들어있었네요? 좋아요, 원하는대로 보내줍시다.

근데 보내는 방식이 sync 였네요. 따라서 task B가 스레드에 할당되어 작업이 끝날때까지는 다음 동작을 수행하지 말고 기다려야겠어요.

즉 이런 상태이죠

  • 그림에서 Task A를 수행하고 있었던 Thread 2는 Task B가 끝날때까지 기다리느라 멈춰있음
  • GCD는 Global queue에 들어온 Task B를 어디 Thread에 할당할지 고민

자 GCD는 결정을 마치고 Task B를 할당합니다. 오잉 근데 Thread 2로 할당되어 버렸네요?

근데 Thread 2는 바로 task B 가 끝날때까지 기다리고 있는 스레드였어요.

할당된 스레드에서 작업을 시작을 해야하는데, 이게 멈춰있는 스레드다?!! 이 스레드는 이제 데드락입니다.

그림과 같이 큐에서 사용하고있는 쓰레드 객체는 정해져 있습니다. 예를 들어 디폴트 글로벌큐는 쓰레드 2, 3, 4번만 사용하고, 백그라운드 글로벌큐는 쓰레드 5, 6번만 사용하는 식으로 말이죠.

따라서 같은 큐에 보내면 같은 스레드에 배치될 수 있는데, 해당 스레드가 sync 로 인해 멈춰있는 상황이라면 데드락 상황이 발생합니다. 물론 다른 스레드에 배치되면 교착 상태가 되지 않겠지만요.

데드락 발생 상황 O
데드락 발생 상황 X

이렇게 현재와 같은 큐에 sync로 작업을 보내면 데드락 상황이 발생할 수도 있기 때문에 같은 큐에 sync 로 작업을 보내지 말아야 합니다.

참고로 글로벌 큐는 Qos에 따라 각각 다른 큐 객체를 생성합니다. 즉 DispatchQueue.global(qos: .utility)DispatchQueue.global() 다른 큐입니다. 따라서 각각 다른 Qos 큐라면 쓰레드가 겹칠일이 없기 때문에 데드락 발생 가능성이 없습니다.

//데드락 발생 가능성 있음
DispatchQueue.global().async {
DispatchQueue.global().sync
}
//데드락 발생 가능성 없음
DispatchQueue.global(qos: .utility).async {
DispatchQueue.global().sync
}

2-3. 메인 스레드에서 DispatchQueue.main.sync 를 사용하면 안된다

코드 작성할때 별도의 처리를 안했다면 우리는 메인 스레드에서 작업을 하는 거였죠? 여기서 만약 DispatchQueue.main.sync {} 로 태스크를 보낸다?

삐빅 에러입니다.

메인 스레드에서 DispatchQueue.main.sync {} 를 시행한다는 의미는 무엇인지 하나씩 뜯어보죠.

  • DispatchQueue.main: main 큐로 태스크로 보낸다. 이때 main 큐는 직렬 큐이기 때문에 task 를 동일한 스레드 (메인 스레드)에만 할당한다.
  • .sync : 해당 task 가 끝날때까지 메인 스레드는 일단 기다린다

즉, 아래 같은 플로우로 상황이 진행됩니다.

  1. 메인 스레드에서 “끝날때까지 기다리고 있을게~” 하고 task 를 메인 큐에 보냄
  2. 메인 큐의 task는 메인 스레드로 할당 (직렬 큐)
  3. 근데 메인 스레드는 기다리고 있는 상태
  4. 결국 아무것도 진행되지 못하는 데드락 상황 발생

3. 객체에 대한 캡처를 주의한다

동작해야 할 task 를 queue에 보낸다는것은 결국 클로저 보내는 것입니다.

따라서 객체에 대한 캡처 현상 발생할 수 있게 되고, 자칫하면 retain cycle이 생길수도 있습니다. 따라서 이러한 부분들을 유의해야하는데 저는 잘 몰라서 그냥 이렇게 weak self 로 쓰고 보는 편이에요 ㅎ

언제 [weak self]를 써야하는지 더 궁금하신 분들은 첨부한 Should we use [weak self] in GCD 링크를 확인하시길!

4. 비동기 작업에서 컴플리션 핸들러의 존재 이유

이건 주의 사항이라기 보다는 completion handler가 왜 필요한지에 대해 살펴보자는 건데요!

우리는 이제 .async 를 통해 비동기로 작업을 보낼 수 있는걸 알고 있어요.

async 로 분홍색 작업을 보냈기 때문에 노란색 작업 바로 시작

비동기로 보낸다는 뜻은 해당 작업이 끝날때까지 기다리지 않고 남아있는 다른 작업을 실행하겠다는 의미였는데요, 다른 작업을 하다가도 보낸 작업이 끝났다면, 시점을 파악해서 마저 필요한 작업을 해줘야겠죠?

그렇다면 보낸 작업이 끝났을 때를 어떻게 아냐! 하면 바로 컴플리션 핸들러를 통해서 알 수 있습니다. 즉, completion handler는 어떤 작업이 끝났음을 알리는 클로저비동기 함수에는 항상 컴플리션 핸들러가 있다고 생각하면 됩니다.

사실 이건 차근차근 시작하는 GCD — 3 에서도 살짝 언급된 내용으로,

“일을 queue에 보내기만 하고 신경을 안쓰고 있는데.. 그럼 실제 해당 작업이 언제 끝나는지는 어떻게 알지?”

Swift에서는 클로저를 통해 해당 시점을 알려줍니다. 이를 completionHandler 또는 completion 이라고 하는데 (혹은 explicit callback)이건 나중에 더 자세히 알아보기로 해요~

이렇게 나중에 더 자세히 알아보자고 했는데 그때가 지금입니다 ㅎㅎ 떡밥회수!

마무리

내용의 기반이 되는 강의에서는 “동기적 함수를 비동기적 함수처럼 만드는 방법” 에 대한 설명도 있었는데, 자체 판단 하에 저는 이 글에서는 담지 않습니다 ㅎㅎ 궁금하신 분들은 수강을 추천드려요!

아휴 연휴에 모두의 마블이나 하구.. 아침에 자서 밤에 일어나고 하니까 증맬 명절같고 좋네여ㅎㅎ 지났지만 모두 메리크리스마스 & 미리 해피 뉴 이어~!

이전 포스팅 👈🏻

다음 포스팅👉🏻

출처

 https://github.com/sujinnaljin/TIL