Concurrency
필요성
- 가장 강력한 쓰레드인 Main Thread라고 해도 과도한 작업을 할당하면 성능 저하 발생
- 앱의 빠른 속도와 성능을 원한다면, 작업을 적절히 여러 쓰레드로 분산시켜야 함
사용법
- 기본적으로 GCD 혹은 Operation을 통해 동시성 프로그래밍을 할 수 있음
- Swift 5.5부터 언어단에서 동시성 프로그래밍을 제공하는 Swift Concurrency도 있음
- RxSwift 혹은 Combine 같은 반응형 프레임워크를 통해서도 동시성 프로그래밍을 할 수 있음
Sync vs Async
- Sync: Queue로 보낸 작업이 완료 될 때까지 기다림

let queue = DispatchQueue.global()
queue.sync {
queue.sync {
queue.sync {
- Async: Queue로 보낸 작업을 기다리지 않고 다음 코드 실행

let queue = DispatchQueue.global()
queue.async {
queue.async {
queue.async {
Serial vs Concurrent
- Serial queue: 한개의 쓰레드에서 전부 진행. 순서가 보장되어, 순차적 실행이 필요한 작업에 적합.

- Concurrent queue: 여러개의 쓰레드에서 진행. 종료 순서가 보장되지 않지만, 빠른 처리가 필요한 독립적인 작업에 적합.

GCD

GCD란
- GCD는 Grand Central Dispatch의 줄임말
- 작업을 Dispatch Queue에 추가하면 GCD가 자동으로 쓰레드를 생성하고 실행
- 작업이 완료되면 GCD는 쓰레드를 자동으로 제거해줌
- 위에서 언급했지만, iOS GCD는 동시성 프로그래밍을 할 수 있는 한 가지 방법
기본 요소들
- Queue: FIFO 대기 행렬
- GCD: 관리자
- Thread: 노동자
- Task: 작동할 코드 블럭
Dispatch Queue 종류
- Main Queue: 1개, Serial, Main Thread
- Global Queue: Concurrent, QoS 종류 6개
- Custom Queue: Serial/Concurrent 선택 가능, QoS 설정 가능
let serialQueue = DispatchQueue(label: "com.hohyeonmoon.serial")
let concurrentQueue = DispatchQueue(label: "com.hohyeonmoon.concurrent", attributes: .concurrent)
QoS 종류
- userInteractive: UI 업데이트, 애니메이션 등 즉각적인 반응이 필요한 작업
- userInitiated: 문서 열기 등 즉시 필요하지만 약간의 대기 가능한 작업
- default: 일반적인 작업
- utility: 데이터 다운로드 등 시간이 걸리는 작업
- background: 백업, 동기화 등 당장 필요 없는 작업
- unspecified: QoS 정보가 없는 작업
let queue = DispatchQueue(label: "com.hohyeonmoon.concurrent", qos: .userInitiated, attributes: .concurrent)
DispatchGroup
- 여러 쓰레드로 분배된 작업을 그룹으로 묶어서 관리할 수 있다
- 작업이 모두 완료되었을 때 콜백을 통해 알림을 받을 수 있다
let group = DispatchGroup()
someQueue.async(group: group) { ... }
someQueue.async(group: group) { ... }
someOtherQueue.async(group: group) { ... }
group.notify(queue: DispatchQueue.main) {
}
wait(timeout:)
을 통해 synchronous 하게 작업이 완료될 때까지 기다릴 수 있다
let group = DispatchGroup()
someQueue.async(group: group) { ... }
someQueue.async(group: group) { ... }
someOtherQueue.async(group: group) { ... }
if group.wait(timeout: .now() + 60) == .timedOut {
}
enter()
과 leave()
를 통해 작업을 시작하고 완료할 수 있다
let group = DispatchGroup()
group.enter()
someQueue.async {
group.leave()
}
DispatchWorkItem
- 클로저로 전달하는 방법 외에도 DispatchQueue에 작업을 제출하는 방법이 있다
- DispatchWorkItem은 큐에 제출하려는 코드를 보관할 실제 객체를 제공하는 클래스
let queue = DispatchQueue(label: "com.hohyeonmoon.concurrent")
let workItem = DispatchWorkItem { print("실행됨") }
queue.async(execute: workItem)
cancel()
을 통해 작업을 취소할 수도 있다notify(queue:execute:)
을 통해 작업이 완료되면 콜백을 통해 알림을 받을 수도 있다
let queue = DispatchQueue(label: "com.hohyeonmoon.concurrent")
let backgroundWorkItem = DispatchWorkItem { }
let updateUIWorkItem = DispatchWorkItem { }
backgroundWorkItem.notify(queue: DispatchQueue.main, execute: updateUIWorkItem)
queue.async(execute: backgroundWorkItem)
DispatchSemaphore
- 동시에 실행할 수 있는 작업의 최대 수를 제한하는 객체
wait()
을 통해 신호를 기다리고, signal()
을 통해 신호를 보낸다- 최대 동시 실행 수를 제한하여 작업 수를 제어할 수 있다
let semaphore = DispatchSemaphore(value: 2)
for i in 1...10 {
queue.async {
semaphore.wait()
defer { semaphore.signal() }
print("Downloading image \(i)")
Thread.sleep(forTimeInterval: 3)
print("Downloaded image \(i)")
}
}
Dispatch Barrier
- 여러 읽기 연산은 동시에 수행하면서, 쓰기 연산 시에는 큐를 독점해야 할 때 사용
- 이미지 배열에 다수 스레드가 동시 읽기를 수행하다가 새로운 이미지를 추가할 때
- barrier 플래그를 사용해 기존 읽기 연산이 모두 끝날 때까지 대기 후 독점적으로 쓰기 처리
class BarrierImages {
private let queue = DispatchQueue(label: "...", attributes: .concurrent)
private var _images: [Image] = []
var images: [Image] {
var copied: [Image] = []
queue.sync {
copied = _images
}
return copied
}
func add(image: Image) {
queue.sync(flags: .barrier) { [weak self] in
self?._images.append(image)
}
}
func remove(at index: Int) -> Image? {
var removed: Image? = nil
queue.sync(flags: .barrier) { [weak self] in
removed = self?._images.remove(at: index)
}
return removed
}
}
Operation
Operation
- GCD 기반으로 동작하는 상위 레벨 프레임워크
- 동시에 실행할 수 있는 동작의 최대 수 지정 가능
- 동작 일시 중지 및 취소 가능
- 작업 간 의존성 설정 가능
GCD vs Operation
- GCD: 가볍고 효율적인 구현, 간단한 작업에 적합, 빠른 성능
- Operation: 복잡한 작업 관리, 작업 상태 모니터링, 작업 간 의존성 관리, 작업 취소 및 재시도 기능
작업 상태 관리
- isReady: 작업이 실행 준비가 되었는지
- isExecuting: 현재 실행 중인지
- isFinished: 작업이 완료되었는지
- isCancelled: 작업이 취소되었는지
let operation = BlockOperation {
print("작업 실행 중...")
}
print(operation.isReady)
print(operation.isExecuting)
print(operation.isFinished)
의존성 관리
let downloadOperation = BlockOperation {
print("이미지 다운로드")
Thread.sleep(forTimeInterval: 2)
}
let filterOperation = BlockOperation {
print("이미지 필터 적용")
Thread.sleep(forTimeInterval: 1)
}
let uploadOperation = BlockOperation {
print("이미지 업로드")
Thread.sleep(forTimeInterval: 1)
}
filterOperation.addDependency(downloadOperation)
uploadOperation.addDependency(filterOperation)
let queue = OperationQueue()
queue.addOperations([uploadOperation, filterOperation, downloadOperation], waitUntilFinished: true)
동시 실행 수 제한
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
큐 일시 중지 및 재개
queue.isSuspended = true
queue.isSuspended = false
작업 취소
queue.cancelAllOperations()
operation.cancel()
Custom Operation
- 복잡한 작업을 위해 Operation을 상속받은 커스텀 클래스를 만들 수 있음
class ImageProcessingOperation: Operation {
private let imageURL: URL
private var task: URLSessionDataTask?
init(imageURL: URL) {
self.imageURL = imageURL
super.init()
}
override func main() {
guard !isCancelled else { return }
let semaphore = DispatchSemaphore(value: 0)
task = URLSession.shared.dataTask(with: imageURL) { data, response, error in
defer { semaphore.signal() }
guard !self.isCancelled else { return }
if let data = data {
print("이미지 다운로드 완료: \(data.count) bytes")
}
}
task?.resume()
semaphore.wait()
}
override func cancel() {
task?.cancel()
super.cancel()
}
}
주의사항
Main Thread
- UI 업데이트는 반드시 Main Thread에서 이루어져야 함
DispatchQueue.main.async {
}
- Main Queue에서 다른 Queue로 작업을 보낼 때 sync 사용 금지
- 버벅이는 현상 발생 가능
DispatchQueue.global().sync {
}
Retain cycle
- 객체에 대한 캡처 현상 주의, retain cycle 발생 가능
DispatchQueue.global().async { [weak self] in
}
Deadlock
- 데드락(= 교착 상태)이란 서로의 리소스를 기다리느라 작업이 영원히 대기 상태로 빠지는 현상
- Main Thread에서 DispatchQueue.main.sync 사용하면 데드락 발생
DispatchQueue.main.sync {
}
- 현재와 같은 Queue에 sync로 작업을 보내면 안됨
- Serial Queue에서 자기 자신에게 sync로 작업을 보내면 데드락 발생 가능
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.async {
serialQueue.sync {
}
}
Race Condition
- Shared property에 여러 스레드가 동시에 접근하고 수정하려 할 때 발생
- 방지하기 위해, 해당 변수를 하나의 Serial 큐를 통해서만 접근하도록 보장
- 읽기 및 쓰기를 모두 안전하게 감싸는 클래스 구현
Priority Inversion
- 우선순위가 높은 큐의 작업이, 우선순위가 낮은 큐의 작업 때문에 뒤로 밀리는 상황
.userInteractive
(높음) 큐와 .utility
(낮음) 큐가 같은 리소스를 잠근 경우- 먼저 잠근 쪽이 낮은 우선순위 큐라면, 높은 우선순위 작업도 잠금이 풀릴 때까지 대기해야 함
- 우선순위 역전을 방지하기 위해, 정말 필요한 경우가 아니면 서로 다른 QoS끼리 자원을 공유하는 상황을 피해야함
- 높은 우선순위가 필요한 작업은 따로 전용 큐로 분리
let high = DispatchQueue.global(qos: .userInteractive)
let low = DispatchQueue.global(qos: .utility)
let semaphore = DispatchSemaphore(value: 1)
high.async {
Thread.sleep(forTimeInterval: 2)
semaphore.wait()
defer { semaphore.signal() }
print("High priority task is now running")
}
low.async {
semaphore.wait()
defer { semaphore.signal() }
print("Running long, lowest priority task")
Thread.sleep(forTimeInterval: 5)
}
참고