Concurrency Programming

Judy·2022년 7월 5일
0

iOS

목록 보기
16/28

동시성

컴퓨터로 영화를 틀어놓을 채로 문서도 보고 다운도 받고 동시에 여러가지 일을 처리할 수 있음

어떻게??

컴퓨터 용어

짤 하나로 정리되는 컴퓨터 용어 😎

코어

CPU에서 실제로 일을 처리하는 부분

코어가 많다 == 일을 할 수 있는 녀석이 많다!

코어는 한 번에 한 가지 일만 처리할 수 있음 -> 근데 어떻게 동시에 여러가지 일을 할 수 있지 ⁉️

맥날 직원을 생각해보자

  • 🤦사람에게 주문을 받고
  • 🍔 주문 받은 햄버거를 가져오고
  • 🍟 감자튀김을 튀기고

직원은 무려 3가지 일을 동시에 하고 있다. 하지만 정말 동시에일까?

직원의 팔이 여섯 개라 주문 받으면서 감자를 튀기면 동시라고 할 수 있갰지만 사실을 한 명의 직원이 여러 일을 조금씩 빠르게 돌아가면서 하는 것이다.

즉 한 번에 하는 것"처럼" 보이는 것이지 실제로는 일을 짧은 단위로 나누어서 번갈아가면서 처리하는 것 --> 동시성 프로그래밍의 원리!

멀티 코어 역시 직원이 여러 명인 것이지 각 직원은 한 번에 하나씩만 처리한다.

스레드

1. 하드웨어 스레드

  • 하이퍼스래딩 기술을 이용해 코어가 2가지 작업을 할 수 있도록하는 논리적인 '코어'
  • 1코어 2스레드 == 하나의 코어가 두 개의 코어처럼 작업

2. 소프트웨어 스레드

  • 논리적인 스레드
  • 프로세스(프로그램)의 작업 단위가 되는 가상의 스레드
  • 스레드는 프로그램의 작업을 처리하는 역할을 함
  • 경우에 따라 멀티 스레드일 수도 있고 단일 스레드일 수 있음
  • 논리적인 가상의 스레드이기 때문에 물리적인 스레드 수와 상관없이 많은 스레드를 만들 수 있음

동시성 프로그래밍은 소프트웨어의 멀티 스레딩을 이용한 기술❗️

병렬 프로그래밍

여러 개의 코어(CPU)마다 작업을 하나씩 맡아 여러 작업을 동시에 처리하는 것

맥날 직원의 병렬 작업을 생각해보자

  • 직원1 : 주문받기
  • 직원2 : 햄버거 조리하기
  • 직원3 : 감자튀김을 준비하기

직원들이 손님에게 햄버거 팔기라는 일을 분담해 하나씩 작업을 처리하고 있어 위 세 가지 일은 정말 동시에 일어나고 있다.

여러 CPU가 하나의 일을 분담해서 처리하기 때믄에 혼자서 일을 처리하는 것보다 빠르게 할 수 있음

--> 물리적인 코어(CPU)가 여러 개일 때만 가능!

  • 많은 연산이 필요한 그래픽 처리나 머신 러닝에서 사용되고 있음

실제로 iOS 개발에서 병렬 프로그래밍을 직접 구현할 일은 없을 것이다.
그래도 개념은 알아두는게 좋다!

직렬성 프로그래밍

하나의 스레드에서만 순서대로 작업을 처리하는 것

동시성 프로그래밍

하나의 CPU가 여러 작업을 동시에 (번갈아가면서) 처리하는 것

  • 동시성 프로그래밍은 싱글 코어에서도 가능
  • 병렬 프로그래밍과 반대되는 개념이 아님!
    --> 병렬 - 멀티 코어 / 동시성 - 다중 스레드

동기 vs 비동기

동기(Synchronous)

앞의 작업이 끝나기를 기다리는 것

현재 작업이 모두 끝나야 다음 작업으로 넘어갈 수 있다!

  • A 코드 ~~ 끝! -> B 작업

비동기(Asynchronous)

작업이 끝나는 것을 기다리지 않고 다음 작업을 실행하는 것

  • A 코드 작업~~~
  • -> B 코드 작업

동시성 프로그래밍 구현하기

1. GCD

  • 가장 먼저 나온 기능
  • Operation의 근간이 되는 API

동시성 프로그래밍을 구현하려면 멀티 스레드 환경이어야 함. 많은 스레드들을 어떻게 관리할까?
Apple에서는 개발자가 코드로 동기/비동기 처리만 해주면 시스템이 스레드를 알아서 관리하게 해줌 == GCD

Grand Central Dispatch
: Apple이 제공하는 멀티 코어 환경과 멀티 스레드 환경에서 최적화된 프로그래밍을 도와주는 기술

Dispatch 프레임워크의 Dispatch Queue 클래스를 주로 사용

DispatchQueue

  • Dispatch(보내다) + Queue(대기열) --> "대기열에 보내다"

DispatchQueue에 작업을 추가해주면 시스템이 알아서 스레드를 관리해서 작업을 처리
DispatchQueueFIFO로 작업을 처리

DispatchQueue의 속성

1. 다중스레드 사용 여부

Serial/Concurrent

  • Serial : 단일 스레드에서 작업 -> 기본값
  • Concurrent : 다중 스레드에서 작업
// Serial Queue
DispatchQueue(label: "Serial")
DispatchQueue.main

// Concurrent Queue
DispatchQueue(label: "Concurrent", attributes: .concurrent)
DispatchQueue.global()
  • main
    메모리에 늘 올라와 있는 기본 스레드
    전역적으로 사용 가능
    UI 작업은 메인 스레드에서만 작업할 수 있음
    다른 스레드가 필요하다면 메인 스레드에서 필요한 만큼의 스레드가 파생됨

  • global
    main 스레드가 아닌 작업을 처리하기 위해 만든 스레드
    global() 메서드가 호출되면 메모리에 올라왔다가 작업이 끝나면 메모리에서 제거됨

2. 동기 vs 비동기

sync/async

// sync
DispatchQueue.main.sync {}
DispatchQueue.global().sync {}

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

사용 예시

1. 메인.비동기 + 메인.비동기

DispatchQueue.main.async {
	// 코드 1
}

DispatchQueue.main.async {
	// 코드 2
}

코드1 -> 코드2

비동기니까 동시에 실행될거라 생각할 수 있어도 main 스레드 하나만 있기 때문에 순서대로 실행

2. 메인.비동기 + 일반 코드

DispatchQueue.main.async {
	// 코드 1
}

// 코드 2

랜덤 실행(코드2 -> 코드1)

둘 중의 어느 코드가 먼저 실행될지 모름

코드1 작업을 DispatchQueue에 넘긴 후 비동기이기 때문에 기다리지 않고 다음 코드를 실행해서 대개 코드2가 먼저 실행됨

3. 글로벌.비동기 + 글로벌.비동기 + 메인.비동기

DispatchQueue.global().async {
	// 코드 1
}

DispatchQueue.global().async {
	// 코드 2
}

DispatchQueue.main.async {
	// 코드 3
}

랜덤 실행

비동기로 DispatchQueue에 넘기기 때문에 각자 실행되어 어떤 것이 먼저 실행될지 모름 == 동시에 실행됨

여러 작업이 동시에 실행되려면 global이면서 async(비동기)여야 함

4. 글로벌.동기 + 글로벌.동기 + 코드

DispatchQueue.global().sync {
	// 코드 1
}

DispatchQueue.global().sync {
	// 코드 2
}

// 코드 3

코드1 -> 코드2 -> 코드3

global이라 다른 스레드에서 실행되지만 동기이기 때문에 실행이 완료되는 것을 기다려서 순서대로 실행됨

4. 메인.동기

// ❌ 에러
DispatchQueue.main.sync {
	// 코드 
}

main.sunc을 호출하게 되면 에러가 발생해 동작하지 않는다 -> 교착상태(deadlock)에 빠짐

  • main: 동기네? 끝날 때 기다려야지! = 멈춤
  • sync 코드: (실행되야 하는데 내가 실행되는 main이 멈춰버렸네...) = 멈춤
  • main: (뭐해..? 나 너 기다리는데..?)
  • sync 코드: (너가 멈춰있는데 어떻게 실행해..?)
  • main: ???
  • sync 코드: ???

이게 바로 deadlock이다 😊

메인 스레드에서 매인의 동기 작업을 불러서 발생한 문제이므로 메인이 아닌 다른 스레드에서 부르면 가능하다!

DispatchQueue.global().async {
	DispatchQueue.main.sync {
    	// 코드
    }
}

DispatchWorkItem

DispatchWorkItem를 이용하면 코드 블록을 캡슐화할 수 있다 -> 마치 클로저처럼 사용

let item = DispatchWorkItem {
	// 실행할 코드
}

DispatchQueue.main.async(excute: yellow)

asyncAfter

async 메서드를 원하는 시간에 호출할 수 있는 메서드

DispatchQueue.global().asyncAfter(deadline: .now() +5, execute: item)

5초 후에 uitem이라는 DispatchWorkItem을 실행

deadline 대신에 wallDeadline을 사용하면 시스템 시간을 기준으로 카운트

  • deadline : 지금부터 5로 후
  • wallDeadline : 5시니까 5시 5초에

asyncAndWait

비동기 작업이 끝나는 시점을 기다릴 수 있는 메서드

비동기 작업이지만 의도적으로 기다려야 할 때 사용 -> sync처럼 동작

DispatchQueue.global().asyncAndWait(execute: yellow)

DispatchQueue 초기화

커스텀 큐를 사용하기 위해 DispatchQueue를 초기화할 수 있다.

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

label

DispatchQueue의 이름을 설정하는 파라미터

let myDispatchQueue = DispatchQueue(label: "myQueue")

디버깅 환경에서 큐를 추적하기 위해 필요한 String

qos

DispathQoS 타입으로 우선순위를 정해주는 파라미터

QoS = Quality of Service


attributes

DispatchQueue의 속성을 정해주는 파라미터

  • serial: 단일 스레드 -> 기본값
  • .concurrent: 다중 스레드
let item = DispatchWorkItem {
	// 코드
}

let myDispatch = DispatchQueue(label: "myQueue", attributes: .initiallyInactive)

myDispatch.async(execute: item) 	// 코드 실행 안됨
myDispatch.active() 	// 코드 실행
  • .initiallyInactive: async, sync 상관없이 큐에 올려만 놓고 acive()를 호출해야 실행

autoreleaseFrequency

DispatchQueue가 자동으로 객체를 해제하는 빈도를 결정하는 파라미터

  • inherit: target과 같은 빈도 -> 기본값
  • workIteml: workItem이 실행될 때마다 객체를 해제
  • never: autorelease를 하지 않음

target

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

QoS

어떤 작업에 더 많은 스레드를 할당할지 결정하는 우선순위

결국 스레드는 시스템이 관리하기 때문에 QoS가 절대적인 우선순위 수치는 아님!

DispatchQueue나 sync, async의 파라미터로 지정할 수 있음

User-interactive

  • 가장 높은 우선순위
  • 메인 스레드에서 작업
  • 사용자 인터페이스 새로고침, 애니메이션 등과 같은 사용자와 상호작용하는 작업
  • 바로 수행되지 않으면 인터페이스가 멈추는 작업

User-initiated

  • 빠른 결과를 요구하는 사용자와의 상호작용하는 작업
  • 문서 열기, 버튼 클릭 작업 등

Default

  • QoS를 할당하지 않았을 때 기본값
  • 중간 정도의 수준

Utility

  • 시간이 걸리나 즉각적인 결과가 요구되지 않는 작업
  • 데이터 읽기, 다운로드 작업 등

Background

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

Unspecified

  • QoS 정보가 없음을 의미
  • 시스템이 QoS를 추론

async의 파라미터

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

group

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

qos

DispatchQoS

flags

코드를 실행할 때 추가로 적용할 속성

  • assingCurrentContext: 실행하는 context(큐 또는 스레드)의 속성을 상속받기
  • barrier: concurrent queue 환경에서 barrier 속성인 코드 블럭이 실행되기 전 코드들만 완료 시키고, 이후 코드들은 실행되지 않음
  • detached: 실행 중인 context의 속성을 적용하지 않기
  • enforceQoS: 현재 실행 중인 context보다 더 높은 QoS를 부여
  • inheritQoS: 실행 중인 context에 더 높은 QoS를 부여
  • noQoS: QoS를 할당하지 않고 실행(assingCurrentContext보다 우선시됨)

CompletionHandler

CompletionHandler, completion -> 함수의 실행 순서를 보장받을 수 있는 클로저

  • escaping: 함수의 실행이 끝나면 함수 밖에서 실행되는 작업

비동기일 때는 메서드가 끝나는 시점을 알 수가 없음
-> CompletionHandler와 같은 클로저를 사용하면 비동기 메서드도 종료되는 시점을 추적 가능

DispatchGroup

비동기적으로 처리되는 작업을 그룹으로 묶어서 추적하는 기능 - async에서만 사용 가능

DispatchGroup으로 async들을 묶어서 그룹의 작업이 끝나는 시점을 추적해서 동작을 수행하게 할 수 있다.

async 작업들이 꼭 같은 스레드나 큐에 있지 않아도 묶을 수 있다.

enter, leave

group으로 지정하는 방법

  1. async을 호출하면서 파라미터로 group 지정
  2. enter, leave를 코드 앞뒤로 호출해서 group 지정
let group = DispatchQueue()

// 1번 방법
DispatchQueue.main.async(group: group) {}
DispatchQueue.global().async(group: group) {}

// 2번 방법
group.enter()
DispatchQueue.main.async {}
DispatchQueue.global().async {}
group.leave()

notify

DispatchGroup의 작업이 끝나면 동작을 수행하기 위한 메서드

let group = DispatchGroup()

group.notify(queue: .main) {
	// 작업이 끝난 후 실행할 코드
}
  • queue: 코드 블록을 실행시킬 큐

wait

DispatchGroup의 작업이 끝나는 것을 기다리는 메서드

별도의 코드를 실행하지 않고 기다리기만 한다.

let group = DispatchGroup()

group.wait()
print("모든 작업이 끝났습니다.")

wait 메서드에 파라미터로 timeout을 설정해 기다리는 시간을 지정할 수 있다.

timeout을 10으로 지정하면 10초만 기다리고 group 작업이 끝나지 않았어도 다음 코드를 실행한다.

let red = DispatchWorkItem {
	// 코드 1
}

let blue = DispatchWorkItem {
	// 코드 2
    DispatchQueue.global().async(execute: yellow)
}

let group = DispathchGroup()

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

group.wait()
print("모든 작업이 끝났습니다")

red, blue를 group으로 묶어 wait으로 기다리지만 blue 안에 있는 yellow는 끝나지 않아도 wait 이후로 넘어간다.

blue 안에 있는 yellow는 비동기 작업으로 이미 다른 스레드로 넘겼기 때문의 blue의 작업을 끝난 것이다!

만약 yellow도 같이 기다리고 싶다면 yellow도 group으로 묶어주면 된다.

동시에 작업을 처리할 때 발생할 수 있는 문제점

Race Condition

하나의 값에 여러 스레드가 동시에 접근해서 발생하는 문제

ex) 같은 배열의 값을 꺼내고 지우는 동작을 하는 3개의 DispatchQueue.global().async
-> 하나의 값이 여러 번 꺼내질 수 있음

Thread Safe

Race Condition이 발생하는 이유는 Swift 스레드가 Thread Safe하지 않기 때문이다

Thread Safe = 여러 스레드가 하나의 값에 동시에 접근하지 못하는 것

Operation

  • GCD를 기반으로 세부적인 기능이 추가된 API

async/await

  • 가장 최신에 나온 기능
  • 다른 언어에 있는 기능을 Swift화 해서 가져온 것



참고 링크
야곰닷넷 - Concurrency Programming

profile
iOS Developer

0개의 댓글