[Swift] Grand Central Dispatch
Grand Central Dispatch
본 포스팅에 대한 내용은 이전 Concurrency Programing 포스팅 내용의 연장된 심화 내용 입니다.
Dispatch Group
다양한 스레드에서 비동기적으로 작업을 그룹 짓고, 각 그룹별로 끝나는 시점을 알고싶을 때 주로 사용
let group1 = DispatchGroup( )
DispatchQueue.global(qos: ).async(group: group1) {
// code
}
DispatchQueue.global(qos: ).async(group: group1) {
// code
}
DispatchQueue.global( ).async(group: group1) {
// code
}
// 메인 스레드에 알리고 작업해야함
group1.notify(queue: DispatchQueue.main) { [weak self] in
self?.textLabel.text = "모든 작업이 완료되었습니다."
}
// 언제까지 기다릴 수 없을 때 사용하는 메서드
if group1.wait(timeout: .now( ) + 60 ) == .timedOut {
print("작업이 60초안에 종료하지 않았습니다.")
}
- 동기적인 기다림
- 어떤 이유로 그룹의 완료 알림에 비동기적으로 응답 할 수 없는 경우, 대신 디스패치 그룹에서
wait
메서드를 사용할 수 있음- 실제앱에서 메인큐(메인쓰레드)에서는
wait
메서드 사용하면 안됨 - 메인큐(메인쓰레드)가 아닌 이미 다른큐(다른쓰레드)에서는
wait
메서드 사용가능할 수 있음
- 실제앱에서 메인큐(메인쓰레드)에서는
- 모든 작업이 완료 될 때까지 현재 대기열을 차단하는 동기적 방법
- 작업이 완료 될 때까지, 얼마나 오래 기다릴지 기다리는 시간을 지정하는 선택적(optional) 파라미터가 필요 (지정하지 않으면 무제한으로 대기)
- 어떤 이유로 그룹의 완료 알림에 비동기적으로 응답 할 수 없는 경우, 대신 디스패치 그룹에서
Async DispatchQueue Group
DispatchQueue.async(group: group1) {
group1.enter( ) // 함수 시작
someAsyncMethod {
// Async code
group1.leave( ) // 함수 끝
}
}
- 디스패치 그룹 클로저 내에서 비동기함수를 호출할때 실제 태스크가 끝나지 않았는데 비동기적으로 처리하는 과정에서 클로저가 종료되면서 태스크도 종료되어 버릴 수 있음
- 함수의 시작과 끝을 알려주는
.enter()
,.leave()
의 짝이 맞으면 디스패치 그룹이 비로소 종료됨.enter()
,.leave()
는 Reference Counting을 사용
Dispatch Work Item
- 작업을 미리 정의해 놓고 사용하는, 큐에 제출하기 위한 객체
- 빈약한 <취소 기능=""> `cancel()` 메서드 취소>
- 작업이 아직 시작 안된 경우(아직 큐에 있을때) 작업이 제거
- 작업이 실행중인 경우
isCancelled
속성이true
로 설정
(직접적으로 실행중인 작업이 멈추는 것은 아님)
- 빈약한 <순서 기능=""> `notify(queue: 실행할큐, execute: 디스패치아이템)` 메서드 순서>
- 직접적으로 실행 다음에, 실행할 아이템(작업)을 지정
Dispatch Semaphore
- 공유 리소스에 접근가능한 작업 수를 제한해야할 경우
( ex. 다운로드 숫자를 제한해야 할때)
// 현재 쓸 수 있는 자원의 수 3개로 제한
let semaphore = DispatchSemaphore(value: 3)
DispatchQueue.async(group: group1) {
group1.enter( ) // 함수 시작
semaphore.wait( ) // 가용 자원 수 -1
someAsyncMethod {
group1.leave( ) // 함수 끝
semaphore.signal( ) // 가용 자원의 수 +1
}
}
Problems of Concurrency 동시성과 관련된 문제
- 2개이상의 쓰레드를 사용하면서, 동일한 메모리 접근 등으로 인해 발생할 수 있는 문제
- 실행시마다 항상 같은 순서로 발생하는 것이 아니기 때문에, 디버그 하기 어려움
Race Condition 경쟁 상황
var a = 1
DispatchQueue.global( ).async { // a에 접근
sleep(1)
a += 1
}
DispatchQueue.global( ).async { // a에 접근
sleep(1)
a += 1
}
// 비동기적으로 a에 접근했을 때 a는 제대로 프린트 되지 않음
print(a)
- 두개 이상의 쓰레드가 한 곳의 메모리(저장공간)에 동시에 접근하여 값을 사용하려고 할때 문제가 발생할 수 있음
- 읽는 것은 동시에 해도 괜찮을 수 있으나, 읽기/쓰기가 동시에 이루어 진다던지 쓰기 작업 여러개가 동시에 이루어지는 것은 Thread-safe 하지 않음
lazy var
를 통해서 변수를 생성할 때도 지연 저장 속성의 특성 상 경쟁 상황이 발생할 수 있음
TSan
- Thread-Sanitizer를 통해 잠재적 경쟁 상황을 찾을 수 있음
Thread-Safety
- 해결 방안 : Thread-safe(쓰레드 세이프)
- 여러 쓰레드가 동시에 쓰여도 안전함
- 동시적 처리를 하면서(여러 스레드를 사용하면서도) 문제없이 스레드를 안전하게 사용
데이터(객체나 변수 등)에 여러 쓰레드를 사용하여 접근하여도, 한번에 한개의 쓰레드만 접근가능하도록 처리하여 경쟁상황의 문제없이 사용
Dispatch Barrier
concurrentQueue.async(flags: .barrier) {
// Barrier Task
}
- concurrent큐 내의 여러개 쓰레드 중에서 배리어 작업의 경우, 한개의 쓰레드만 사용해 serial(직렬)로 실행가능한 방법
- concurrent큐임에도 불구하고, 시리얼하게 동작
- 보낸 쓰레드는 기다리지 않고 비동기적으로 동작
- 주로는 메인쓰레드가 아닌 쓰레드에서 다시 접근할때 Barrier 의미가 있음
객체 설계
- 객체에 접근할때 메인 큐(쓰레드)가 아닌 다른 큐에서 접근할 가능성이 있는 지 확인 후 객체 내부에 Thread-safe처리
- Serial Queue + sync
- Barrier 처리
Deadlocks 교착 상태
2개이상의 쓰레드가 2개이상의 배타적인 자원 사용으로 인해 서로 점유하려고 하면서 자원사용이 막혀 작업이 이러지도 저러지도 못하고 진행이 되지 않는 상태
- 다양한 발생 가능성
- 동기 작업이 현재의 쓰레드가 필요한 경우
- 앞선 작업이 현재의 쓰레드가 필요한 경우
- 여러개의 세마포어가 존재할때, 순서 잘못 설계 등
- 보통 교착 상태는 시리얼 큐로 해결 가능
- 세마포어나 제한된 리소스 순서 같은 것들을 조심히 사용해야하고, 객체 등 설계 시에 각별히 주의해야 함
Priority Inversion 우선 순위 역전
낮은 우선 순위의 작업이 자원을 배타적으로 사용하고 있을 때, 다른 작업이 자원을 사용하지 못하게 막고 있으므로 작업의 우선 순위가 바뀔 수 있음
- 다양한 발생 가능성
- 시리얼큐에서 높은 우선순위 작업이 낮은 우선순위의 뒤에 보내지는 경우
- 낮은 우선순위의 작업이 높은 우선순위가 필요한 자원을 잠그고 있는 경우
(ex. lock코드, 세마포어 등) - 높은 우선순위 작업이 낮은작업에 의존하는(디펜던시) 경우 (Operation)
- 1차적으로 GCD가 우선순위를 조정해서 알아서 해결
(실직적으로는 낮은 우선순위 작업의 우선순위를 높여서 우선처리)- (안전하게) 공유된 자원 접근시 - 동일한 QoS 사용
Async / Sync / Serial / Concurrent
Async vs Sync
- 작업을 보내는 시점에서 기다릴지 말지에 대해 다루는 것
concurrent vs serial
- Queue(대기열)로 보내진 작업들을 여러개의 스레드로 보낼 것인지 한개의 스레드로 보낼 것인지에 대해 다루는 것
Serial + Sync
DispatchQueue.main( ).sync {
// main 큐에서 동기 방식으로 실행
}
메인 스레드의 작업 흐름이 queue에 넘긴 태스크가 끝날때까지 멈춰있고(sync)
넘겨진 태스크는 queue에 먼저 담겨있던 작업들과 같은 스레드에 보내지기 때문에
해당 작업들이 모두 끝나야 실행 (Serial Queue)
serial + Async
DispatchQueue.main( ).async {
// main 큐에서 비동기 방식으로 실행
}
메인 스레드의 작업 흐름이 태스크를 queue에 넘기자마자 반환되고 (async)
넘겨진 task는 queue에 먼저 담겨있던 작업들과 같은 스레드에 보내지기 때문에
해당 작업들이 모두 끝나야 실행 (Serial Queue)
Concurrent + Sync
DispatchQueue.global( ).sync {
// background 큐에서 동기 방식으로 실행
}
메인 스레드의 작업 흐름이 queue에 넘긴 태스크가 끝날때까지 멈춰있고(sync)
넘겨진 task는 queue에 먼저 담겨있던 작업들과 다른 스레드에 보내질 수 있기 때문에
해당 작업들이 모두 끝나지 않아도 실행 (Concurrent Queue)
Concurrent + Async
DispatchQueue.global( ).async {
// background 큐에서 비동기 방식으로 실행
}
메인 스레드의 작업 흐름이 태스크를 queue에 넘기자마자 반환되고 (async)
넘겨진 task는 queue에 먼저 담겨있던 작업들과 다른 스레드에 보내질 수 있기 때문에
해당 작업들이 모두 끝나지 않아도 실행 (Concurrent Queue)
주의 사항
main 큐에서는 sync 메서드 사용 금지
- 메인 쓰레드에서
sync
메서드를 호출하게되면 끊임없이 앱의 이벤트 처리가 대기상태로 멈추게 되면서 교착 상태에 빠지고 모든 앱의 이벤트 처리가 멈추게 됨 - 꼭 사용해야 할 상황이 생긴다면
async
큐로 보낸 뒤 그 안에서sync
큐로 보내는 방법을 활용해야 함- 그러나,
sync
메서드는 경쟁 상황을 피하는데는 매우 유용 - 큐가 Serial 큐이고 객체에 접근하는 유일한 방법이라면,
sync
메서드는 메인 쓰레드가 아닌 모든 쓰레드가 일관된 값을 얻는 것을 보장하면서 실행
- 그러나,
같은 global 큐 에서 async 큐 안에 sync 작업 호출 금지
async
큐 안에sync
큐를 사용할때 같은 global 큐에sync
작업을 보낸다면 교착 상태가 발생- QoS 설정을 통해 다른 큐를 사용하는 것이 좋음
참고
Ellen iOS Concurrency Class
Dumb-veloper tistory Blog
김종권의 iOS tistory Blog
sujinnaljin medium Blog