Concurrency Programming Guide

Jake Yeon·2021년 1월 6일
1

ETC

목록 보기
3/3
post-thumbnail

아래의 글은 Apple 공식 문서를 보고 일부분을 정리한 글입니다.
이번 포스팅에서는 Apple에서는 어떠한 방식으로 concurrent programming을 할 수 있도록 기능들을 제공하는지와 이러한 기능들이 생기게 된 이유에 대해서 다루게 됩니다.

❗️추가로 dispatch queue, dispatch source, operation queue에 대한
자세한 내용은 다른 포스팅에서 다룰 예정입니다.

그럼 시작합니다!!

Concurrency and Application Design

기존의 computing에서는 CPU clock의 speed에 의해서 컴퓨터의 단위 시간 당 일의 효율이 정해졌다. 그러나 기술의 발전과 processor들이 작아지면서 발열과 논리적인 제약이 프로세스의 maximun clock speed를 제한하기 시작했다. 그래서 다른 방법을 찾은 것이 각각의 chip에 들어가는 processor core의 수를 증가시키는 방법이었다. core의 수 증가로 인해서 하나의 chip은 CPU speed의 증가나, chip size의 변화, 발열 문제 없이 시간 당 더 많은 instruction을 실행할 수 있었는데, 이러한 core의 증가로 인해서 발생하는 문제는 바로 extra core들을 어떻게 사용해서 이익을 취해야하는가 였다.

multiple core의 이익을 취하기 위해서 컴퓨터는 동시에(simultanesouly) 여러 개의 일을 처리할 수 있는 소프트웨어가 필요했다. OS X, iOS와 같은 multitasking OS들은 주어진 시간 내에 수백 개의 프로그램을 동시에 실행시킬 수 있고 각 프로그램은 서로 다른 core에서 작업할 수 있다. 그러나 대부분의 이러한 프로그램들은 실제 처리 시간을 거의 소모하지 않는 system daemons이거나 background application들이다. 따라서 실제로 진짜 필요한 것은 individual application들이 extra core들을 더욱 효과적으로 사용할 수 있는 방법이다.

전통적으로 multiple core를 사용하는 방법은 multi thread들을 만드는 것이다. 그러나 core의 개수가 증가함에 따라서 thread를 추가하는 해결책에 문제가 발생했다. 가장 큰 문제는 threaded code가 임의적인 core의 개수에 맞게 잘 확장되지 않는다는 것이다. core의 개수만큼 thread를 생성할 수도 없고 그렇다고 프로그램이 잘 돌아갈 것이라고 기대를 할 수 도 없다. 즉, 우리가 알아야하는 것은 효율적으로 사용할 수 있는 core의 개수인데, 이 개수는 app이 스스로 계산을 해야한다. 우리가 이 개수를 정확히 관리한다고 해도 여전히 thread들을 효율적으로 동작하고 서로 다른 thread들끼리 방해하지 않도록 programming을 해주어야한다.

이러한 문제를 요약하자면, app이 다양한 수의 컴퓨터 core들을 활용할 수 있는 방법이 있어야하며, single app에 의해 수행되는 작업의 양은 변화하는 시스템의 조건에 맞게 동적으로 확장될 수 있어야한다. 또한 이러한 core를 활용하는데 필요한 작업량을 늘리지 않도록 solution이 간단해야하는데 Apple은 이러한 솔루션을 가지고 있다. 즉, Apple의 운영체제가 위에서 언급한 모든 문제들에 대한 해결책을 제공한다는 것이다.

The Move Away from Threads

비록 thread는 수년 동안 존재해왔고 그 사용을 계속 하고 있지만, 확장 가능한 방식으로 여러 작업을 실행하는 문제는 해결하지 못했다. 즉, 확장 가능성이 있는 시스템의 경우에는 조건이 변경되면 동적으로 thread의 생성하고 수를 맞춰야하는데 이러한 솔루션을 만들어햐는 부담은 개발자인 우리에게 있었다. 또한 이러한 스레드를 만들고 관리하는 비용들은 app에서 부담을 해야하는 것이 문제이다.

OS X, iOS는 그래서 concurrency 문제를 해결하기 위해서 thread에 의존하기 보다는 asynchronous를 설계했다. asynchronous 기능은 OS에 이미 수십년 전부터 존재했고 disk에서 read(), write()같이 시간이 오래 걸리는 작업을 수행하는데 사용되어 왔다. 이러한 작업들이 호출되면 Asynchronous 함수는 작업을 시작하기 위해 백그라운드에서 일부 작업을 수행하지만 실제로 작업이 완료되기전에 반환된다.(상태를 반환하는것) 일반적으로 이 작업에는 백그라운드 스레드를 가져와 해당 스레드에서 원하는 작업을 시작한 뒤, 작업이 완료되면 호출자에게 callback함수와 같은 알림을 보내는 작업이 포함된다. 과거에는 수행하려는 작업에 대해 asynchronous 함수가 존재하지 않는 경우 우리가 스스로 우리만의 asynchronous 함수와 스레드를 생성해야 했다. 그러나 OS X, iOS는 이제 사용자가 직접 스레드를 관리할 필요 없이 asynchronous으로 작업할 수 있는 기술을 제공한다.

이러한 비동기식 작업을 하는 기술 중 하나는 GCD(Grand Central Dispatch)이다. GCD는 우리가 app에 쓰는 스레드 관리 코드를 시스템 수준까지 가져온다. 따라서 우리가 할 일은 단지 실행시킬 task를 정의하고 적절한 dispatch queue에 넣어주면 된다. GCD는 필요한 스레드를 만들고 해당 스레드에서 실행할 task를 예약하는 작업을 처리한다. 이제 스레드 관리는 시스템의 일부이기 때문에 GCD는 작업 관리 및 실행에 대한 전체적인 접근 방식을 제공하여 기존 thread보다 더 나은 효율성을 제공하게 된다.

Operation Queue는 dispatch queues와 흡사한 Objective-C 객체이다. 실행할 task를 정의한 후 해당 태스크의 예약 및 실행을 처리하는 operation queue에 추가할 수 있다. GCD와 마찬가지로 operation queue는 모든 스레드 관리를 처리하며, 시스템에서 작업이 최대한 빠르고 효율적으로 실행되도록 한다.

Dispatch Queues

Dispatch queue에서 custom tasks들을 실행하는 메카니즘은 C에 기반되어 있다.

  • serial : 한 번에 하나의 task만 실행하고 해당 task가 끝나기 전까지 dequeue를 하는것과 새로운 task를 시작하는것을 기다린다.
  • concurrently : 미리 시작된 task가 끝나는 것을 기다리지 않고 가능한 한 많은 양의 task를 실행한다.

위의 두 가지 상황으로 task들을 처리하지만 항상 FIFO 순을 따른다.

다음과 같은 장점을 가진다.

  • 직관적이며 간단한 프로그래밍 인터페이스를 제공
  • 자동적이고 전반적인 thread pool 관리를 제공
  • 개조된 어셈블리의 속도를 제공 (They provide the speed of tuned assembly.)
  • App 메모리에는 스레드 스택이 남지 않으므로 메모리 효율성이 훨씬 높음
  • 로드 중에 kernel에 trap하지 않는다!
  • task는 asynchronous식으로 전송되기 때문에 deadlock 상태에 빠질 일이 없다.
  • serial dispatch queue는 lock이나 동기화를 보다 효율적인 대안을 제공한다.

Dispatch Sources

dispatch source는 특정 유형의 시스템 이벤트를 비동기적으로 처리하기 위한 C 기반 메커니즘이다. dispatch source는 특정 유형의 시스템 이벤트에 대한 정보를 캡슐화하고 해당 이벤트가 발생할 때마다 specific block object 또는 함수를 dispatch queue에 추가한다. 다음과 같은 시스템 이벤트들을 모니터링할 때 dispatch source를 사용할 수 있다.

  • Timers
  • Signal handlers
  • Descriptor-related events
  • Process-related events
  • Mach port events
  • Custom events that you trigger

Operation Queues

Operation queueconcurrent dispatch queue와 같은 Cocoa equivalent이며, NSOperationQueue class에 의해 구현된다. dispatch queue는 항상 FIFO를 따른다면 operation queue는 task의 실행 순서를 결정할 때 다른 요인을 고려한다. 이러한 요인들 중 가장 중요한 것은 주어진 task가 다른 task의 완료 여부에 달려있느냐 하는 것이다. 즉, dependency가 있는지 말이다. 따라서 task를 정의할 때 dependency를 구성하고, 이러한 dependency를 사용하여 task에 대한 복잡한 실행 순서(로직)를 생성할 수 있다.

operation queue에 추가하는 task들은 NSOperation class의 객체여야만 한다. operation object수행 할 작업과 이를 수행하는데 필요한 데이터를 캡슐화하는 Objective-C object이다. NSOperation class는 기본적으로 추상 기본 클래스이기 때문에 일반적으로 task를 수행할 사용자 정의 하위 클래스를 정의한다. 그러나 Foundation 프레임워크에는 작업을 수행하기 위해 현재 상태로 만들고 사용할 수 있는 몇 가지 구체적인 하위 클래스가 포함되어 있다.

Operation objects KVO notifications을 생성하므로 task의 진행을 모니터링하기에 유용할 수 있다. 비록 operation queue항상 task를 concurrent하게 수행하지만, dependency를 사용해서 필요하면 serial처럼 작업하도록 구현할 수 있다.

Performance Implications

operation queues, dispatch queues 그리고 dispatch sources 들은 concurrently하게 더 많은 코드를 실행할 수 있도록 한다. 그러나 이러한 기술들은 App의 효율성이나 응답성을 향상시킨다는 보장을 주지는 못한다. 여전히 사용자의 필요에 효과적이며 App의 다른 리소스에 과도한 부담을 주지 않는 방식으로 queue들을 사용하는 것은 우리의 책임이다.

예를 들어서 10,000개의 operation objects를 생성하여 operation queue에 추가할 수는 있지만 이렇게 하면 App에서 잠재적으로 중요하지 않은 양의 메모리를 할당하게 되고 이로 인해서 paging이나 성능의 저하가 발생할 수 있는 것이다.

따라서 queues나 thread를 사용하여 코드에 concurrency를 도입하기 전에 항상 App의 현재 성능과 일련의 기준 metric을 수집해야한다. concurrency를 도입해서 변경을 한 뒤에는 추가 metrics을 수집하고 기준선과 비교하여 App의 전반적인 효율성이 향상되었는지 확인해야 한다. 만약 concurrency을 사용하였는데 App의 효율성이 떨어지거나 응답성이 떨어지는 경우에는 사용 가능한 성능 툴들을 사용해서 잠재적인 원인들 확인해봐야한다.
Performance Overview

When to Use Thread

비록 operation queue랑 dispatch queue가 task를 concurrently하게 수행하는데 더 선호되지만 만병통치약은 아니다. App에 따라서 custom thread를 사용해야만 하는 경우가 있을 수 있다. 사용자 정의 thread를 생성해야하는 경우 가능한 적은 수의 thread를 직접 작성하도록 노력해야하며 다른 방법으로 구현할 수 없는 특정 task에만 해당 thread를 사용하도록 해야한다.

Thread는 실시간으로 실행해야하는 코드를 구현하는 좋은 방법이다. 물론 dispatch queue도 작업을 최대한 빨리 실행할 수 있지만 실시간 제약 조건은 해결하지는 못한다. 또한 백그라운드에서 실행되는 코드에서 더욱 예측 가능한 동작이 필요한 경우에도 thread가 더 나은 대안을 제공할 수 있다. 그러나 threaded programming은 반드시 필요한 경우에만 항상 신중하게 사용해야한다.
Threading Programming Guide

참고

  1. Concurrency Programming Guide
profile
Hope to become an iOS Developer

0개의 댓글