본문 바로가기
Swift

동시성 프로그래밍 가이드 읽어보기.

by SeoB-P 2022. 1. 27.

동시성 프로그래밍을 이해하기 위해 애플의 문서를 읽어보았다.

 

동시성과 어플리케이션 다자인(Concurrency and Application Design)

과거에는 컴퓨터가 수행할 수 있는 단위 시간당 최대 작업량이 CPU의 클럭 속도에 의해 정해졌었음.

하지만, 기술이 발전하고 프로세서의 설계가 compact해짐에 따라 열과 같은 물리적 제약이 최대 클럭 속도를 제한하기 시작했다.

이 문제를 해결하기 위해 각 칩의 프로세서 코어 수를 늘리기 시작했다 (싱글코어 -> 듀얼코어,쿼드코어 등등)

코어수를 늘리면 CPU속도를 높이거나 칩 크기 등을 바꾸지 않아도 단일 칩에서 초당 더 많은 명령을 실행할수 있었다.

이제 추가된 이 코어들을 잘 활용을 해야하는데...

 

당연히 계산할 수 있는 코어가 늘었으니 컴퓨터에 여러 작업을 수행할 수 있는 소프트웨어도 필요하다.

(피지컬이 좋아졌다고 축구를 잘해지는게 아니듯이 몸을 잘 활용해서 축구를 할 테크닉이 필요하다)

OS X또는 iOS와 같은 최신 멀티태스킹 운영체제는 주어진 시간에 100개 이상의 프로그램이 실행될 수 있어서 다른 코어에서 각 프로그램을 스케줄링 하는 것이 가능해야 한다.

그러나 이러한 프로그램의 대부분은 실제 처리 시간을 거의 소모하지 않는 데몬 혹은 백그라운드 응용 프로그램이기 때문에 여분의 코어가 생길 것이고 이에 따라, 실제로 필요한 것은 개별 응용 프로그램이 여분의 코어를 효과적으로 사용할 수 있는 방법이다!

 

*데몬: 운영체제에서 사용자가 직접적으로 제어하지 않고, 백그라운드에서 돌면서 여러 작업을 하는 프로그램 = 시스템 프로세스

*코어: 이름에도 알수 있다시피 CPU내에서도 가장 핵심 부품으로 기본 연산과 계산작업을 함 .

 

어플리케이션이 다중 코어를 사용하는 전통적인 방법은 다중 스레드를 생성하는 것.

하지만, 이러한 스레드 해결방식에도 여러 크고 작은 문제가 있었다.

그 중 가장 큰 문제는 스레드 코드가 임의의 수의 코어에는 잘 확장이 되지 않는다는 점,

코어 수만큼 스레드를 생성할 수 없었고 당연히 프로그램이 잘 실행되기를 기대할 수 없었다.

효율적으로 사용할 수 있는 건 코어인데, 이걸 어플리케이션이 자체적으로 계산하기 어려운 문제였던것이다.

만약 코어의 숫자를 정확히 맞추더라도 여전히 스레드를 프로그래밍하고 관리하고 서로 간섭하지 못하게 해야하는 복잡한 문제가 생겼다.

 

문제를 요약하자면.

1. 어플리케이션이 다양한 수의 코어를 활용할 수 있는 방법이 필요하다.

2. 단일 어플리케이션에서 수행되는 작업의 양은 변화하는 시스템 조건을 수용하기 위해 동적으로 확장 될수 있어야한다.

3. 해결책들은 이러한 코어를 활용하는데 필요한 작업량이 증가하지 않을 정도로 간단해야 한다.

 

다행히!!

Apple의 운영체제가 이러한 문제에대해 해결책을 제시한다.

고로, 해결책을 구성하는 기술과 이를 활용하기 위해 코드에 적용할 수 있는 디자인을 살펴보자.

 

들어가기에 앞서.. - 스레드에서 벗어나기

스레드는 오랫동안 사용되어왔고 앞으로도 계속 사용이 될것이지만 확장 가능한 방식으로 여러 작업을 실행하는 문제를 해결하지 못한다.

스레드를 사용하면서 확장 가능한 솔루션을 만드는 책임이 전적으로 개발자에게 달려있기 때문.

개발자는 시스템 조건이 변경됨에 따라 동적으로 스레드 수를 결정해야하고 관리해야함 ->  개발 비용 상승

그리고 또다른 문제는 어플리케이션이 쓰레드의 작성 및 유지보수와 관련된 대부분의 비용을 부담하게 됨. -> 각 어플의 부담 증가

 

So..! OS X 및 iOS는 비동기식 설계 접근 방식을 취하여 동시성 문제를 해결하려고 함.

비동기란 동기의 반대 말로 작업요청이 순차적으로 실행되는게 아니라 먼저 요청된 작업이 끝나지 않더라도 수행이 되는 병렬적 방식.

비동기 기능 은 오랫동안 운영체제에 존재해왔으며 디스크에서 데이터 읽기와 같이 오래걸릴수 있는 작업을 시작하는데 자주 이용이됨.

비동기 함수는 호출이 되면 실행중인 작업을 시작하기 위해 일부작업을 수행하지만 해당 작업이 실제로 완료되기 전에 반환.

일반적으로 이 작업에는 백그라운드 스레드를 확보하고 해당 스레드에서 원하는 작업을 시작한 다음 작업이 완료되면 호출자에게 알람을 보내는 작업이 포함됨.(일반적으로 이때 콜백함수를 이용함)

 

예전에는 비동기 함수가 존재하지 않으면 자신만의 비동기 함수를 작성해야 했다.

하지만 애플은 GCD(Grand Central Dispatch)라는 기술을 이용하기 시작했다.

이 기술은 자체 응용 프로그램에서 작성하는 스레드 관리 코드를 가져와서  해당 코드를 시스템 수준으로 이동한다.

실행하려는 작업을 정의하고 적절한 Dispatch Queue에 추가만 하면 된다.

GCD는 필요한 스레드를 만들고 해당 스레드에서 실행되도록 작업을 예약합니다.

스레드 관리가 이제 시스템의 일부이기 때문에 GCD는 작업 관리 및 실행에 대한 전체적인 접근 방식을 제공하여 기존 스레드 보다 더 나은 효율성을 제공함.

과거 ->  자신만의 비동기 함수 + 스레드 관리

현재 ->  실행하려는 작업 정의 후 Dispatch Queue에 추가. -> GCD가 알아서 필요한 스레드를 만들고 실행되도록 작업예약

사실 스레드에서 벗어나기라고 해석했지만 스레드를 잘 관리에 더 가까운 듯 하다..

 

Queue들과 비동기  기술들

Dispatch Queue

Dispatch Queue는 작업의 줄로써 작업을 순차적 혹은 비동기적으로 실행하지만 항상 선입선출로 실행한다

(즉, 항상 열에 추가된것과 동일한 순서로 작업을 빼고 시작함)

하지만, Concurrent Dispatch Queue는 이미 시작한 작업이 완료될때 까지 기다리지 않고 가능한 많은 작업을 함(비동기)

장점

1. 간단한 프로그래밍 인터페이스를 제공한다.


2. 자동적이고 전체적인 스레드 풀 관리 기능을 제공합니다.


3. 튜닝된?(조정된) 조립 속도를 제공합니다. (아마 큐안에있는 작업들의 속도를 조정시킨다는 의미일듯..? 아님 ㅈㅅ)


4. 스레드 스택은 응용 프로그램 메모리에 남아 있지 않기 때문에 훨씬 더 메모리 효율적입니다.


5. 부하가 걸린 커널에 트랩하지 않는다.


6. 디스패치 큐에 작업을 비동기식으로 디스패치하면 큐가 교착 상태가 될 수 없습니다.


7. 경합 속에서 원활하게 확장된다. (작업이 겹치더라도 원활하게 작동되도록함)


8. 직렬 디스패치 큐는 잠금 및 기타 동기화 원시 요소에 대한 보다 효율적인 대안을 제공한다.

 

디스패치 큐에 제출하는 작업은 함수 또는 블록객체 내부에 캡슐화 되어야 한다.

고유한 어휘 범위에서 블록을 정의하는 대신 일반적으로 다른 함수 또는 메서드 내부에 블록을 정의하여

해당 함수 또는 메서드에서 다른 변수에 엑세스 할 수 있습니다.

블록은 원래 범위 밖으로 이동하여 힙에 복사할 수도 있다.

이는 디스패치 큐에 블록을 제출할 때 발생.

So.. 비교적 적은 코드로 매우 동적인 작업 구현 가능

*블록객체:함수 포인터와 유사하지만 몇가지 추가 이점이 있는 C언어 기능

Dispatch Source

특정 유형의 시스템 이벤트(low-level)를 비동기적으로 처리하기 위한 C기반 메커니즘.

특정 유형의 시스템 이벤트에 대한 정보를 캡슐화 하고 해당 이벤트가 발생할 때마다 특정 블록 개체 또는 기능을 디스패치 대기열에 제출.

사실 Disptach Source는 GCD 기술의 일부이다.

처리할 수 있는 이벤트들

1. 타이머(Timer)

 

2. Signal handlers

 

3. Descripotr-related events

 

4. 프로세스 관련 이벤트

 

5. mach port event

 

6. trigger하는 사용자 정의 이벤트

OperationQueue

OperationQueue는 디스패치 대기열과 매우 유사하게 작동하는 OBjective-C 개체이다.

실행하려는 작업을 정의한 다음 해당 작업의 예약 및 실행을 처리하는 작업 대기열에 추가함.

GCD와 마찬가지로 작업 대기열은 모든 스레드 관리를 처리하여 시스템에서 작업이 최대한 빠르고 효율적으로 실행되도록 한다.

 하지만, Dispatch Queue는 항상 선입선출로 작업을 실행하는 반면 Operation Queue는 작업의 순서를 정할때 다른 요소를 고려함.

그 요소 중 가장 중요한 것은 종속성(다른 작업의 완료에 의존 여부 - 작업의 순서가 중요하다!)

작업을 정의할 때 종석성을 구성하고 이를 사용해서 작업에 대한 복잡한 실행 순서 그래프를 생성 가능.

 Operation Queue에 제출 해야하는 Task는 NSOperation 클래스의 인스턴스여야한다.

Operation객체는 수행할 작업과 해당 작업을 수행하는데 필요한 데이터를 캡슐화 하는 Object-C개체이다.

NSOperation 클래스는 기본적으로 추상적인 기본클래스이므로 사용자가 지정한 클래스를 정의해서 작업을 수행해야한다.

하지만 우리가 자연스럽게 import하는 Foundation 프레임워크에는 그대로 생성하고 사용할 수 있는 구체적인 하위클래스가 이미 있다.

 

Operation 객체는 진행률을 모니터링 하는데 KVO(Key Value Overvation)라는 유용한 알람을 생성한다.

OperationQueue는 항상 동시에 작업하지만 앞서 말햇던 종속성을 사용하면 필요할때만 연속적으로 실행 될수 있다.

Operation Queue는 우선순위와 준비상태와 준비상태에 따라 정렬된 오퍼레이션 객체를 발생시킨다.

대기열에 작업을 추가하면 작업이 완료 될때 까지 queue에 남아있다.

작업을 추가한 후에는 대기열에서 직접적으로 제거할 수 없다.(Operation  queue는 작업이 완료될때까지 작업을 유지하고 queue그 자체는 operation들이 모두 완료될때 까지 유지가된다. 만약, 완료되지 않은 operation들과 함께 queue를 정지시키면 메모리 릭 가능성 ⬆️)

 

 

비동기식 설계 기법

많은 이점이 있는 비동기식 설계기법 그럼 할수 있다면 무조건 하는것이 좋을까?

아마 대답은 No일것이다.

문서에는 동시성을 지원하도록 코드를 재설계하는 것을 고려하기전에 진짜 필요한지 자문해야한다고 했다.

동시성은 기본 스레드가 사용자 이벤트에 더 빠르게 응답할 수 있고 코어를 더 사용해 같은시간에 코드의 효율성을 더 향상시킬수 있다.

But. 또한 오버헤드가 추가되고 코드의 전반적인 복잡성이 증가해서 디버그가 어려워진다고 했다.

심지어는 잘못 설계하고 수행할 경우 이전보다 느려지고 느리게 응답할 수도 있기 때문에 신중해야한다.

그렇기에 몇확한 답은 없지만 문서에서는 몇가지 지침을 알려주었다. 

 

1. 어플리케이션의 예상 동작 정의

 동시성 추가하기를 생각하기 전에 항상 어플의 정상적인 작동으로 간주 될수 있는 것을 정의해야한다.

(당연히 정상작동을 정의해야 얘가 잘 되고 있는지.. 얼마나 빨라졌는지...등등을 검증이 가능하다)

    - 어플리케이션이 수행하는 작업과 각 작업과 관련된 개체 또는 데이터 구조 열거.

    - 타이머와 기반 작업과 같이 어플리케이션이 사용자 상호 작용없이 수행 할 수 있는 작업 열거.

    -  높은 수준의 작업 목록을 얻은 후에는 각 작업을  성공적으로 완료하기 위해 수행해야 하는 단계 집합으로 더 세분화. 

이 과정에서는 어플리케이션이 전체 상태에 미치는 영향을 주로 관심을 기록해야한다 - 주로 종속성.(개체와 데이터 구조간)

 

2. 실행 가능한 작업 단위 출력

 1번 작업이 끝나면 코드가 어떤 곳에서 동시성을 이용해 이점을 얻을 수 있는지를 확인 할 수 있어야 한다.

작업에서 단계 순서를 바꾸면 결과가 바뀔 경우 해당 단계를 연속적으로 계속 수행해야 할수도 있지만,

순서를 변경해도 출력에 영향을 주지 않는다면 그 단계를 동시에 수행하는 것을 고려해야한다.

두 경우 모두 수행할 단계를 나타내는 실행 가능한 작업 단위를 정의한다.

이작업 단위는 블록이나 Operation 개체를 사용하여 캡슐화 하고 적절한 Queue에 전달하는 것이 된다

 

이렇게 사용자가 식별한 실행 작업단위에 대해 최소한 초기에는 초기에 수행되는 작업의 양에 너무 걱정하지 말라.

스레드를 회전시키는데 항상 비용이 수반되지만  Dispatch Queue와 Operation Queue의 장점중 하나는 대부분 이러한 

비용이 기존 스레드보다 훨씬 적다.

따라서 스레드를 사용하는 것보다 대기열을 사용하여 더 작은 작업 단위를 효율적으로 실행할 수 있다.

물론, 항상 실제 성과를 측정하고 필요에 따라 작업의 크기를 조정해야하지만 적어도 처음에는 어떤 작업도 작다고

생각 하지 말라.

 

3. 필요한 Queue 식별

 이제 작업이 별개의 작업 단위로 분할되고 블록 객체 또는 Operation 개체를 사용하여 캡슐화 했으므로 코드를 실행한 Queue를 

정의해야한다.

 블록을 사용하여 작업을 구현한 경우 직렬 Queue 또는 Concurrent Dispatch Queue에 블록을 추가 할 수 있다.

특정 주문이 필요한 경우 항상 직렬에, 그렇지 않은 경우 Concurrent Dispatch Queue 혹은 다른 Dispatch Queue에 가능하다.

 

 Operation 개체를 이용해서 작업을 구현한 경우 관련 개체간의 종속성을 구성해야한다.

이렇게 구성한 종속성은 종속된 개체가 작업을 완료할 때까지 한 작업이 실행되지 않도록 한다.

 

 

영어도 잘 못하기에 번역기의 힘과 안돌아가는 머리를 짜내 작성해보았습니다.

설명이 정말 쉽지 않게 되어있지만 직접 들어가서 읽어보는 것을 추천드리며..

혹시나 잘못된 부분이나 틀린부분이 있어도 양해 부탁드립니다!

 

https://developer.apple.com/documentation/foundation/operationqueue

https://developer.apple.com/documentation/dispatch/dispatchqueue

https://developer.apple.com/documentation/dispatch/dispatchsource

https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ConcurrencyandApplicationDesign/ConcurrencyandApplicationDesign.html#//apple_ref/doc/uid/TP40008091-CH100-SW1

 

Concurrency and Application Design

Concurrency and Application Design In the early days of computing, the maximum amount of work per unit of time that a computer could perform was determined by the clock speed of the CPU. But as technology advanced and processor designs became more compact,

developer.apple.com

 

'Swift' 카테고리의 다른 글

Generic을 이용한 CompileTime에 Type 검사하기.  (0) 2022.05.12
Memory Leak Case When ReferenceType in Dictionary  (0) 2022.05.11
깊은 복사 & 얕은 복사 (Swift)  (2) 2022.05.08
MetaType_iOS  (0) 2022.05.05
메모리와 Array  (1) 2022.01.12

댓글