본문 바로가기

개발/iOS

[iOS] GCD - Main Queue, Global Queue와 동기(sync), 비동기(async) 작업 비교

반응형

 

 

GCD 개념 돌아보기 

더보기

1. GCD란 무엇인가?

GCD (Grand Central Dispatch)는 iOS에서 멀티스레딩을 간편하게 관리하기 위해 제공되는 기술. 
이를 통해 여러 스레드를 쉽게 관리하고, 백그라운드 작업과 UI 업데이트 작업을 구분할 수 있음.

메인 큐 (Main Queue) UI 작업을 실행하는 스레드, 메인 스레드에서 실행.
글로벌 큐 (Global Queue) 메인 스레드를 제외한 백그라운드에서 실행되는 큐로 여러 스레드를 활용해 비동기 작업을 처리함.
큐 (Queue) 작업을 실행할 순서를 결정하는 객체로, 큐에 담긴 작업은 하나씩 실행.

 

2.  DispatchQueue.global() 과  DispatchQueue.main 

.global() 전역 큐로, 백그라운드 스레드에서 작업을 실행. 
하나 이상의 스레드로, 비동기로 여러 스레드에서 동시에 작업을 처리할 수 있음.
.main 메인 큐, 즉 메인 스레드에서 실행되며 UI 관련 작업을 처리함. 
iOS에서 
UI 업데이트 작업은 반드시 메인 스레드에서만 실행함.

 

3. 동기(sync)와 비동기(async)

async 작업을 비동기적으로 실행함. 작업이 완료될 때까지 기다리지 않고, 다른 작업을 병렬로 처리할 수 있음.
sync 작업을 동기적으로 실행함. 현재 스레드는 작업이 완료될 때까지 대기함.

 

4. DispatchQueue.global().sync에서 발생할 수 있는 문제점

DispatchQueue.global().sync는 동기적으로 작업을 실행함. 
이때 메인 스레드에서 global().sync를 호출하면, 메인 스레드는 해당 작업이 끝날 때까지 대기하게 됨. 
* 메인 스레드는 한 번에 하나의 작업만 처리되기 때문에, sync 작업이 끝날 때까지 다른 작업이 대기하게 되는 구조임.

문제점 설명
UI가 멈춤 메인 큐는 UI 작업을 담당하고 있으며 UI 관련 작업은 메인 스레드에서만 실행되어야함. 
global().sync를 메인 스레드에서 호출하고, 그 안에서 main.sync를 호출하려고 하면, 메인 큐에서 작업을 
실행하려고 하는 시점에
 메인 스레드가 다른 작업을 기다리고 있기 때문에 UI가 멈추게 됨.
데드락(Deadlock) global().sync와 **main.sync**를 동시에 사용할 때, 데드락이 발생할 수 있음. 
데드락은 두 작업이 서로가 끝날 때까지 기다리며, 결과적으로 작업이 진행되지 않는 상태를 말함.


global().sync는 백그라운드 스레드에서 실행되지만, 그 안에서 main.sync를 호출하면 메인 스레드는 다른 작업을 기다리는 상태가 됨. 동시에 global().sync도 메인 스레드가 작업을 마칠 때까지 기다리기 때문에 서로 기다리게 되어 데드락이 발생함.

 

 

데드락 발생 예시

그럼 언제 데드락이 발생하는지 하나하나 뜯어보자.
* 참고 : 메인 스레드에서 Main.sync가 호출되는 경우 무조건 데드락이 발생함.

경우 상위 호출 내부 호출 데드락 발생
Case 1 Main.async Main.sync O
Case 2 Main.sync Main.async O
Case 3 Global().async Main.sync X
Case 4 Global().sync Main.async X
Case 5 Global().async Global().sync O / X
Case 6 Global().sync Global().async O / X
Case 7 Main.async Global().sync X
Case 8 Main.sync Global().async O

 

Case 1 : 데드락 발생 

// 메인 큐 + 비동기 작업
DispatchQueue.main.async { // Act 1
	...
    // 메인 큐 + 동기 작업
    DispatchQueue.main.sync { // Act 2
    ...
    }
}

순서 흐름

  1. 메인 스레드는 비동기 작업인 DispatchQueue.main.async(Act 1)를 호출함. Act 1는 메인 큐에 배치됨.
  2. 메인 스레드에 의해 Act 1이 실행되며 DispatchQueue.main.sync(Act 2)를 호출함. 
    sync는 동기적이기 때문에 메인 스레드는 Act2가 완료될 때까지 대기함.
  3. Act 2는 메인 큐에서 실행되기 위해 기다림.
    하지만 메인큐는 Act 1이 실행 중이므로, Act 2는 Act 1이 완료되기를 기다림.
  4. 데드락 발생.
    Act 1이 sync를 호출했기 때문에 메인 스레드는 Act 1을 끝내지 못하고 기다리고, Act 2는 메인 스레드가 완료되기를 기다림.
    상호 대기 상태에 빠져 데드락이 발생함.

 

 

Case 2 : 데드락 발생

DispatchQueue.main.sync { // Act 3
	...
    DispatchQueue.main.async { // Act 4
    ...
    }
}

순서 흐름

  1. 메인 스레드는 동기 작업인 DispatchQueue.main.sync(Act 3)를 호출함.
    Act 3은 동기적이기 때문에 메인 스레드는 Act 3이 끝날 때까지 기다림.
    즉, Act 3을 메인 큐에서 실행하려고 대기 상태로 진입함.
  2. 메인 큐에서 Act 3이 동기적으로 실행되어 그 안의 DispatchQueue.main.async(Act 4)를 호출함.
    Act 4는 비동기적으로 실행되지만, 메인 큐는 sync 작업을 기다리고 있기 때문에 Act 4는 Act 3이 완료되기를 기다림.
  3. Act 4가 실행되지 않으면서 Act 3의 작업이 끝나지 않아 두 작업은 서로 대기 상태에 빠짐.
  4. 데드락 발생.
    메인 스레드는 Act 3을 끝내지 못해 기다리고, Act 4은 Act 3이 끝날 때까지 실행되지 않기 때문에 계속해서 서로를 기다리는 상태가 되어 데드락이 발생함.

 

 

Case 3 : 원활

DispatchQueue.global().async { // Act 5
	...
    DispatchQueue.main.sync { // Act 6
    ...
    }
}

순서 흐름

  1. 백그라운드 스레드가 비동기 작업인 DispatchQueue.global().async(Act 5)를 호출함. 
  2. 백그라운드 스레드에서 DispatchQueue.main.sync(Act 6)를 호출함.
    sync는 메인 큐에서 동기적으로 실행되는 작업이기에 메인 스레드에서 실행되어야 함.
    Act 6는 메인 스레드에서 동기적으로 실행되고, 메인 스레드에서 실행을 완료할 때까지 백그라운드 스레드는 대기하게 됨.
  3. 메인 큐에서 동기 작업 진행.
    메인 스레드가 다른 작업을 하고 있을 수 있기 때문에, Act 6는 작업을 대기할 수 있으나 메인 스레드가 작업을 처리할 수 있으므로 백그라운드 스레드는 Act 6가 끝날 때까지 대기함.
  4. 데드락 발생하지 않음.
    메인 스레드는 Act 6 작업을 처리할 준비가 되어 Act 5의 대기는 데드락을 유발하지 않음.
    Act 6 작업이 완료되면 Act 5 작업도 종료되어 메인 스레드와 백그라운드 스레드는 다른 작업을 이어서 처리함.

 

 

Case 4 : 원활

DispatchQueue.global().sync { // Act 7
	...
    DispatchQueue.main.async { // Act 8
    ...
    }
}

순서 흐름

  1. 백그라운드 스레드가 동기 작업인 DispatchQueue.global().sync(Act 7)를 호출함. 
  2. 백그라운드 스레드에서 DispatchQueue.main.async(Act 8)를 호출함.
    Act 8은 메인 큐에서 비동기적으로 작업을 실행함.
    메인 큐에 작업을 추가하지만, 즉시 실행되지 않고 큐에 쌓임.
  3. 백그라운드 스레드는 Act 8 작업을 기다리지 않음.
  4. 데드락 발생하지 않음.
    메인 큐가 작업을 진행할 때까지 기다리지 않기 때문에 데드락이 발생하지 않음.

 

 

Case 5 : 스레드 배정에 따라 달라짐

DispatchQueue.global().async { // Act 9
	...
    DispatchQueue.global().sync { // Act 10
    ...
    }
}

순서 흐름

  1. 백그라운드 스레드가 비동기 작업인 DispatchQueue.global().async(Act 9)를 호출함. 
  2. 백그라운드 스레드에서 DispatchQueue.global().sync(Act 10)를 호출함.
    sync는 동기적으로 실행되기에 백그라운드 스레드는 이 작업이 완료될 때까지 기다림.
  3. 스레드 배정
    * 참고 : globa()은 하나 이상의 스레드로 구성되기 때문에 같은 스레드이거나 다른 스레드로 배정될 수 있음.
    • 같은 스레드일 경우
      • Act 10의 작업이 끝날 때까지 Act 9가 대기함.
      • 하지만 동기적으로 실행되는 Act 10은 현재 스레드에서 실행되며, 그 스레드는 작업이 완료될 때까지 기다려야 함.
      • 즉, 스레드의 작업이 완료되려면 Act 10은 작업이 끝나야 하는데 Act 10의 작업이 끝나지 않아 현재 스레드는 계속 대기 상태에 머물게 됨.
    • 다른 스레드일 경우
      • Act 10의 작업은 다른 스레드에서 실행됨.
      • 동기적으로 실행되는 Act 10은 배정된 스레드의 작업이 끝날 때까지 기다리지만, Act 9와 다른 스레드에서 실행되므로 교착 상태에 빠지지 않음.
      • Act 10의 작업이 종료되면 Act 9가 실행되고 종료됨.
  4. 데드락 발생 여부
    • 같은 스레드일 경우
      • 자기 자신을 기다리는 대기 상태에 놓인 스레드는 데드락이 발생함.
    • 다른 스레드일 경우
      • 데드락이 발생하지 않음.

 

 

Case 6 : 스레드 배정에 따라 달라짐

DispatchQueue.global().sync { // Act 11
	...
    DispatchQueue.global().async { // Act 12
    ...
    }
}

순서 흐름

  1. 백그라운드 스레드가 동기 작업인 DispatchQueue.global().sync(Act 11)를 호출함. 
  2. 백그라운드 스레드에서 DispatchQueue.global().async(Act 12)를 호출함.
    async는 비동기로 실행되기에 스레드는 기다리지 않고 다른 작업을 진행함.
  3. 스레드 배정
    • 같은 스레드일 경우
      • 동기적으로 실행되는 Act 11이 현재 스레드의 작업이 끝나기를 기다림. 따라 Act 12는 Act 11 작업이 완료되기를 기다림.
      • 하지만 Act 11은 Act 12 작업이 끝나기를 기다리기 때문에, 서로가 서로를 기다리는 교착 상태에 빠지게 됨.
    • 다른 스레드일 경우
      • 동기적으로 실행되는 Act 11은 현재 스레드의 작업이 끝나기를 기다림.
      • 비동기로 실행되는 Act 12은 다른 스레드에서 실행되므로 Act 11를 기다리지 않음.
      • 여기서 Act 11은 동기적으로 실행되기 때문에, 비동기 방식인 Act 12가 끝날 때까지 기다리지 않음.
      • Act 12는 별도의 스레드에서 실행되기 때문에 Act 11의 완료 여부에 영향받지 않고, Act 11은 작업이 완료되어 종료됨.
  4. 데드락 발생 여부
    • 같은 스레드일 경우
      • 서로가 서로를 기다리는 교착 상태에 빠져 데드락이 발생하게 됨.
    • 다른 스레드일 경우
      • 데드락이 발생하지 않음.

 

 

Case 7 : 원활

DispatchQueue.main.async { // Act 13
	...
    DispatchQueue.global().sync { // Act 14
    ...
    }
}

순서 흐름

  1. 메인 스레드가 동기 작업인 DispatchQueue.main.async(Act 13)를 호출함.
  2. 백그라운드 스레드에서 DispatchQueue.global().sync(Act 14)를 호출함.
    sync는 동기적으로 실행되기에 Act 13은 Act 14가 끝날 때까지 대기해야 함.
    하지만 Act 13은 메인 스레드, Act 14는 백그라운드 스레드로 서로 다른 스레드이며 Act 13은 비동기적으로 실행되기에 교착 상태가 일어나지 않음.
  3. 백그라운드 스레드에서 Act 14가 처리되는 동안 메인 큐는 다른 작업을 처리함. Act 14가 완료되며 Act 13도 끝남.
  4. 데드락 발생하지 않음.

 

 

Case 8 : 데드락 발생 

DispatchQueue.main.sync { // Act 15
	...
    DispatchQueue.global().async { // Act 16
    ...
    }
}

순서 흐름

  1. 메인 스레드에서 시작함. 메인 스레드가 동기 작업인 DispatchQueue.main.sync(Act 15)를 호출함.
  2. Act 15는 메인 큐에서 동기적으로 실행하려고 함.
    Act 15는 이미 메인 스레드에서 실행중이기에 메인 스레드가 완료될 때까지 기다림.
    Act 15와 메인 스레드가가 서로를 기다리는 상황이 발생함.
  3. Act 16은 비동기적으로 실행되지만, 메인 스레드의 Act 15가 데드락 상태이기 때문에 Act 16까지 진행되지 않음.
  4. 데드락 발생.
    만약 Act 15가 메인 스레드에서 시작하는게 아닌, 백그라운드 스레드에서 시작하면 위 문제를 회피할 수 있음.
    메인 스레드에서 DispatchQueue.main.sync를 호출하는건 무조건 데드락이 걸리므로 주의해야함.

 

 

그럼 main.sync를 사용해야할 때는 언제일까?

  1. 백그라운드 스레드에서 UI 값을 즉시 필요로 할 때
    • 백그라운드 스레드에서 UI 컴포넌트의 값이나 상태를 즉시 알아야 하는 경우
    • 백그라운드 작업에서 UI 컴포넌트의 크기, 위치 또는 다른 속성을 기반으로 계산을 수행해야 할 때
  2. 순차적 실행이 중요한 경우:
    • 메인 스레드에서 작업이 완료된 후에만 백그라운드 작업을 계속해야 할 때
    • 데이터 일관성이 중요한 특정 작업 시퀀스에서
  3. 테스트 코드:
    • 단위 테스트나 UI 테스트에서 비동기 작업의 완료를 기다려야 할 때
    • 테스트 환경에서는 종종 동기식 작업이 테스트를 단순화하고 더 예측 가능하게 만듦
  4. 레거시 API와의 호환성:
    • 동기식 응답을 기대하는 오래된 API나 라이브러리와 통합할 때

 

그러나 main.async를 사용하는게 훨씬 안전하므로 사용할 때 주의해서 사용하자.


또, 비동기 코드를 깔끔하게 쓸 수 있는 async/await나 Combine 이 있으니 sync를 직접 작성하는 일은 많지 않다.
* async/await는 항상 비동기적으로 작동하는게 아닌 필요할 때 비동기로 작동하는 도구이니 자주 사용하자.

 

반응형

'개발 > iOS' 카테고리의 다른 글

[iOS] GoogleMap 마커, 경로 사용하기  (0) 2023.03.14
[iOS] YoutubePlayer 사용하기  (0) 2022.07.15
[iOS] 기기 별 지원 정보 (OS, Face ID, Touch ID)  (0) 2021.10.18
[iOS] iOS의 4가지 층  (0) 2021.03.24