Swift 동시성 프로그래밍 - 1

백상휘·2022년 2월 6일
1

iOS_Programming

목록 보기
3/10
post-thumbnail

누가 동시성 프로그래밍을 물어본다면 'GCD로 여러 개의 스레드를!' 이라고 외치면서 4초 동안 몇 바퀴 돌면서 물리 피해를 줘보도록 하자

'저기요... 이번에 짠 프로그램 돌려보니까 너무 끊기는데 이거 수정 안되나요?'
라는 말을 들었을 때 어떻게 대답해야할지 잘 모르겠다면 이 게시글을 번호 순대로 찬찬히 읽어보시길 권장 드린다.

해당 게시글은 CodeSquad의 교육자료와 raywenderlich 의 교육 컨텐츠 중 'Concurrency by Tutorials' 를 읽고 정리한 것이다.

해당 게시글은 async / await 문법에 대해 다루지 않는다.

현재 모든 아이폰(2022.02 판매되고 있는 모델 기준)들의 코어 수는 칩의 모델 상관없이 6개다. 이 정도라면 우리가 데이터 목록을 Collection 형태로 순서에 따라 화면에 부드럽게 보여주는 정도는 기대해 볼 수 있지 않을까?

실제 이 게시물의 끝은 서울 열린데이터 광장 Open API를 이용한 성능측정용 애플리케이션 하나와 내가 개발하고 있는 캘린더 앱을 개선하는 것으로 끝맺을 예정이다. 2개의 앱을 실행시키고 사용해보면 뚝뚝 끊긴다는 느낌을 많이 받는다.

Mac은 말할것도 없지만 아이패드, 아이폰, 애플워치 등은 기본적으로 컴퓨터이고, 컴퓨터는 기본적으로 동기화 된 작업을 수행한다. 사용자의 명령을 입력받으면, 잠시 계산을 위해 멈췄다가(Interrupt, 인터럽트) 결과를 출력하는 것이다.

앞에서 말한 입력-출력은 동기화 된 작업이다(1 task / time). 리스트는 스크롤 할 때마다 뚝뚝 끊기고, 무언가를 실행한 프로그램은 작업을 끝낼 때까지 얼어 있을 것이다. 작업마다 우선순위를 높게 하여 하드웨어 자원을 할당해서 빨리 끝내고, 오래 걸리는 작업은 우선순위를 적게 하여 하드웨어 자원도 적게 나눠서 두 작업을 동시에 실행시키면 사용자 경험을 상승시킬 수 있다.

결국 얘기가 '모든 데이터 처리 코드에 동시성을 적용하면 성능이 향상된다'로 정리된 것 같다. 아쉽게도 동시성에는 대표적으로 얘기되는 취약점 혹은 단점이 존재한다.

-문제점해결방안(대표적)
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는 데이터를 읽거나 쓰면서 사용자가 원하는 작업을 수행하게 된다.

자세한 내용은 https://tldp.org/LDP/tlk/kernel/processes.html


Thread(스레드)는 프로세스의 가장 중요한 실행 제어 부분만을 떼어 놓은 프로세스이다. 즉, 하나의 작업단위인 것이다. Light Weight Process 라고도 부른다. Process는 하나 이상의 Thread를 가진다. 스레드의 특징은 다음과 같다.

  • 스레드 실행에 대한 상태 관리
  • 실행을 위한 별도 스택
  • 지역 변수와 스레드 특정 데이터를 저장하는 데이터 저장소
  • 프로세스의 메모리와 자원에 대한 접근을 기록하는 컨텍스트 정보

Swift는 직접 Thread를 서브클래싱 하는 것보단 GCD를 이용한 DispatchQueue, OperationQueue 라이브러리/클래스를 사용하길 권장하고 있다.


필자는 Macbook Air M1 모델을 사용하고 8Gb 메모리가 탑재되어 있다. 그런데 Xcode도 실행하고, Safari 브라우저에 탭도 10개 정도 띄우고, 애플뮤직으로 음악도 듣고, 카카오톡 메신저도 켜 놓았다. 지금 내가 카카오톡 메신저를 보고 있지는 않지만 누군가가 카카오톡 메시지를 보내면(물론 상상 속의 얘기다) 알림을 확인할 수 있을 것이다. 이 모든 작업이 8Gb로 모두 처리할 수 있을까?

경험상 Xcode, Safari 두 개만 사용해도 8Gb 는 간당간당하다. 앞으로 설명할 Process Scheduling 는 메모리의 효과적인 사용을 위해 마련된 프로세스 관리 전략이다.

0.2 Process Scheduling

프로세스는 5개의 상태를 갖는다.

-시작 커널 명령어설명
Created(new)fork()저장되어 있던 작업이 새 프로세스로 만들어진다.
Ready, Runningfork()프로세스가 메모리에 적재된다.
Runningschedule()
context_switch()
작업이 실제 처리되고 있는 상태.
Waitingcontext_switch()context_switch()에 의해 작업이 잠시 멈춘 상태. 아직 끝나진 않음.
signal에 의한 멈춤은 Interruptible, 하드웨어 상황에 따른 멈춤은 UnInterruptible이라고 부른다.
Terminated, Zombieexit()프로세스가 멈춘 상태.

Process Scheduling은 효율적인 작업 처리를 위해 프로세스의 상태를 관리하는 것을 말한다.

즉, 프로세스(Xcode, Safari, Apple Music....)를 많이 띄워놓는다고 해도 그걸 동시에 모두 실행해놓고 있지는 않는다는 것이다.

0.3 Swift와 Thread

실제 Swift에서 Thread 클래스를 직접 서브클래싱하는 것은 대단히 안좋은 생각이다. 이는 OS에게 관리하도록 놔두고 미리 만들어진 클래스들을 활용하는 것이 좋다.

Swift Thread 를 활용하기 위한 클래스에는 DispatchQueue, Operation, OperationQueue 가 있다. 생성자를 이용해서 DispatchQueue/OperationQueue 를 객체화 하면 OS 는 하나 이상의 스레드를 만들 준비를 한다.

0.4 Concurrent, Concurrency

우선 들어가기 전에 Asynchronous(비동기)가 Concurrency(동시성)의 의미가 다르다는 것부터 짚고 넘어가자.

Asynchronous는 Process의 다음 작업이 이전 작업의 return을 기다리지 않고 바로 작업을 실행하는 것을 말한다. Asynchronous 가 Entry-Point 와 소통하지 않는다. (반대로 Synchronous의 경우 Process의 다음 작업이 이전 작업의 return을 기다리기 때문에 작업 순서가 보장된다)

Concurrency 는 간단하다. 많은 작업을 여러 개의 스레드에서 동시에 처리하는 것을 말한다. 짧게 Multi-Threading 이라고도 부른다. Concurrent 한 작업은 Entry-Point와 소통이 가능하다는 특징도 있어서 Asynchronous 와 구별 가능하다.

1. Quality of Service

비동기 작업에 대해 얘기하기 전에, 작업의 우선순위를 정할 수 있는 QoS(Quality Of Service)에 대해 알아보자.

QoS는 작업을 처리하는 DispatchQueue, OperationQueue에 적용되는 타입(혹은 값)으로 각 큐 안의 작업(스레드) 우선순위를 정한다. 큐 혹은 내부의 스레드들이 높은 우선순위를 갖는 경우 빠르게 처리되고 OS 자원을 더 많이 사용하게 된다.

우선
순위
이름사용예시
1userInteractiveUI, Event 관련 작업.
예시: UI 요소 계산, 에니메이션, 이벤트 핸들링
2userInitiated작업의 실행 및 결과로 사용자가 앱을 일시적으로 사용 못하게 함.
예시: 문서를 팝업 형태로 불러옴
3default일반적으로 사용하지 않는 것을 권장한다.
default는 unspecified -> default -> utility의 구조를 갖는다
4utility사용자가 의도하였지만, 지속적으로 관찰하진 않는 작업에 사용된다.
예시: I/O, networking, 지속적인 데이터 inout
5background사용자와의 상호작용이 전혀 없는 작업에 사용된다.
예시: Prefetching, backup, 외부 서버와의 동기화 작업
6unspecified이전 버전의 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를 추천하고 있다. 또한 애플의 추천이기도 하다.

-DispatchQueueOpeartionQueue
수준저수준고수준
작업단위Closure, DispatchWorkItem class, DispatchGroup classOperation class, OperationQueue class
의존성추가DispatchWorkItem notify 메소드 사용Operation addDependency 사용
작업취소DispatchWorkItem cancel 메소드 사용Operation cancel 메소드 사용
기본 QoSdefaultbackground
추천 사용처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보다 고수준의 라이브러리/클래스 입니다. 여러가지 유틸리티를 제공하기 때문에 객체지향적인 방식들을 그대로 사용할 수 있어서 유용한 측면이 많다.

4.2 Operation State

Operation 클래스의 상태값은 클래스 내에 지역 변수로 존재한다.

의미invoke method
isReady객체화 후 실행할 준비가됨Operation.init()
isExecuting작업이 실행되었음Operation.start()
isCancelled작업 취소로 인해 작업 종료됨Operation.cancel()
isFinished작업 완료로 인해 작업 종료됨-

상태값들은 Read-Only 이다. 에러 핸들링에 유용하다.

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 {
	var taskHandler: () -> UIImage?
    private(set) 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() // makeImage() 를 이용해 이미지 생성.

operation.taskHandler = { _ -> UIImage in
	return imageFactory.makeImage()
}
operation.completionBlock = {
	if let image = operation.result {
		operation.cancel()
		cell.imageView.image = image
	}
}
completeOperation.completionBlock = {
	cell.imageView.image = operation.result
}

operation.notify(completeOperation)
queue.addOperation(operation)
queue.addOperation(completeOperation)
profile
plug-compatible programming unit

0개의 댓글