반응형
GCD
- GCD는 Grand Central Dispatch의 줄임말로, 멀티 코어 프로세스 시스템의 스레드 관리에 대한 책임을 운영체제 레벨에게 넘겨주는 기술입니다.
- GCD 내부에는 DispatchQueue를 읽는 멀티코어 실행엔진을 가지고 있어 이것이 등록된 작업을 읽어 스레드에 할당합니다.
- 개발자는 내부 동작에 대해 이해할 필요없이 Queue에 작업을 할당하기만 하면 되기 때문에 스레드관리가 훨씬 쉬워집니다.
- OperationQueue 역시 내부적으로는 GCD를 활용하고 있습니다.
Dispatch Queue
- Dispatch Queue는 FIFO Queue의 형태로 작업을 순서대로 전달받습니다.
- 작업은 Block( { } ) 혹은 Dispatch Work Item 인스턴스로 캡슐화하여 전달합니다.
serialQueue vs. concurrentQueue
let serialQueue = DispatchQueue(label: "SerialQueue")
let concurrentQueue = DispatchQueue(label: "ConcurrentQueue", attributes: .concurrent)
- Dispatch Queue는 실행 방법에 따라 Serial Queue와 Concurrent Queue로 구분되어집니다.
- Serial Queue는 추가된 작업을 하나씩 처리됩니다.
- 두개 이상의 작업이 동시에 처리되지 않기 때문에 Queue 기반의 동기화작업에서 많이 사용합니다.
- Dispatch Queue를 생성할 때 아무런 옵션을 부여하지 않으면 Serial Queue로 생성됩니다.
- Concurrent Queue는 추가된 작업을 동시에 처리합니다.
- 동시에 실행되는 작업의 수는 시스템의 상태에 따라 자동적으로 처리됩니다.
- Serial Queue는 추가된 작업을 하나씩 처리됩니다.
main vs. global()
DispatchQueue.main.async {
...
}
- mainQueue는 main thread에서 동작하는 특별한 Dispatch Queue 입니다.
- 따라서 UI update 관련 작업은 반드시 mainQueue에서 실행해야합니다.
- Serial Queue입니다.
- 앱 생성시점에서 자동으로 생성되기 때문에 언제든 DispatchQueue.main로 접근할 수 있습니다.
DispatchQueue.global().async {
...
}
- global() 메서드는 background thread에서 동작시킬 작업을 관리하는 Dispatch Queue를 리턴합니다.
- global(qos:)값을 부여하여 그에 해당하는 Dispatch Queue를 받아올 수도 있습니다.
Quality of Service
- GCD에도 QoS 개념이 존재합니다.
- 작업의 중요도를 4가지로 구분하여 표현합니다. 상위에 존재하는 QoS가 더 높은 중요도를 가집니다.
- userInterface
- userInitiated
- utility
- background
sync vs. async
- sync 메서드는 파라미터로 전달된 작업을 동기 방식으로 추가합니다.
- 즉, 해당 작업이 완료될 때까지 대기합니다.
- main queue에서 사용할 경우 에러가 발생하므로 주의하여 사용해야합니다.
- why? main queue는 serial queue이기 때문에 하나의 task가 종료되어야 다음 task를 실행할 수 있습니다. main queue sync를 호출하고 작업을 전달하면 현재 수행하던 작업을 block하고 해당하는 작업을 수행하려고 합니다. 그런데 이렇게 전달된 작업은 block된 작업이 끝나야만 수행할 수 있습니다. 결국 앱은 deadlock 상태에 빠지며 죽어버리게 됩니다.
- async 메서드는 파라미터로 전달된 작업을 비동기 방식으로 추가합니다.
- 작업이 완료될 때까지 기다리지 않습니다.
- 현재 진행중인 작업에 영향을 주지 않기 때문에 주로 async 메서드로 작업을 추가합니다.
- 주의할 점은 두 메서드가 Dispatch Queue 내부의 동작방식에는 영향을 주지 않는다는 것입니다.
- 동작방식은 Dispatch Queue를 생성할 때 결정한 attributes(serial or concurrent)에 따라 결정됩니다.
asyncAfter
- async 메서드에는 특정시간에 작업을 수행하도록 예약하는 메서드가 존재합니다.
- deadline에는 DispatchTime값을 전달합니다.
- DispatchTime의 static 메서드인 now()를 통해 현재시간을 가져올 수 있습니다.
let delay = DispatchTime.now() + 3
concurrentQueue.asyncAfter(deadline: delay) {
...
}
- 이렇게 구현하면 3초 후에 작업을 수행합니다.
concurrentPerform
- concurrentPerform 메서드는 iterations에 지정된 수만큼의 작업을 반복처리하는데, 시스템 자원이 허락하는 만큼 코드를 병렬적으로 처리합니다.
- 클로저에는 반복 index가 전달됩니다.
var start = DispatchTime.now()
for index in 0..<20 {
print(index, terminator: " ")
Thread.sleep(forTimeInterval: 0.1)
}
var end = DispatchTime.now()
print(Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1000000000)
start = DispatchTime.now()
DispatchQueue.concurrentPerform(iterations: 20) { index in
print(index, terminator: " ")
Thread.sleep(forTimeInterval: 0.1)
}
end = DispatchTime.now()
print(Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1000000000)
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 2.025312042
// 0 1 3 5 6 7 4 2 9 8 10 11 12 13 14 15 16 17 19 18 0.303290042
- 단순히 반복문으로 구현했을 때와 수행시간을 비교해보았습니다.
- 병렬로 실행할 경우 실행시간이 매우 빨라짐을 알 수 있습니다.
- 다만 실행순서는 보장되지 않으므로 주의해야합니다.
DispatchWorkItem
- DispatchWorkItem은 Dispatch Queue 혹은 DispatchSource에 들어갈 작업을 캡슐화하는 객체입니다.
- 직접 실행할 수도 있지만 그렇게 사용하는 경우는 많지 않습니다.
let workItem = DispatchWorkItem(block: {
...
})
- block을 기반으로 간단하게 생성할 수 있습니다.
DispatchQueue.main.async(excute: workItem)
- DispatchQueue에서 제공하는 메서드로 해당 작업을 간단하게 추가할 수 있습니다.
workItem.cancel()
- Operation과 같이 취소 기능을 제공하지만 그리 효율적이지 않아 많이 사용하지 않습니다.
- Operation과 같이 isCancel 속성에 접근하여 작업을 중단해야합니다.
- 보통 클로저 내부에서는 해당 속성에 바로 접근할 수 없기 때문에 따로 참조를 만들어두고 접근해야합니다.
workItem.notify(queue: DispatchQueue.main) {
...
}
- notify 메서드를 사용해 종료시 수행할 작업을 정의할 수 있습니다.
- queue 파라미터에는 해당 작업을 수행할 queue를 입력합니다.
let result = workItem.wait(timeout: 3)
// .timedOut or .success
- notify 메서드는 반드시 작업이 정상적으로 종료되어야 작동합니다.
- deadlock이 발생할 경우 수행되지 않습니다.
- wait 메서드를 사용하면 특정시간만큼만 대기하고 작업이 종료되도록 만들 수 있습니다.
- 이를 통해 작업의 deadlock을 방지할 수 있습니다.
- 해당 함수의 리턴값을 통해 작업이 정상적으로 종료되었는지, timeout이 지나 종료되었는지 알 수 있습니다. (.timedOut or .success)
- wait 메서드는 동기적으로 작동하기 때문에 main thread에서는 사용을 유의해야 합니다.
- 네트워크에 요청한 작업을 일정시간동안 대기할 때 활용할 수 있습니다.
DispatchSourceTimer
- 타이머를 기반으로 이벤트 처리 블록을 넣을 수 있는 Dispatch Source입니다.
- cf. DispatchSource : 파일시스템, 타이머, UNIX Signal 등의 low-level 이벤트의 처리를 조정하는 객체
- cf. 생성된 타이머는 따로 제거하는 과정이 없다면 계속해서 존재하며 리소스를 사용합니다. 따라서 현재 기능에서 벗어나 페이지를 벗어나는 등의 작업을 수행한다면 반드시 현재 돌아가고 있는 timer를 제거해야합니다.
let timer = DispatchSource.makeTimerSource(flag: [], queue: DispatchQueue.main)
timer.schedule(deadline: .now(), repeating: 1)
timer.setEventHandler(handler: {
...
})
- DispatchSource의 makeTimerSource 메서드로 DispatchSourceTimer를 생성할 수 있습니다.
- DispatchSourceTimer의 schedule 메서드로 작동시간(deadline) 및 주기(repeating)를 결정할 수 있습니다.
- DispatchSourceTimer의 setEventHandler 메서드로 해당 이벤트가 발생하면 동작할 작업을 입력할 수 있습니다.
timer.resume()
- resume 메서드로 timer를 실행합니다.
timer.suspend()
- suspend 메서드로 timer를 일시중지합니다.
timer.cancel()
- cancel 메서드로 timer를 완전중지합니다.
- 이렇게 완전중지된 timer는 재사용할 수 없습니다.
DispatchGroup
- DispatchGroup은 Dispatch Queue에 추가된 작업을 가상으로 그룹화하여 관리합니다.
- 서로다른 Dispatch Queue에 추가된 작업을 같은 그룹으로 묶을 수도 있습니다.
- 그룹화된 작업들은 하나의 큰 작업으로 인식됩니다.
- 즉 그룹에 포함된 모든 작업이 종료되어야 해당 그룹이 완료됩니다.
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
...
self.group.leave()
}
- 작업을 시작하기 전에 enter 메서드를 통해 작업이 시작되었음을 알리고, 작업이 종료되면 leave 메서드를 호출해 작업 종료를 알리는 방법이 있습니다.
- 이렇게 구성할 경우 모든 enter로 count된 작업이 모두 종료되면 그룹 작업이 종료된 것으로 인식합니다.
- 다만 이 방식은 leave 메서드를 호출하는 것을 잊어버릴 위험이 있기 때문에 많이 사용하지 않습니다.
DispatchQueue.main.async(group: group) {
...
}
- Dispatch Queue의 async 메서드에는 group을 파라미터로 받는 메서드가 존재합니다.
- 이를 사용하면 굳이 enter 및 leave를 호출해줄 필요가 없기 때문에 매우 유용합니다.
group.notify(queue: DispatchQueue.main) {
...
}
let result = group.wait(timeout: 3)
// .timedOut or .success
- DispatchGroup에서는 DispatchWorkItem에서 확인한 것과 같은 notify, wait 메서드를 제공합니다.
- notify 메서드는 그룹의 작업이 모두 종료된 후 수행할 작업을 정의할 수 있습니다.
- wait 메서드는 작업 수행을 기다릴 시간을 정의할 수 있습니다. (리턴값을 통해 정상적으로 종료되었는지 확인할 수 있습니다.)
DispatchSemaphore
- Counting Semaphore의 일종으로 여러 실행 Context에서 Resource에 대한 접근을 제어하는 객체입니다.
- signal() 메서드를 통해 세마포어 수를 늘리고 wait() 메서드를 통해 세마포어를 감소시키는 것이 핵심입니다.
- 본격적인 학습에 앞서 예제를 하나 살펴봅시다.
let group = DispatchGroup()
var value = 0
DispatchQueue.global().async(group: group) {
for _ in 1...1000 {
value += 1
}
}
DispatchQueue.global().async(group: group) {
for _ in 1...1000 {
value += 1
}
}
DispatchQueue.global().async(group: group) {
for _ in 1...1000 {
value += 1
}
}
group.notify(queue: DispatchQueue.main) {
print(value)
}
- DispatchGroup을 활용해 value를 1씩 늘리는 작업 3개를 수행한 후에 해당 값을 출력하도록 해봤습니다.
- 이론대로라면 value는 3000이 되어야합니다.
- 하지만 그에 조금 못미치는 2972가 출력되었습니다.
- 몇번을 시도해보아도 숫자가 조금씩 변하며 3000에는 미치지 못하는 숫자가 나옵니다.
- 이는 각 작업 단위에서 value에 동시에 접근했기 때문에 발생하는 일입니다. 2개 이상의 작업이 같은 시간에 value에 접근하여 값을 올리면서 value가 2 이상이 아닌 1만 상승하게 된 것입니다.
- 이러한 문제를 해결하기 위해서 사용할 수 있는 것이 DispatchSemaphore입니다. (물론 SerialQueue를 활용해 순서대로 작업을 처리하도록 변경할 수도 있습니다.)
let semaphore = DispatchSemaphore(value: 1)
- value 값을 주면서 DispatchSemaphore 객체를 생성합니다.
- 해당 value는 한번에 허용할 작업의 개수입니다.
- 작업을 실행할때는 wait 메서드를 사용합니다. wait 메서드에서는 value가 0보다 큰지 확인합니다. 0이라면 작업을 수행하지 않고 해당 value가 1 이상이 될때까지 기다립니다. 작업이 시작되면 value 값을 1 줄입니다.
- cf. wait 메서드에 timeout을 부여해 기다릴 시간을 결정할 수도 있습니다. timeout만큼의 시간만 기다리고 작업을 수행합니다. 이를 활용해 deadlock을 방지할 수 있습니다.
- 작업이 종료되면 signal 메서드를 사용합니다. signal 메서드는 작업이 끝났다는 의미이므로 value 값을 1 늘려줍니다.
let group = DispatchGroup()
var value = 0
let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async(group: group) {
for _ in 1...1000 {
semaphore.wait()
value += 1
semaphore.signal()
}
}
DispatchQueue.global().async(group: group) {
for _ in 1...1000 {
semaphore.wait()
value += 1
semaphore.signal()
}
}
DispatchQueue.global().async(group: group) {
for _ in 1...1000 {
semaphore.wait()
value += 1
semaphore.signal()
}
}
group.notify(queue: DispatchQueue.main) {
print(value)
}
- DispatchSemaphore를 활용해서 앞선 코드를 개선해봤습니다.
- 이제는 제대로 3000이 출력되네요.
- semaphore를 통해 작업순서를 제어할수도 있습니다.
var value = "변경전"
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 1)
value = "변경후"
}
DispatchQueue.global().async {
print(value)
}
- 하나의 작업은 value를 "변경후"로 바꿔주고, 하나의 작업은 이를 출력합니다.
- 하지만 변경작업은 sleep 메서드를 사용해 지연해줬기 때문에 바로 처리되지 않습니다.
- 그 결과 작업이 이와같이 출력됩니다.
- 하지만 이는 우리가 원하는 결과가 아닙니다. 우리는 "변경후"를 출력해주고 싶습니다.
var value = "변경전"
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 1)
value = "변경후"
semaphore.signal()
}
DispatchQueue.global().async {
semaphore.wait()
print(value)
}
- DispatchSemaphore를 활용해 이를 해결할 수 있습니다.
- 두번째 작업의 wait는 특정 작업에서 signal을 발생시켜야 작업수행이 가능해집니다.
- 첫번째 작업을 끝낼때 signal을 발생시켜줬습니다.
- 이제는 작업순서가 정렬되어 "변경후"가 출력되네요!
참고 및 출처 : https://developer.apple.com/documentation/dispatch/dispatchqueue
반응형
'🍎 Apple > Concurrency & GCD' 카테고리의 다른 글
[Concurrency] Task (0) | 2023.03.31 |
---|---|
[Concurrency] async / await (2) | 2023.03.31 |
[Swift] Operation Queue (0) | 2022.05.27 |
[Swift] Actor (4) | 2022.03.08 |
[Swift][문서의역] Task (0) | 2022.03.08 |