태그: async, concurrent, DispatchGroup, DispatchQueue, DispatchSemaphore, GCD, global, main, OperationQueue, serial, sync
- This topic has 2개 답변, 3명 참여, and was last updated 4 years, 3 months 전에 by GangWoon.
-
글쓴이글
-
-
TTOzzi참가자
- 글작성 : 10
- 답글작성 : 13
iOS 커뮤니티들의 기존 질문, 답변에 제가 찾은 정보와 찾으면서 참고했던 비슷한 질문들을 같이 정리해보았습니다.
잘못된 정보 수정이나 추가하고 싶은 답변이 있으시다면 댓글을 달아주시거나 Question-Archive 에 issue 나 PR 로 알려주시면 수정하겠습니다!Q.
DispatchQueue.global() 과 DispatchQueue.init() 의 차이가 무엇인가요?
백그라운드 스레드를 관리하는 큐를 생성하고 싶은데요. DispatchQueue.global() 으로 생성하는 방법과 DispatchQueue.init() 으로 label 을 설정하여 생성하는 방법에는 어떤 차이가 있나요?
A.
- DispatchQueue.global() 은 인자로 받은 우선순위에 해당하는 전역 시스템 큐를 반환합니다. 이 전역 시스템 큐는 여러 작업을 동시에 수행하는 concurrent queue 입니다.
-
DispatchQueue 의 생성자로 만든 큐는 인자로 받은 여러 설정을 적용한 큐를 생성합니다.
let myQueue = DispatchQueue(label: "myQueue")
위와 같이 label 만으로 만들게 되면, unspecified 의 우선순위를 가진 serial queue 를 생성합니다.
let myQueue = DispatchQueue(label: "myQueue", qos: .userInitiated, attributes: .concurrent)
우선순위와 serial, concurrent 구분을 직접 설정해줄 수도 있습니다.
-
지금까지의 예시들을 봤을 때 두 방식에 별다른 차이가 없는데요. 그럼 왜 global() 을 사용하지 않고 생성자를 사용할까요?
첫 번째는 디버깅에서의 이점이 있습니다.
func debugQueue(_ queue: DispatchQueue) { for _ in 1...5 { queue.async { print("break point") } } for _ in 1...5 { queue.async { print("break point") } } }
디버깅 상황을 만들기 위해 큐를 인자로 받아 그 큐에서 간단한 동작을 하는 함수를 만들었습니다. 위에서 만든 myQueue 를 인자로 넘겨주고 print 가 실행되는 부분에 브레이크포인트를 설정해보겠습니다.
Xcode 의 Debug navigator 를 보면 브레이크포인트가 설정된 코드가 실행된 큐와 스레드, 그리고 해당 큐가 관리 중인 스레드들도 확인할 수 있습니다. 지금은 큐가 하나뿐이지만 사용하는 큐가 더 많아지고, 해야 하는 작업량도 많아진다면 label 을 통해 큐를 구분할 수 있으니 디버깅하기 편해지겠죠?
반면에 DispatchQueue.global() 을 넘겨주면 시스템에 구현되어있는 전역 큐를 사용하게 되어 디버깅이 어려워지게 됩니다.
두 번째는 barrier 와 같은 DispatchQueue 의 몇 가지 세부적인 설정을 사용하기 위함입니다. barrier 란 단어 뜻 그대로 동시성을 지원하는 비동기 큐에서 장벽 같은 역할을 해주는 기능인데요.
위 그림처럼 비동기 작업들이 실행되다가 flag 가 barrier 로 설정된 작업이 실행되면 그 작업이 끝날 때 까지 serial queue 처럼 동작하게 됩니다. 간단한 예시코드를 보겠습니다.
let myQueue = DispatchQueue(label: "myQueue", attributes: .concurrent) for i in 1...5 { myQueue.async { print("\(i)") } } myQueue.async(flags: .barrier) { print("barrier!!") sleep(5) } for i in 6...10 { myQueue.async { print("\(i)") } }
숫자들을 비동기로 출력하는 코드 사이에 flags 를 barrier 로 설정한 코드를 추가해주었습니다.
숫자들이 출력되다가 barrier 블록이 실행되자 다음 작업들이 barrier 블록이 끝날 때까지 기다리는 것을 확인할 수 있습니다. 만약 barrier 가 없다면 concurrent queue 에서 비동기로 숫자들을 출력하기 때문에 한번에 모든 숫자가 출력될 것입니다. barrier 공식문서를 보면 barrier 를 지정하는 큐는 직접 만든 concurrent queue 여야만 한다고 합니다. barrier 를 포함한 몇몇 세부적인 설정들은 글로벌 큐에서는 사용할 수 없습니다.
참고할 만한 비슷한 질문, 자료
Q.
DispatchQueue 에서 main.sync 는 언제 사용하나요?
DispatchQueue 에 대해 공부하면서 생긴 의문인데, 많은 자료에서 DispatchQueue.main 에서 sync 를 호출하지 말라고 합니다. 왜 호출하지 말라고 하는 것이며, 그럼 왜 GCD 에서 DispatchQueue.main.sync 가 구현되어있는 건가요?
A.
- 먼저 DispatchQueue.main 에서 sync 를 호출하면 안 되는 이유를 이해하려면 Deadlock(교착 상태) 이란 개념을 알아야 하는데요. Deadlock 이란
두 개 이상의 작업이 서로 상대방의 작업이 끝나기만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태
라고 합니다. Deadlock 은 DispatchQueue.main 뿐만 아니라 백그라운드 스레드를 관리하는 큐에서도 발생할 수 있습니다. -
간단한 코드로 예를 들어보겠습니다. DispatchQueue 의 생성자로 myQueue 를 생성해주었습니다. 생성자의 attributes 를 concurrent 로 설정해주지 않으면 기본값으로 serial queue 를 생성하게 됩니다.
let myQueue = DispatchQueue(label: "label") myQueue.async { myQueue.sync { // 외부 블록이 완료되기 전에 내부 블록은 시작되지 않습니다. // 외부 블록은 내부 블록이 완료되기를 기다립니다. // => deadlock } // 이 부분은 영원히 실행되지 않습니다. }
myQueue 는 serial queue 이므로 한 번에 하나의 작업을 합니다. serial queue 에 추가되는 작업은 DispatchQueue 에서 관리하는 고유한 스레드에서 실행됩니다. 따라서 위 예시에서 sync 내부 블록은 외부 블록이 완료될 때까지 기다리고, sync 블록 이후의 동작은 sync 내부 블록이 완료될 때까지 기다리는 deadlock 이 발생하게 됩니다.
이 문제는 myQueue 를 생성할 때
let myQueue = DispatchQueue(label: "label", attributes: .concurrent)
와 같이 concurrent 속성을 설정하여 한번에 여러 작업을 실행할 수 있도록 해주어 해결할 수 있습니다.
-
다시 본론으로 돌아오면, DispatchQueue.main 은 프로그램의 메인 스레드에서 작업을 실행하는 전역적으로 사용이 가능한 serial queue 입니다. 앱의 run loop 와 함께 작동하며, 대기 중인 동작을 run loop 의 다른 이벤트 처리와 조율해줍니다. 이러한 DispatchQueue.main 에서 sync 를 호출하게 된다면 끊임없이 앱의 이벤트 처리를 하고 있던 메인 스레드가 sync 호출에 의해 멈추게 되고 위에서 예시로 들었던 코드와 같이 deadlock 이 발생하게 됩니다.
-
그럼 DispatchQueue.main.sync 는 어떤 경우에 사용할까요? 백그라운드 스레드에서 이루어지는 작업들 사이에 순서에 맞게 메인 스레드에서 어떠한 작업이 이루어져야 할 때 사용합니다. 다음 코드를 보겠습니다.
DispatchQueue.global().async { // UI 업데이트 전 실행되어야만 하는 코드 DispatchQueue.main.sync { // UI 업데이트 } // UI 업데이트 후에 실행되어야만 하는 코드 }
백그라운드 스레드에서 작업이 진행되는 중 UI 업데이트가 이루어져야 하는 상황입니다. 좀 더 구체적으로 예를 들자면 테이블 뷰나 콜렉션 뷰에 셀을 추가하거나 제거할 때, 셀의 추가, 제거 작업이 이루어지던 중 네트워크 통신과 같은 다른 비동기 작업으로 인해 DataSource 의 데이터가 변경된다면 DataSource 의 데이터와 뷰의 데이터가 일치하지 않아 에러가 발생하게 됩니다. 이를 방지하기 위해 사용합니다. 애플의 PHPhotoLibraryChangeObserver 문서
Handling photo library changes in a collection view
의 예시코드에서도 사진앨범의 데이터와 업데이트한 콜렉션 뷰의 데이터가 달라지는 상황을 방지하기위해 DispatchQueue.main.sync 를 호출하는 것을 확인할 수 있습니다.
참고할 만한 비슷한 질문, 자료
- How do I create a deadlock in Grand Central Dispatch?
- why concurrentQueue.sync DON’T cause deadlock
- Concurrency Programming Guide
- Difference between DispatchQueue.main.async and DispatchQueue.main.sync
- How to dispatch on main queue synchronously without a deadlock?
Q.
특정한 여러 개의 비동기 작업이 완료된 후에 다른 작업을 실행해주고 싶어요.
반복문을 사용하여 네트워크 요청을 여러 번 보내는데, 모든 요청이 완료된 후 다른 작업을 실행하는 방법이 있나요?
A.
- DispatchGroup 을 활용해 그룹에 연결된 작업들이 완료되었을 때 핸들러를 실행해줄 수 있습니다. 5개의 이미지를 다운받은 후 UI 를 업데이트해줘야 하는 상황을 예시로 들어보겠습니다.
override func viewDidLoad() { super.viewDidLoad() for _ in 1...5 { DispatchQueue.global().async { print("이미지 다운로드 요청!") sleep(3) print("이미지 다운로드 완료!") } } print("UI 업데이트!") }
이미지 데이터를 다운받는데 3초가 걸린다고 가정하고 작성한 예시코드입니다. 코드를 실행해보면
이미지 다운로드를 비동기로 요청하기 때문에 UI 업데이트와 이미지 다운로드 간의 순서를 보장하지 않는 것을 확인할 수 있습니다.
override func viewDidLoad() { super.viewDidLoad() let downloadGroup = DispatchGroup() for _ in 1...5 { DispatchQueue.global().async(group: downloadGroup) { print("이미지 다운로드 요청!") sleep(3) print("이미지 다운로드 완료!") } } downloadGroup.notify(queue: .main) { print("UI 업데이트!") } }
downloadGroup 이라는 DispatchGroup 을 만들어 다운로드 요청 작업들을 downloadGroup 에 연결해주고 downloadGroup 의 작업이 끝나면 main 큐에서 UI 업데이트를 하도록 해주었습니다.
이미지 다운로드가 모두 완료된 후 UI 업데이트가 출력되는 것을 확인할 수 있습니다.
downloadGroup.wait()
wait() 을 활용해 wait() 을 호출한 큐에서 그룹의 작업이 완료될 때까지 동기적으로 기다리도록 할 수도 있습니다. 그러나 이 방법은 wait() 을 호출한 큐를 차단하므로 deadlock 이 발생할 수 있어 주의해야 합니다. deadlock 발생을 피하려고 wait(timeout:) 으로 최대 대기 시간을 설정해주기도 합니다.
-
비동기 작업관련 API 가 내부적으로 비동기로 구현되어 있어 async 블록에서 group 지정을 해줄 수 없는 경우 다음과 같이 enter(), leave() 를 활용하는 방법도 있습니다.
override func viewDidLoad() { super.viewDidLoad() let downloadGroup = DispatchGroup() for _ in 1...5 { guard let imageURL = URL(string: "https://example.com") else { return } downloadGroup.enter() URLSession.shared.dataTask(with: imageURL) { (data, response, error) in // 이미지 다운로드 요청 완료 후 동작 downloadGroup.leave() }.resume() } downloadGroup.notify(queue: .main) { print("UI 업데이트!") } }
참고할 만한 비슷한 질문, 자료
- Waiting until the task finishes
- Waiting until two async blocks are executed before stating another block
- Swift DispatchGroup notify before task finish
Q.
동시에 실행되는 비동기 작업의 개수를 제한할 수 있나요?
비동기적으로 데이터베이스에서 데이터를 수집하고 있습니다. 동시에 실행되는 작업의 개수를 제한할 방법이 있나요?
A.
- OperationQueue 의 maxConcurrentOperationCount 나 DispatchSemaphore 를 활용해 제한할 수 있습니다. 데이터를 수집하는데에 3초의 시간이 소모되며, 동시에 3개의 작업만 해야 한다고 가정하고 예시코드를 작성해 보겠습니다.
-
OperationQueue 를 활용한 방법
func acquire(data: Int) { print("데이터 \(data) 수집 시작!") sleep(3) print("데이터 \(data) 수집 완료!") } let acquiringQueue = OperationQueue() acquiringQueue.maxConcurrentOperationCount = 3 for i in 1...10 { acquiringQueue.addOperation { acquire(data: i) } }
데이터 수집 작업을 관리할 acquiringQueue 라는 OperationQueue 를 만들어 주었습니다. OperationQueue 의 maxConcurrentOperationCount 를 3으로 설정해 동시에 실행할 수 있는 작업의 수를 3개로 제한해주었습니다. 다음은 총 10개의 데이터를 수집하는 코드를 실행한 모습입니다.
작업을 3개씩 실행하는 것을 확인할 수 있습니다.
-
DispatchSemaphore 를 활용한 방법
let acquiringQueue = DispatchQueue(label: "acquiringQueue", attributes: .concurrent) let acquiringSemaphore = DispatchSemaphore(value: 3) DispatchQueue.global().async { for i in 1...10 { // count -= 1 acquiringSemaphore.wait() acquiringQueue.async { acquire(data: i) // count += 1 acquiringSemaphore.signal() } } }
이번에는 DispatchQueue 의 생성자로 동시성을 지원하는 사용자 지정 큐와 DispatchSemaphore 를 생성해주었습니다. 세마포어를 만들 때 사용 가능한 리소스의 수를 지정해줍니다. 이 값은 세마포어가 제한할 작업의 개수가 됩니다. 비동기 호출 직전에 wait() 을 호출하여 세마포어의 값을 1 감소시킵니다. 세마포어의 값이 음수가 되면 함수는 커널에 스레드를 차단하도록 지시합니다. wait() 을 메인 스레드에서 호출하게 되면 메인 스레드를 차단하기 때문에 주의해야합니다. 비동기 작업이 완료된 후 signal() 을 호출하여 세마포어의 값을 1 증가시키면 차단된 스레드 중 하나가 차단 해제되고 남은 작업을 수행하게 됩니다. 실행 결과는 OperationQueue 를 활용한 방법과 같습니다.
-
동시에 실행되는 작업의 개수를 제한하는 경우 외에도 일반적으로 DispatchSemaphore 는 공유 자원에 접근하는 스레드의 개수를 제한해야 할 때 유용하게 사용합니다.
참고할 만한 비슷한 질문, 자료
2020-08-16 오후 2:47 #28307
-
-
글쓴이글
- 답변은 로그인 후 가능합니다.