GGURUPiOS

동시성 프로그래밍 - GCD (2) 본문

Swift/동시성 프로그래밍

동시성 프로그래밍 - GCD (2)

꾸럽 2023. 4. 24. 14:41

GCD (2) 활용

DispatchQueue를 main, global() 말고 커스텀해서 사용할 수 있음

DispatchQueue의 초기화

convenience init(label: String,
								qos: DispatchQoS = .unspecified,
								attributes: DispatchQueue.Attributes = [],
								autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit,
								target: DispatchQueue? = nil)

label

let dispatchQueue = DispatchQueue(label: "ggurup")

DispatchQueue의 label을 설정해주는 파라미터

DispatchQueue의 이름은 변수나 상수 네이밍으로 정해주면 됨

→ 이 label은 디버깅 환경에서 추적하기 위한 String 값임

qos

qos 는 DispatchQoS 타입의 값을 받는 파라미터임

실행 될 Task들의 우선 순위를 정해줌

( 밑에서 추가 설명 )

attributes

attributes는 DispatchQueue의 속성을 정해주는 값임

기본 값은 serial Queue 가되고, .concurrent 로 초기화하면 concurent Queue 가 됨 ( 멀티 스레드 환경 )

그 외에

.initiallyInactive 라는 속성이 하나 더 있음 (말 그대로 Inactive 상태로 시작된다)

DispatchQueue 들은 코드 블럭을 호출하는 즉시 작업이 처리 되나, 이 속성은 그 작업을 제어할 수 있음

active()를 호출하기 전까지 작업을 처리하지 않음

let a = DispatchWorkItem {
	for _ in 1...5 {
		print("a")
		sleep(1)
	}
}

let dispatchQueue = DispatchQueue(label: "ggurup", attributes: .intiallyInactive)

dispatchQueue.async(execute: a)
dispatchQueue.activate() // 이 때 작업 시작 됨

autoreleaseFrequency

자동으로 객체를 해제하는 빈도의 값

객체를 autorealease 해주는 빈도이며 기본값은 inherit 임

  • inherit: traget과 같은빈도
  • workItem: workItem이 실행될 때 마다 객체 해제
  • never: autorelease를 하지 않음

target

코드 블록을 실행할 큐를 target으로 설정


QoS (Quality of Service )

우선순위 → 그러나 무엇을 먼저 처리, 끝낸다는 의미가 아닌 무엇에 중점을 둘 까에 대한 맥락

초기화 구문 뒤의 async,sync 파라미터로 DispatchQoS 우선 순위를 할당할 수 있음

DispatchQoS의 유형 ( 4개의 주요 유형과 2개의 특수 유형 ) ( 열거형 )

  • userInteractive, userInitialted, default, utility, background, unspectified

User-interactive

사용자와 상호작용하는 작업에 할당

메인 스레드에서 작업 ( 사용자 인터페이스 새로고침, 애니메이션 )

User-initiated

버튼 액션처럼 빠른 결과를 요구하는 유저와의 상호작용 작업

Default

기본값 (중간수준)

Utility

데이터를 읽거나 다운로드하는 작업 ( 어느정도 시간이 걸리는 작업 )

Background

index 생성, 동기화, 백업 등 사용자가 볼 수 없는 백그라운드의 작업에 할당

Unspecifed

QoS의 정보가 없음, 시스템이 추론해야함


async 의 파라미터

func async(group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping () -> Void)

// group, qos, flags 

group

DispatchQueue의 async 코드 블럭을 묶어서 관리해주는 DispatchGroup임

(다음장에서 설명)

qos

(이전장에서 설명)

flags

DispatchWorkItemFlags 타입의 값을 받는 파라미터

추가 속성을 겾렁함

기본값은 아무 속성 부여하지 않음

flgas 값은 OptionSet이므로 여러 속성을 한번에 부여 가능

  • assingCurrentContext: 코드블럭을 실행하는 context의 속성을 상속 ( qos와 같은 속성을 동일하게 함)
  • barrier: concurrent queue에서 차단역할을 함. barrier 속성의 코드블럭이 실행되기 전 실행되었던 코드들은 오나료되고, barrier 속성의 코드블럭이 실행되기 전까지 다른 코드블럭은 실행되지 않음
  • detached: 실행할 코드블럭에 실행중인 context의 속성 적용 X
  • enforceQoS: 실행 중인 context의 QoS보다 더 높은 우선순위를 부여
  • inheritQoS: enforceQoS와 반대개념. 실행 중인 context에 더 높은 우선순위
  • noQoS: QoS를 할당 X 코드블럭 실행 ( assingCurrentContext 보다 우선 )

CompletionHandler

completionHandler 혹은 completion 이라는 클로저를 가진 메소드 들

→ 함수의 실행 순서를 보장 받을 수 있는 클로저임

비동기 작업은 언제 끝날지를 정확히 파악할 수 없음

만약 비동기 작업 A가 끝나야 작업 B를 수행해야 한다면?

→ completionHandler, completion 같은 클로저를 구현 해 작업이 끝나는 시점에 원하는 동작 수행


DispatchGroup

DispatchGroup은 비동기 작업들을 그룹으로 묶어, 상태를 추적

async들을 묶어 작업이 끝나는 시점을 추적하여 다른 동작을 수행시킬 수 있음

async에서만 사용 가능하며 notify, wait, enter, leave 등 기능을 사용할 수 있음

group 등록 ( enter, leave )

특별한 초기화 구문은 없음

init()으로 사용

2가지 사용방법

  • async를 호출하며 파라미터로 group 지정
  • enter, leave를 코드의 앞뒤로 호출하여 group 지정
let group = DispatchGroup()

group.enter()
DispatchQueue.main.async {}
DispatchQueue.global().async {}
group.leave()

/*
DispatchGroup이 enter() 부터 leave() 까지 포함된다 라는 의미임
위의 코드에서는 
DispatchQueue.main.async {}
DispatchQueue.global().async {}
두 작업들이 group에 묶임
*/

enter와 leave로 묶인 group 에 대해서는 notify() , wait() 으로 작업을 추적할 수 있음

notify

notify 는 group의 작업이 끝나는 시점에 원하는 동작을 수행하기 위한 메서드임

group.notify(queue: .main) {
	print("작업이 끝남")
}

// 파라미터 queue는 코드블럭을 실행시킬 queue를 말함

위와 같이 작성하면, 그 전 예제에서

DispatchQueue.main.async {} DispatchQueue.global().async {}

의 작업이 끝나면 notify 의 코드블럭이 실행됨

wait

wait은 group의 수행이 끝나기를 기다리기만 하는 메서드임

let group = DispatchGroup()

DispatchQueue.global().async(group: group, execute: blue)
DispatchQueue.global().async(group: group, execute: red)

group.wait()
print("작업 완료")

위와 같이 작성하면 두 작업이 끝날 때 까지 기다림

만약 두 작업이 끝나면 다음코드 (위에서는 print문) 이 실행 됨

wait에는 timeout 파라미터를 설정할 수도 있음

timeout 시간 동안만 기다리겠다는 의미임

group.wait(timeout: 10)

→ 10초 동안 그룹을 기다리고, 만약 작업이 10초가 넘어가면 그 다음 코드들을 실행


DispatchSemaphore

DispatchSemaphore는 공유 자원에 접근할 수 있는 스레드의 수를 제어해주는 역할을 함

Race Condition을 방지해주는 역할을 해줄 수 있음

Race Condition이란 ? → 하나의 값에 여러 스레드에서 동시에 접근하는 경우

DispatchSemaphore는 semaphore count를 카운트 하는 식으로 동작함

허용된 스레드의 수만큼 접근할 수 있도록 함

만약 허용된 스레드의 수만큼 접근된 상태이면, 다른 스레드는 기다리게 됨

하나의 스레드가 접근 → count - 1

하나의 스레드의 접근이 끝나면 → count + 1

(재고 처럼 관리 된다고 보면 될 듯)

let semaphore = DispatchSemaphore(value: 1) 

DispatchQueue.global().async {
    semaphore.wait()

    semaphore.signal()
}

wait()은 접근을 알리는 메서드이며

signal()은 접근을 끝냄을 알리는 메서드임

wait() 과 signal()을 반드시 짝지어서 호출해야함

Serial Queue로 race condition 해결

Race Condition이 발생하는 이유는 → 여러 스레드에서 순서가 없이 배열에 접근했기 때문임

DispatchSemaphore처럼 순서를 만들어주면 됨

Serial Queue로 질서를 만들어 줄 수 있음


UI작업을 꼭 메인 스레드에서 해야하는 이유

  1. UIKit은 Thread Safe 하지 않음
    1. UIKit의 대부분 요소는 여러 스레드에서 접근이 가능함 → Race Condition 발생 가능
  2. 다른 Serial Queue는 안되고 메인스레드여야 하는 이유
    1. 메인 스레드에는 Main RunLoop가 동작하고 있음
    2. 모든 스레드는 각자 RunLoop를 가질 수는 있지만, 각자 UI를 그리면 그려지는 시점이 모두 제각각이 됨 (비효율, Race Condition 발생 가능) (1번과 비슷한 내용)