Swift 동시성 프로그래밍 - 1
누가 동시성 프로그래밍을 물어본다면 'GCD로 여러 개의 스레드를!' 이라고 외치면서 4초 동안 몇 바퀴 돌면서 물리 피해를 줘보도록 하자
'저기요... 이번에 짠 프로그램 돌려보니까 너무 끊기는데 이거 수정 안되나요?'
라는 말을 들었을 때 어떻게 대답해야할지 잘 모르겠다면 이 게시글을 번호 순대로 찬찬히 읽어보시길 권장 드린다.
해당 게시글은 CodeSquad의 교육자료와 raywenderlich 의 교육 컨텐츠 중 'Concurrency by Tutorials' 를 읽고 정리한 것이다.
해당 게시글은 async / await 문법에 대해 다루지 않는다.
현재 모든 아이폰(2022.02 판매되고 있는 모델 기준)들의 코어 수는 칩의 모델 상관없이 6개다. 이 정도라면 우리가 데이터 목록을 Collection 형태로 순서에 따라 화면에 부드럽게 보여주는 정도는 기대해 볼 수 있지 않을까?
실제 이 게시물의 끝은 서울 열린데이터 광장 Open API를 이용한 성능측정용 애플리케이션 하나와 내가 개발하고 있는 캘린더 앱을 개선하는 것으로 끝맺을 예정이다. 2개의 앱을 실행시키고 사용해보면 뚝뚝 끊긴다는 느낌을 많이 받는다.
Mac은 말할것도 없지만 아이패드, 아이폰, 애플워치 등은 기본적으로 컴퓨터이고, 컴퓨터는 기본적으로 동기화 된 작업을 수행한다. 사용자의 명령을 입력받으면, 잠시 계산을 위해 멈췄다가(Interrupt, 인터럽트) 결과를 출력하는 것이다.
앞에서 말한 입력-출력은 동기화 된 작업이다(1 task / time). 리스트는 스크롤 할 때마다 뚝뚝 끊기고, 무언가를 실행한 프로그램은 작업을 끝낼 때까지 얼어 있을 것이다. 작업마다 우선순위를 정하여 따로 떼어 내어 하드웨어 자원을 할당해서 빨리 끝내고, 어차피 오래 걸릴 작업은 우선순위를 적게 두고 하드웨어 자원을 적게 할당하여 동시에 실행시키면 사용자 경험을 상승시킬 방법이 필요하다.
그러면 결국 할 수 있는 얘기는 '모든 데이터 처리 코드에 동시성을 적용하면 성능이 향상된다'는 얘기로 들릴 수 있겠지만, 동시성에는 대표적으로 3가지의 취약점 및 단점이 존재한다.
- | 문제점 | 해결방안(대표적) |
---|---|---|
Race Condition, 경쟁 상태 | 같은 자원 혹은 주소체계를 사용할 경우 발생. | DispatchQueue Barrier를 사용하거나 특정 작업을 동기화 시킨다. |
Deadlock, 교차 상태 | 2개 이상의 Thread가 자원을 공유할 경우 발생. | Semaphore, Mutex를 사용한다. |
Priority inversion, 우선순위 침범 | Qos가 의도한 바와 다르게 뒤바뀜. | Queue 자체를 다양화하여 자원을 공유. Semaphore를 사용할 수도 있다. |
이 게시물을 작성할 수 있게 해 준것은 raywenderlich 에서 출간한 'Concurrency by Tutorials' 라는 책 덕분이다.
이 책은 Swift의 기초 문법과 영어로 할 수 있는 표현방식들을 이해하거나 필자처럼 '대충 이정도 뜻이겠거니' 하고 넘어갈 수 있다면, 현업에서도 사용할 수 있는 이론과 테크닉을 얻어갈 수 있을 것이다.
총 페이지 수는 amazon에서 검색해보니 134페이지, 긴 책은 아니다. 필자는 해당 웹페이지의 1년 구독권을 사서 웹화면을 통해 읽어보았다.
이 책이 다루는 주제는 다음과 같다.
- Concurrency의 장단점. 주의사항.
- GCD(DiepatchQueue) 라이브러리.
- Operation, OperationQueue 클래스.
- 위의 두 라이브러리를 실제 사용하는 방법.
0. Process / Thread / Concurrent
0.1 프로세스와 스레드의 관계
Process
(프로세스)는 Processor
(프로세서, CPU)의 하위 단위이다. 프로세서가 작업해야할 작업 단위를 의미하기도 하고 실행한 응용 프로그램 자체를 의미하기도 한다. 원래는 저장장치에 저장되어 있던 프로그램이 메모리에 적재되면서 운영체제가 실행을 관리하게 되면 프로그램 혹은 작업이 Process
가 된다.
메모리 안에서 Process는 데이터를 읽거나 쓰면서 사용자가 원하는 작업을 수행하게 된다.
Thread
(스레드)는 프로세스의 가장 중요한 실행 제어 부분만을 뗴어 놓은 프로세스이다. 즉, 하나의 작업단위인 것이다. 이것도 프로세스이지만 프로세스의 하위 요소이므로, Light Weight Process
라고도 부른다. Process
는 하나 이상의 Thread
를 가진다. 스레드의 동작은 다음과 같다.
- 스레드 실행에 대한 상태 관리
- 실행을 위한 별도 스택
- 지역 변수와 스레드 특정 데이터를 저장하는 데이터 저장소
- 프로세스의 메모리와 자원에 대한 접근을 기록하는 컨텍스트 정보
Swift는 직접 Thread를 서브클래싱 하는 것보단 GCD를 이용한 DispatchQueue, OperationQueue 라이브러리/클래스를 사용하길 권장하고 있다.
필자는 Macbook Air M1 모델을 사용하고 8Gb 메모리가 탑재되어 있다. 그런데 Xcode도 실행하고, Safari 브라우저에 탭도 10개 정도 띄우고, 애플뮤직으로 음악도 듣고, 물론 카톡도 켜 놓았다. 지금 내가 카톡을 보고 있지는 않지만 누군가가 카톡을 보내면(물론 상상 속의 얘기다) 알림을 확인할 수 있을 것이다. 여기까지만 들었을 땐 이 모든 작업이 8Gb에 다 들어갈 수 있을까? 꼭 이것때문은 아니지만 앞으로 설명할 Process Scheduling에 대한 이야기를 준비했다.
0.2 Process Scheduling
프로세스는 5개의 상태를 갖는다.
- | 시작 커널 명령어 | 설명 |
---|---|---|
Created(new) | fork() | 저장되어 있던 작업이 새 프로세스로 만들어진다. |
Ready, Running | fork() | 프로세스가 메모리에 적재된다. |
Running | schedule() context_switch() | 작업이 실제 처리되고 있는 상태. |
Waiting | context_switch() | context_switch()에 의해 작업이 잠시 멈춘 상태. 아직 끝나진 않음. signal에 의한 멈춤은 Interruptible, 하드웨어 상황에 따른 멈춤은 UnInterruptible이라고 부른다. |
Terminated, Zombie | exit() | 프로세스가 멈춘 상태. |
Process Scheduling
은 프로세스가 다음의 상태를 바꿔가면서 효율적인 작업 처리를 관리하는 것을 말한다.
즉, 프로세스를 많이 띄워놓는다고 해도 그걸 동시에 모두 실행해놓고 있지는 않는다는 것이다.
0.3 Swift와 Thread
실제 Swift에서 Thread 클래스를 직접 서브클래싱하는 것은 대단히 안좋은 생각이다. 이는 OS에게 관리하도록 놔두고 이를 추상화하는 클래스를 사용하는 것이 좋다.
DispatchQueue를 객체화하게 되면, OS가 하나 이상의 스레드를 만들 준비를 한다. DispatchQueue, Operation, OperationQueue가 있다.
0.4 Concurrent, Concurrency
잠시 오해가 있는 부분인 Asynchronous(비동기)가 Concurrency(동시성)의 의미 차이에 대해 알아보자.
프로그래밍에서 Asynchronous는 Process의 다음 작업이 이전 작업의 return을 기다리지 않고 바로 작업을 실행한다. 반대로 Synchronous는 Process는 다음 작업이 이전 작업의 return을 기다리기 때문에 순서대로 작업이 실행된다.
Concurrent 혹은 Concurrency 하다는 의미는 작업이 Single Thread 인지 Multi Thread 인지 판단해보면 된다(Concurrent는 Multi Thread). 또한, Concurrent는 작업의 시작 부분과 소통이 가능하다는 특징을 가지고 있기 때문에 그렇지 않은 작업과 쉽게 구분 가능하다.
1. Quality of Service
우선 비동기 작업에 대해 얘기하기 전에, 작업의 우선순위를 정할 수 있는 QoS(Quality Of Service)에 대해 알아보자.
QoS는 작업을 처리하는 DispatchQueue, OperationQueue에 적용되는 타입(혹은 값)으로 각 큐 안의 작업(스레드)들 우선순위를 정한다. 큐 혹은 내부의 스레드들이 높은 우선순위를 갖는 경우 빠르게 처리되고 에너지를 더 많이 사용하게 된다.
우선 순위 | 이름 | 사용예시 |
---|---|---|
1 | userInteractive | UI, Event 관련 작업. 예시: UI 요소 계산, 에니메이션, 이벤트 핸들링 |
2 | userInitiated | 작업의 실행 및 결과로 사용자가 앱을 일시적으로 사용 못하게 함. 예시: 문서를 팝업 형태로 불러옴 |
3 | default | 일반적으로 사용하지 않는 것을 권장한다. default는 unspecified -> default -> utility의 구조를 갖는다 |
4 | utility | 사용자가 의도하였지만, 지속적으로 관찰하진 않는 작업에 사용된다. 예시: I/O, networking, 지속적인 데이터 inout |
5 | background | 사용자와의 상호작용이 전혀 없는 작업에 사용된다. 예시: Prefetching, backup, 외부 서버와의 동기화 작업 |
6 | unspecified | 이전 버전의 API와의 호환성을 위해 남겨놓은 값이다. 사용을 권장하지 않음. |
2. Grand Central Dispatch (GCD)
2.1 GCD의 정의와 Operation(OperationQueue)
GCD
는 C 라이브러리 중 하나인 libdispatch를 Swift에서 사용할 수 있도록 구현한 라이브러리이다. 다른 언어나 라이브러리에 비해 굉장히 경량화된 Thread를 다룬다는 장점이 있다(라고 Apple이 2009년에 소개할 때 말했다). Thread를 모아두는 Thread Pool 자체를 개발자가 아닌 운영체제가 다루게 한다는 특징이 있다.
기존에 있는 라이브러리를 구현한 것이라서 그런지 라이센스는 신기하게도 Apache 재단이 가지고 있다.
그리고 앞으로 설명할 DispatchQueue
/ Operation
, OperationQueue
는 이러한 GCD를 이용하여 만든 라이브러리로 이것들을 이용하면 Swift에서 동시성 프로그래밍을 할 수 있다.
실제 웹에서 'DispatchQueue vs OperationQueue' 같은 주제를 찾아보면 고수준의 OperationQueue를 추천하고 있다. 또한 애플의 추천이기도 하다.
- | DispatchQueue | OpeartionQueue |
---|---|---|
수준 | 저수준 | 고수준 |
작업단위 | Closure, DispatchWorkItem class, DispatchGroup class | Operation class, OperationQueue class |
의존성추가 | DispatchWorkItem notify 메소드 사용 | Operation addDependency 사용 |
작업취소 | DispatchWorkItem cancel 메소드 사용 | Operation cancel 메소드 사용 |
기본 QoS | default | background |
추천 사용처 | background에서 작업을 실행해야할 경우 | 재사용할 기능이 있을 경우 |
3. DispatchQueue
3.1 DispatchQueue 의 정의
DispatchQueue
는 Grand Central Dispatch 라이브러리를 구현한 Queue 데이터 구조 중 하나이다. FIFO 방식의 처리방식으로, 먼저 들어간 것이 먼저 출력 혹은 처리되는 구조 말이다. 이 방식으로 작업을 처리하게 되면 개발자의 의도에 따라 프로그램을 처리할 수 있다.
3.2 DispatchQueue
DispatchQueue
는 개발자가 의도한 task를 closure 형태로 받아 실행한다.
let queue = DispatchQueue(label: "Hello, DispatchQueue!").global(qos: .utility)
queue.async {
// task for executing asynchronously.
DispatchQueue.main.async {
// task for executing UI Update.
}
}
3.3 DispatchWorkItem
DispatchWorkItem
는 실행할 클로저(작업) 등을 전달하고 취소 기능도 사용하고 싶을 때 쓴다
let url = URL(string: urlString)!
let completeUrl = URL(string: completeUrlString)!
let session = URLSession.shared
let secondWork = DispatchWorkItem {
session.dataTask(with: completeUrl) { data, resource, error in
self.show(
UIAlertController(
title: "Result?",
message: "Success!! :-)",
preferredStyle: .alert)
)
}
}
let firstWork = DispatchWorkItem {
session.dataTask(with: url) { data, resource, error in
if error != nil {
// 원한다면 취소도 가능.
// 취소 작업은 현재 실행중인 작업 이후의 작업에만 영향을 끼친다.
// 즉, 이후의 작업을 즉시 return 시킨다.
secondWork.cancel()
}
}
}
firstWork
.notify(
queue: DispatchQueue.main
, execute: secondWork
)
DispatchQueue
.global(qos: .background)
.async(execute: firstWork)
3.4 DispatchGroup
DispatchGroup
는 여러 작업을 동시에 실행시키고 싶을 때 사용한다.
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)
queue.async(group: group) {
upload(file: file1)
}
queue.async(group: group) {
upload(file: file2)
}
let isTimeout = group.wait(timeout: .now() + 10) == .timeout
self.show(
UIAlertController(
title: isTimeout ? "Fail Alert" : "Result"
, message: isTimeout ? "It took so long~" : "File Uploaded"
, prefferedStyle: .alert)
)
4. Operation, OperationQueue
4.1 OperationQueue, Operation의 정의
Operation
, OperationQueue
는 DispatchQueue보다 고수준의 라이브러리/클래스 입니다. 여러가지 유틸리티를 제공하는 Operation/OperationQueue는 실제 코드를 클린하게 만들거나, 코드를 재사용하는 방식이 객체지향에서 사용하던 방식들을 그대로 사용할 수 있어서 유용한 측면이 많다.
4.2 Operation State
Operation 클래스의 상태값은 클래스 내에 지역 변수로 존재한다. 이를 통해 Operation 작업의 종료 및 실행을 제어하는 척도로 삼을 수 있어 유용하다.
의미 | invoke method | |
---|---|---|
isReady | 객체화 후 실행할 준비가됨 | Operation.init() |
isExecuting | 작업이 실행되었음 | Operation.start() |
isCancelled | 작업 취소로 인해 작업 종료됨 | Operation.cancel() |
isFinished | 작업 완료로 인해 작업 종료됨 | - |
상태값들은 Read-Only 이다. 직접 수정이 불가한 Boolean 값들임에 주의하자.
4.2 OperationQueue, Opertaion
final class MyOperation: Operation {
override func main() {
// task
}
}
let queue = OperationQueue()
let op = MyOperation()
op.completionBlock = { // 기본 제공되는 클로저
DispatchQueue.main.async {
guard let cell = collectionView.cellForRow(at: indexPath) else {return}
cell.textLabel.text = "Job done~"
}
}
queue.addOperation(op) // 실행
4.3 Operation Cancel
final class MyOperation: Operation {
private let taskHandler: () -> UIImage?
private var result: UIImage?
override func main() {
result = taskHandler()
}
override func cancel() {
super.cancel()
result = nil
}
}
let tasks = [IndexPath: UIImage?]()
let queue = OperationQueue()
let operation = MyOperation()
let completeOperation = Operation()
let imageFactory = ImageFactory()
operation.taskHandler = { _ -> UIImage in
return imageFactory.makeImage()
}
completeOperation.completionHandler = {
cell.imageView.image = operation.result
}
operation.notify(completeOperation)
queue.addOperation(operation)
queue.addOperation(completeOperation)
if let image = tasks[indexPath] {
operation.cancel()
cell.imageView.image = tasks[indexPath]
} else {
tasks[indexPath] = operation.result
}
https://tldp.org/LDP/tlk/kernel/processes.html
Author And Source
이 문제에 관하여(Swift 동시성 프로그래밍 - 1), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@sanghwi_back/Swift-동시성-프로그래밍-1저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)