티스토리 뷰

반응형

Apple Developer Document - Concurrency and ApplicationDesign

Concurrency and Application Design

컴퓨터가 처음 생겼을 때는 수행할 수 있는 단위 시간당 최대 작업량이 CPU의 클럭 속도에 의해 결정되었다. 이젠 기술이 발전하여 열과 기타 물리적 제약에 의해 프로세서의 최대 클럭 속도가 제한되기 시작했다. 칩 제조회사는 칩의 성능을 향상할 수 있는 다른 방법을 모색했고 각 칩의 프로세서 코어 수를 늘리는 게 그 해결책이었다. 코어 수를 늘리면 단일 칩이 CPU 속도를 높이거나 칩 크기, 열 특성을 변경하지 않아도 초당 더 많은 명령을 실행할 수 있었다. 이렇게 변한 칩들의 늘어난 코어를 어떻게 활용할 수 있을까?

 

여러 코어를 활용하려면 컴퓨터의 여러 작업을 동시에 수행할 수 있는 소프트웨어가 필요하다. OS X, iOS와 같은 멀티 태스킹 운영 체제의 경우 특정 시점에 수백개 이상의 프로그램이 실행될 수 있기 때문에 각기 다른 코어에서 프로그램을 예약할 수 있어야 한다. 보통 이런 프로그램은 실제 처리 시간이 거의 걸리지 않는 백그라운드 프로그램들이기 때문에 실제 프로그램이 추가 코어를 보다 효과적으로 사용하는 방법이 필요하다.

 

프로그램이 여러개의 코어를 사용하는 일반적인 방법은 스레드를 만드는 것이다. 그러나 코어 수가 증가하면 스레드 솔루션에 문제가 발생한다. 가장 큰 문제는 스레드 코드가 임의의 수의 코어로 확장되지 않는다는 점이다. 코어의 수만큼 많은 스레드를 만들 수 없고 프로그램이 잘 작동할지 예상할 수 없다. 이러한 프로그래밍에 필요한 것은 효율적으로 사용할 수 있는 코어의 수이다. 이러한 값은 프로그램이 스스로 계산할 수 없기 때문에 문제가 발생하게 된다. 이것을 알기 위해 많은 노력을 하여 이를 해결하더라도 스레드를 효율적으로 동작시키는 방법, 스레드끼리 방해하지 않는 방법과 같은 어려움도 남아있다.

 

따라서 문제를 요약하면 프로그램이 여러개의 코어를 활용할 수 있는 방법이 필요하단 것이다. 하나의 프로그램이 수행하는 작업도 시스템 상태에 따라 동적으로 확장할 수 있어야 한다. 또한 여러 개의 코어를 사용하는 방법이 간단해야 한다. Apple의 운영체제가 이러한 문제의 해결방법을 제공한다는 것이고 이 글에서는 제공한 기술을 사용하는 방법에 대해 알아보자

The Move Away from Threads

스레드는 오랜기간 사용되어 왔고 사용되고 있지만 여러 작업을 확장 가능하게 실행하는 문제를 해결하지 못한다. 스레드를 사용하면 이러한 확장 가능의 문제를 개발자가 해결해야 하는 부담이 있다. 시스템 상태가 변함에 따라 스레드 수를 동적으로 결정해줘야 한다. 다른 문제는 프로그램이 사용하는 스레드를 생성하고 유지 보수하는 비용을 프로그램 자체적으로 부담한다는 것이다.

 

스레드에 의존하는 대신 OS X, iOS는 동시성 문제를 해결하기 위해 비동기식 디자인 방식을 사용한다. 비동기 기능은 운영 체제에서 오랫동안 존재해왔고 종종 디스크에서 데이터를 읽는 등의 작업과 같이 오랜 시간이 걸릴 수 있는 작업을 시작하는데 사용한다. 비동기 함수가 호출되면 백그라운드에서 작업을 수행하고 작업이 완료되기 전에 반환한다. 일반적으로 이러한 동작은 백그라운드 스레드를 확보하고 해당 스레드에서 작업을 시작하고 작업이 완료되면 호출자에게 알림을 보내는 작업이 포함된다. 예전에는 수행하려는 작업에 비동기 함수가 존재하지 않으면 자체 비동기 함수를 작성하고 자체 스레드를 생성해줘야 했다. 하지만 OS X, iOS는 스레드를 직접 관리하지 않아도 작업을 비동기적으로 수행할 수 있는 기술을 제공한다.

 

비동기식으로 작업을 시작하는 기술 중 하나는 GCD(Grand Central Dispatch)이다. 이 기술은 일반적으로 프로그램이 작성해야하는 스레드 관리 코드를 시스템 수준으로 이동시켰다. 즉 개발자가 해야 할 일은 실행할 작업을 정의하고 적절한 dispatch queue에 추가하기만 하면 된다. 그렇게 하면 GCD는 작업에 필요한 스레드를 생성하고 해당 스레드에서 작업이 실행되도록 예약하게 된다. 시스템이 스레드 관리를 하게 되면서 GCD는 기존의 스레드보다 나은 효율성을 제공한다.

 

Operation Queue는 Dispatch Queue와 매우 비슷한 Objective-C 객체이다. 실행하려는 작업을 정하고 이를 operation queue에 추가하면 해당 작업들을 스케줄링하고 처리해준다. GCD와 비슷하게 operation queue도 모든 스레드 관리를 처리해준다.

Dispatch Queues

Dispatch Queue는 사용자가 만든 작업을 실행하기 위한 C언어 기반 메커니즘이다. Dispatch queue는 작업을 동시에 순차적으로 실행하지만 항상 FIFO(First In First Out)방식으로 실행한다. 즉 dispatch queue는 항상 추가된 순서대로 작업을 실행하고 제거한다. 직렬 dispatch queue는 한 번에 하나의 작업만 실행하고 해당 작업이 끝나서 제거될 때까지 대기한다. 반대로 병렬 dispatch queue는 이미 시작된 작업이 끝날 때까지 기다리지 않고 실행할 수 있는 최대한의 작업을 실행한다.

 

Dispathch queue의 장점은 아래와 같다.

 

  • 직관적이고 간단한 프로그래밍 인터페이스를 제공한다.
  • 전체적인 스레드 관리 기능을 자동으로 제공한다.
  • 어셈블리 수준의 속도를 제공한다.
  • 프로그램 메모리에 스레드 스택이 존재하지 않기 때문에 메모리 효율적이다.
  • 로드중인 커널에 트랩 되지 않는다.
  • 작업을 dispatch queue에 비동기식으로 추가하면 큐를 deadlock 상태로 만들 수 없다.
  • 경쟁에 의해 확장된다.(?)
  • 직렬 dispatch queue는 lock과 동기화에 대한 효율적인 대안을 제공한다.

Dispatch queue에 제출된 작업은 함수 또는 블록 객체 안에서 캡슐화되어야 한다. 블록 객체는 OS X v10.6, iOS 4.0에 도입된 C언어 기능으로 함수 포인터와 비슷하지만 몇 가지 추가적인 장점이 있다. Lexical scope(자체 어휘 범위)에서 블록을 정의하는 대신 다른 함수나 메서드에 블록을 정의하여 해당 함수, 메서드의 다른 변수들에 접근할 수 있다. Dispatch queue에 작업을 제출할 때 블록을 원래 범위 밖으로 이동하여 힙에 복사할 수도 있다. 이러한 것들은 비교적 적은 코드로 많은 작업을 구현할 수 있게 해 준다.

Dispatch queue는 GCD 기술의 일부이며 C 런타임의 일부이다. 프로그램에서의 dispatch queue 사용에 대한 자세한 정보는 Dispatch Queues를 참고하고 블록과 블록의 장점에 대한 내용은 Blocks Programming Topic에서 알아보자

Dispatch Sources

Dispatch source는 시스템 이벤트를 비동기적으로 처리하기 위한 C언어 기반 메커니즘이다. Dispatch source는 특정 유형의 시스템 이벤트에 대한 정보를 캡슐화하고 해당 이벤트가 발생할 때마다 블록 객체 또는 함수를 dispatch queue에 제출한다. Dispatch source를 사용하여 다음 유형의 시스템 이벤트를 모니터링할 수 있다.

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

Dispatch source는 GCD 기술의 일부이다. 이에 대한 더 많은 정보는 Dispatch Sources를 확인하도록 하자.

Operation Queues

Operation queue는 병렬 dispatch queue와 동등한 Cocoa이고 NSOperationQueue 클래스로 구현된다. Dispatch queue는 항상 FIFO로 작업을 실행하지만 Operation queue는 작업의 실행 순서를 결정할 때 다른 요소를 고려한다. 고려하는 요소 중 가장 중요한 것은 수행되려는 작업이 다른 작업의 완료에 연관이 있는지이다. 작업을 정의할 때 종속성을 구성하고 작업 실행 순서 그래프를 만들 수 있다.

 

Operation queue에 제출한 작업은 NSOperation 클래스의 인스턴스여야 한다. 작업을 Objectice-C 객체로 수행하려는 작업과 작업에 필요한 데이터를 캡슐화한다. NSOperation 클래스는 기본적으로 추상 클래스이기 때문에 일반적으로 실행할 작업을 상속받은 클래스로 정의한다. 하지만 Foundation 프레임워크에서는 작업을 만들기 위해 바로 사용할 수 있는 서브 클래스들을 제공한다.

 

작업 객체는 Key-Value Observing(KVO) 알림을 생성하여 작업 진행 상황을 모니터링하는 유용한 방법으로 사용될 수 있다. Operation queue는 항상 작업을 동시에 실행하지만 종속성을 사용할 땐 순서에 맞게 실행할 수도 있다. Operation Queue에 대한 더 많은 정보는 Operation Queues에서 찾아볼 수 있다.

Asynchronous Design Techniques

동시성을 지원하기 위해 코드를 다시 짜는 것을 고려하기 전에 그러한 것이 필요한지에 대해 먼저 생각해봐야 한다. 동시성은 main 스레드가 사용자 이벤트를 자유롭게 응답할 수 있게 하여 코드의 응답성을 향상할 수 있다. 또한 동시에 더 많은 코어를 활용하여 효율성도 향상시킬 수 있다. 그러나 오버 헤드가 추가되고 코드의 복잡성이 증가하여 코드 작성 및 디버깅이 어려워진다.

 

복잡성을 증가시키기 때문에 동시성은 제품 주기가 끝날 때 프로그램에 이식할 수 있는 기능이 아니다. 잘 작동되게 하려면 프로그램에서 수행하는 작업과 자료구조를 잘 만들어야 한다. 잘못 만들게 되면 이전보다도 더 느리게 실행되고 응답성도 떨어질 수 있다. 따라서 설계를 시작할 때 목표를 설정하고 접근 방식에 대해 생각하는 것이 좋다.

 

모든 프로그램마다 요구 사항과 수행하는 작업이 다르기 때문에 다음 섹션에서는 디자인 프로세스 중에 올바른 선택을 할 수 있도록 몇 가지 지침을 제공할 것이다.

Define Your Application's Expected Behavior

프로그램에 동시성을 추가하기 전에 프로그램의 동작에 대해 정의해야 한다. 프로그램의 동작들을 이해할 수 있으면 나중에 디자인을 확인할 수 있다. 또한 이러한 이해는 어떻게 동시성을 도입하여 성능 향상을 할 것인지에 대한 아이디어도 줄 수 있을 것이다.

 

가장 먼저 할 일은 프로그램이 수행하는 작업과 작업과 관련된 객체 혹은 자료 구조를 나열하는 것이다. 작업의 시작을 사용자가 메뉴 아이템이나 버튼을 누르는 것으로 시작할 수 있다. 이러한 작업은 개별 동작을 제공하며 시작, 종료 지점이 명확하다. 또 타이머 기반 작업과 같이 사용자와의 상호작용 없이 작동하는 작업들을 나열해야 한다.

 

이렇게 나열한 high-level tasks가 있으면 각각의 작업에 대해 작업을 성공적으로 완료하기 위해 수행 해야 하는 단계로 세분화한다. 이 단계에서 데이터 구조 및 수정사항이 프로그램 전체에 미치는 영향을 생각해야 한다. 또한 객체와 자료 구조 사이의 종속성에 유의해야한다. 예를 들어, 작업이 객체 배열을 동일하게 변경하는 경우 한 객체의 변경 사항이 다른 객체에도 영향을 끼치는지와 같은 부분을 생각해야한다. 객체가 독립적으로 수정될 수 있다면 그 지점이 객체를 동시에 수정할 수 있는 지점일 수 있다.

Factor Out Executable Units of Work

프로그램 작업에 대한 이해를 통해 어디가 동시성을 사용하면 이득을 얻을 수 있는 지점인지 식별할 수 있어야 한다. 작업에서 순서를 변경하여 결과가 변경된다면 해당 단계를 연속적으로 수행해야 한다. 만약 순서를 변경해도 결과가 변하지 않는다면 해당 단계에 동시성을 도입하는 것이 좋다. 두 단계 모두 실행 가능한 작업 단위로 만들고 적절한 큐로 디스 패치한다.

 

이렇게 식별한 각각의 작업에 대하여 초기에 수행되는 작업량에 너무 걱정할 필요는 없다. 스레드를 spinning up 하는 데에는 항상 비용이 들지만 dispatch queue, operating queue의 장점 중 하나는 이러한 비용이 기존의 스레드보다 훨씬 적다는 것이다. 따라서 큐를 사용하여 더 작은 단위의 작업을 효율적으로 실행할 수 있다.

Identify the Queues You Need

이제 작업이 작은 작업 단위로 분류되었고 이를 캡슐화하였기 때문에 해당 작업을 실행하는 데 사용할 queue를 정의해야 한다. 또한 주어진 작업들을 올바르게 수행할 수 있는 순서를 정해야 한다.

 

블록을 사용하여 작업을 구현한 경우 블럭을 직렬 또는 병렬 dispatch queue에 추가할 수 있다. 특정 주문이 필요할 경우엔 블럭을 직렬 dispatch queue에 추가하고 그렇지 않은 경우에는 병렬 dispatch queue에 추가하거나 여러 다른 종류의 dispatch queue에 넣으면 된다.

작업 객체를 사용하여 작업을 구현한 경우 객체 구성보다 queue를 고르는 게 더 재밌을 수 있다. 실행할 작업들을 연속적으로 수행하려면 작업 간에 종속성을 구성해야 한다. 종속성은 종속된 작업은 해당 작업이 완료되기 전까진 실행이 될 수 없다.

Tips for Imporving Efficiency

코드를 더 작은 작업으로 나누고 queue에 추가하는 것 이외에도 queue를 사용하여 코드의 효율성을 향상하는 방법들이 있다.

 

  • 메모리 사용량이 중요한 경우 작업 내에서 직접 값을 계산하는 것이 좋다. 프로그램이 이미 메모리에 바인딩되어있는 경우 메인 메모리에서 캐시 된 값을 로드하는 것보다 값을 직접 계산하는 것이 더 빠를 수 있다. 계산될 값은 주어진 코어의 레지스터와 캐시를 직접 사용하는데 이는 메인 메모리보다 훨씬 빠르다. 물론 테스트 결과에 의해 성능이 우수하다고 판단되는 경우에만 이러한 작업을 수행하면 된다.
  • 일련의 작업 중 동시에 수행할 수 있는 작업들을 빠르게 식별해야 한다. 만약 작업이 순차적으로 실행되어야 한다면 아키텍처를 변경하여 공유 리소스를 제거해야 한다. 자원이 필요한 각 클라이언트에 대해 사본을 만들거나 모든 자원을 제거하는 것도 고려햐야한다.
  • lock의 사용을 피하는 게 좋다. Dispatch queue, Operation queue에서 제공하는 기능들로 인해 대부분의 경우 lock이 불필요하다. lock을 사용하여 일부 공유 리소스를 보호하는 대신 작업을 올바른 순서로 실행하기 위해 직렬 queue를 사용하거나 종속성을 사용하는 게 좋다.
  • 시스템 프레임워크에 많이 의존하는 게 좋다. 동시성을 달성하는 가장 좋은 방법은 시스템 프레임워크에서 제공하는 내장 동시성을 활용하는 것이다. 많은 프레임워크에서는 내부적으로 스레드 및 기타 기술을 사용하여 동시성을 구현한다. 작업을 정의할 때 기존 프레임워크가 원하는 것을 정확하게 수행하고 동시에 수행하는 함수나 메서드를 정의하는지 확인해야 한다. 해당 API를 사용하면 노력을 줄일 수 있고 동시성도 잘 제공할 가능성이 높다.

Performance Implications

더 많은 코드를 동시에 실행할 수 있도록 operation queue, dispatch queue, dispatch source가 제공된다. 하지만 이러한 것들이 프로그램의 효율성이나 응답성 향상을 보장하는 것은 아니다. 이러한 것들을 잘 사용하는 것은 개발자의 몫이다. 예를 들어 10,000개의 작업을 만들어 operation queue에 추가할 순 있지만 프로그램이 잠재적으로 메모리를 차지하여 페이징 및 성능 저하를 발생할 수도 있다.

코드에 동시성을 도입하기 전에 프로그램의 현재 성능을 반영하는 기준을 수집해야 한다. 도입 후에는 프로그램의 효율성이 향상되었는지 확인해야 한다. 만약 동시성을 도입했는데 효율성이나 응답성이 떨어지면 잠재적인 원인을 찾아야 한다.

이러한 성능 및 성능 측정 도구에 대한 내용은 performance Overview에서 알아볼 수 있다.

Concurrency and Other Technologies

코드를 모듈 식 작업으로 제작하는 것이 프로그램의 동시성을 향상하는 가장 좋은 방법이다. 그러나 이러한 방법은 모든 경우에서 프로그램의 요구를 충족시키지 못할 수 있다. 작업에 따라 프로그램의 전체 동시성을 추가로 개선할 수 있는 다른 방법이 있을 수 있다. 이 섹션에서는 디자인의 일부로 사용하는 다른 기술에 대해 간단히 설명한다.

OpenCL and Concurrency

OS X에서 Open Computing Language(OpenCL)는 컴퓨터의 그래픽 프로세서에서 general-purpose 계산을 하기 위한 표준 기반 기술이다. OpenCL은 대규모 데이터에 적용할 계산이 잘 정의되어있는 경우 사용하기 좋은 기술이다. 예를 들어 OpenCL을 사용하여 이미지의 픽셀에 대한 필터 계산을 수행하거나 이를 사용하여 한 번에 여러 값에 대한 복잡한 계산을 수행할 수 있다. 즉 OpenCL은 데이터를 병렬로 작동할 수 있는 문제에서 보다 적합하다.

 

OpenCL은 대량의 데이터를 병렬 작업으로 수행하는데 적합하지만 일반적인 계산에는 적합하지 않다. GPU가 데이터를 데이터를 준비하고 필요한 작업 커널을 준비하고 그래픽카드로 전송하여 데이터를 작동하게 되는 과정은 많은 노력이 필요하다. 마찬가지로 OpenCL에서 생성된 결과를 검색하는 데에도 많은 노력이 필요하다. 결과적으로 시스템과 상호 작용하는 작업은 일반적으로 OpenCL과 함께 사용하지 않는 것이 좋다. 예를 들어 OpenCL을 사용하여 파일 또는 네트워크 스트림의 데이터를 처리하지 않는다. 대신 OpenCL을 사용하여 수행하는 작업은 그래픽 프로세서로 전송되고 독립적으로 계산될 수 있도록 독립성을 줘야 한다.

 

OpenCL에 대한 더 많은 정보는 OpenCL Programming Guide for Mac에서 찾아볼 수 있다.

When to Use Threads

Operation queue, dispatch queue를 사용하여 작업에 동시성을 부여하는 것이 기본적인 방법이지만 항상 옳은 것은 아니다. 프로그램에 따라 사용자가 직접 스레드를 만들어야 하는 경우도 있다. 이렇게 직접 스레드를 만들 경우 최대한 적은 수의 스레드를 만들도록 노력해야 하고 다른 방법으로는 구현할 수 없는 특정 작업에만 해당 스레드를 사용해야 한다.

 

스레드는 여전히 실시간으로 실행되어야 하는 코드를 구현하는 좋은 방법이다. Dispatch queue는 가능한 한 빨리 작업을 실행하려고 하지만 실시간 제약 조건을 해결하지는 않는다. 백그라운드에서 실행되는 코드가 더 예측 가능하도록 하고 싶다면 스레드가 좋은 대안일 수 있다.

 

다른 스레드 프로그래밍과 마찬가지로 항상 필요한 경우에만 스레드를 사용해야 한다. 항상 필요한 경우 스레드를 사용하기 위한 방법은 Threading Programming Guide를 참고하자.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함