지난 포스트들에서 스레드에 대해서 알아보았습니다. 하지만 스레드를 제대로 사용하기 위해서는 POSIX 스레드를 사용해야 한다던가, Core Foundation 프레임워크를 사용해야 하는 등 현재 Swift 자체만으로 사용하기 어려운 부분이 많고, 저수준의 개념이다보니 프로그래머가 신경써야할 부분이 많아 초보자가 쓰기에는 난점이 많았습니다. 그래서 스레드 프로그래밍은 굉장히 어렵고, 코드를 잘못짜면 오히려 성능과 안정성에서 전혀 이득을 보지 못하는 경우도 있었습니다.

애플은 이 문제를 해결하기 위해, 직접 스레드를 만들고 관리하지 않고도 스레드가 실행할 작업만 정의하면 시스템이 이를 알아서 스레드를 만들어 처리하는 방식을 도입하게 됩니다. 이번 포스트에서는 이를 도입하게 된 배경과, 사용법에 대해서 알아보도록 하겠습니다.

이 포스트는 다음 가이드를 참고로 하여 작성되었습니다.

Concurrency Programming Guide

스레드 프로그래밍의 한계와 그 대안

프로세서가 코어 수를 늘리는 방향으로 발전하게 되면서, 이 여러 코어를 활용할 방법으로 등장한 것이 ‘멀티스레드 프로그래밍’입니다. 하지만 이 방식은 계속해서 코어수의 변화에 유연하게 대응하지 못하는 문제를 가지고 있습니다. 설령 이것이 가능하다 하더라도, 여러 스레드가 원활히 돌아가도록 프로그래밍 하는 것 그 자체가 어려운 일이기도 하고요.

이 문제에 대해 Apple은 비동기 디자인 접근 방식을 취합니다. 비동기 디자인은 오랜 시간이 걸리는 작업을 할 때, 자기 자신은 바로 반환하고 실제 작업은 백그라운드에서 수행하도록 하고 나중에 콜백을 통해 결과를 받아보는 방식입니다. 이 과정은 일반적으로 백그라운드 스레드를 사용하게 됩니다. 이 방법은 오래전부터 비동기로 동작하는 함수들을 제공하는 방식으로 널리 쓰였지만, 원하는 작업을 하는 비동기 함수가 없을 경우에는 직접 백그라운드 스레드를 만들고 관리하는 코드를 작성해야 하는 번거로움이 있었습니다. 하지만 지금은 Apple이 백그라운드 스레드 관련코드를 직접 만들지 않고도 비동기 함수를 만드는 방법을 제공하기 때문에 이를 손쉽게 적용할 수 있습니다.

그 중에 하나가 GCD(Grand Central Dispatch)입니다. 이는 스레드 관리 코드를 애플리케이션에서 시스템 쪽으로 가져가는 기술로, 애플리케이션 프로그래머는 단시 실행하고자 하는 작업을 정의하고 이를 적절한 DispatchQueue에 넣기만 하면 됩니다. 그러면 CGD가 필요한 스레드를 만들고 이를 자동으로 스케쥴링 해줍니다. 스레드 관리에 드는 노력을 줄였기 때문에, 기존 스레드 방식에 비해 더 높은 생산성을 가질 수 있게 됩니다.

또 다른 방법으로 OperationQueue가 있습니다. GCD와 굉장히 유사하며, 하고자 하는 작업을 Operation 객체로 만들어 스케쥴링과 실행을 담당하는 OperationQueue에 넣어 실행하는 방식입니다.

DispatchQueue

DispatchQueue는 커스텀 작업을 수행하기 위한 C 기반의 매커니즘입니다. DispatchQueue는 작업을 순차적으로도, 동시에도 실행할 수 있지만 무조건 먼저 들어간 작업은 먼저 실행됨을 보장합니다.(끝나는 시간은 작업에 따라 달라질 수 있겟죠.) 그 외에도 DispatchQueue는 다음과 같은 이점들이 있습니다.

  • 명확하고 쉬운 인터페이스를 제공합니다.

  • 스레드 관리를 자동으로 해줍니다.

  • 어셈블리 레벨에서 튜닝되어 빠른 동작을 제공합니다.

  • 메모리 면에서도 효율적입니다(애플리케이션 메모리를 점유하지 않기 때문입니다.)

  • 작업중에 예외를 발생시키지 않습니다.

  • 비동기적으로 작업을 queue에 넣어도 데드락이 생기기 않습니다.

  • 경쟁 상태(Race Condition)하에서도 우아한 확장성을 제공합니다.

  • Serial DispatchQueue는 lock이나 여러 동기화 연산보다 더 효과적입니다.

DispatchQueue로 들어가는 작업은 반드시 함수나 클로져(Objective-C는 Block) 형태로 제공되어야 합니다.

Dispatch Source

Dispatch Source는 특정 시스템 이벤트들을 비동기적으로 처리하기 위한 매커니즘입니다. DispatchSource는 특정 시스템 이벤트가 발생했을 때 그 정보를 캡슐화하고, 이벤트가 일어날 때마다 특정 함수나 클로저를 DispatchQueue를 통해 실행하도록 해줍니다. 지원하는 시스템 이벤트는 다음과 같습니다.

  • 타이머

  • 시그널 : UNIX의 시그널을 의미합니다. 자세한 정보는 위키백과를 참조해주세요.

  • 파일 디스크립터 관련 이벤트 : 특정 파일에 연관된 핸들러를 지정합니다.

  • 프로세스 관련 이벤트 : 특정 프로세스에 연관된 핸들러를 지정합니다.

  • 마하 포트(Mach Port) 이벤트

  • 그 외 각종 커스텀 이벤트

OperationQueue

OperationQueue는 Concurrent DispatchQueue에 대응하는 Cocoa 프레임워크의 객체입니다. OperationQueue는 DispatchQueue와는 다르게 작업의 실행순서에 다른 요소를 고려합니다. 가장 중요한 요소는 주어진 작업이 다른 작업이 끝나는 것에 의존하는 지의 여부입니다. 프로그래머는 이러한 의존성을 설정하여 복잡한 실행 순서 그래프를 구성할 수도 있습니다.
OperationQueue에 들어가는 작업은 반드시 Operation 객체의 인스턴스여야만 합니다. 이는 수행하고자 하는 작업과 데이터를 캡슐화한 객체입니다. 다만 Operation은 추상 클래스이기 때문에, 시스템이 제공하는 Operation 타입의 구체 클래스를 쓰거나 직접 서브클래싱을 해서 써야합니다. Operation객체는 KVO(Key-Value Observing)을 위한 프로퍼티들을 제공하여 주기 때문에, 이를 통해 작업의 진행상황을 모니터링 할 수 있습니다. 또한 OperationQueue 자체는 여러 작업을 동시에 수행하지만, 의존성을 설정하는 것으로 순차적으로 실행하게 만들 수도 있습니다.

비동기 디자인 테크닉

동시성을 지원하도록 코드를 바꾸기 전에 가장 먼저 해야할 것은, 진짜로 동시성이 필요한지를 생각해보는 것입니다. 동시성은 메인 스레드가 유저 이벤트에 잘 반응할 수 있도록 해주어 반응성을 향상시키기도 하고, 더 많은 코어를 사용하여 작업 효율을 향상 시킬수도 있습니다. 하지만 분명히 추가적인 부담(overhead)가 있고, 코드의 복잡성이 커지는 단점도 있습니다.

그렇기 때문에 동시성이라는 것은 프로젝트 말미에 딱하고 붙일 수 있는 요소가 아닙니다. 제대로 하려면 프로그램이 수행할 작업과 작업에 쓰일 자료 구조를 신경써서 만들어야 하며, 잘못하면 안하니만 못한 결과를 낼 수도 있습니다. 그러므로, 프로그램을 디자인 하기 시작할 때 부터 동시성을 목표로 설정하고 이를 이루기 위한 시도들을 해야 합니다.

애플리케이션마다 요구사항이 다르고 수행하는 작업이 다르기 때문에, 동시성 프로그래밍 디자인에 대한 정답은 없습니다만, 아래의 가이드는 좋은 결정을 내리기 위한 지침이 될 수 있습니다.

  1. 실행 가능한 일의 단위를 뽑아내라
    애플리케이션이 수행해야 할 작업을 이해할 때부터, 동시성을 사용해서 이익을 얻을 수 있는 부분을 알 수 있어야 합니다. 만약 작업의 순서가 바뀌었을 때 결과가 바뀐다면 이는 순차적으로 처리되어야 하고, 순서가 바뀌어도 결과가 바뀌지 않는다면 동시에 처리하는 것을 고려할 수 있습니다. 두 경우 모두 각 작업을 한 개나 여러개의 클로저 혹은 Operation 객체로 캡슐화 후, 적절한 Queue 에 집어넣게 됩니다.

  2. 필요한 Queue를 확인하라
    작업을 캡슐화했다면 이제는 어떤 Queue를 사용할 지를 결정해야 합니다. 이 때 해야할 일은 이 작업이 어떤 순서로 진행되야 올바른 결과를 낼 수 있는 지 증명하는 것입니다.

    클로저를 이용한다면, DispatchQueue를 활용할 것입니다. 만약 순서대로 진행되어야 한다면 Serial Dispatch Queue를 사용해야 할 것이며, 그렇지 않다면 Concurrent Dispatch Queue를 사용해야 합니다.

    Operation 객체를 이용한다면, 당연히 OperationQueue를 이용할 수 밖에 없습니다. 대신 여기서 순차적으로 작업을 실행하고 싶다면, Operation 객체 간의 의존성 설정을 해줘야 합니다. 의존성이 설정되면, 특정 작업에 의존적인 작업은 그 특정 작업이 끝나야지만 실행될 수 있습니다.

  3. 효율성을 높이기 위한 팁

    1. 메모리 접근 빈도가 프로그램 속도에 영향을 미칠정도라면, 저장값이 아니라 계산값을 쓰는 것을 고려해보라
      이미 애플리케이션이 메모리에서 돌고 있다면 계산값이 더 빠를 가능성이 있습니다. 저장값은 메인메모리에서 읽어와야 하지만, 계산 값은 레지스터와 캐시를 사용할 수 있기 때문입니다. 하지만 이는 다양한 변수가 있을 수 있어서, 테스팅을 한 뒤에 결정해야 합니다.

    2. 순차적인 작업을 빠르게 파악하고, 이 작업들을 좀 더 동시적인 작업으로 바꿀 수 있는지 확인해보라
      만약 공유 자원에 의존하고 있기 때문에 순차적으로 작업이 실행되어야 한다면, 이를 제거할 수 있는 아키텍처를 고려해보아야 합니다. 모든 클라이언트가 같은 복사본을 가지고 작업을 하도록 하거나, 아예 이 공유자원을 쓰지 않도록 만드는 것도 생각해 볼 수 있습니다.

    3. lock 사용은 자제하라
      DispatchQueue와 OperationQueue를 이용하면 대부분의 경우는 lock을 필요료 하지 않습니다. lock을 사용해서 공유 자원을 보호하기 보다는, 작업 순서를 지정하는 것이 좋습니다.

    4. 가능하면 시스템이 제공하는 프레임워크를 사용하라
      동시성을 얻는 가장 좋은 방법은 시스템에서 제공하는 프레임워크를 적용하는 것이 좋습니다. 작업을 정의할 때, 이미 같은 일을 비동기적으로 수행하는 함수가 있는지 확인해봅시다. 이러한 API를 이용하면 적은 노력으로도 동시성의 이점을 최대한 누릴 수 있습니다.

성능에 미치는 영향

비록 위의 기술들이 동시성을 가진 코드를 작성하기 쉽게 만들긴 하지만, 이것이 곧 성능 향상이나 반응성 향상을 보장하지는 않습니다. Queue를 효과적으로 사용하는 것과, 애플리케이션에 지나친 부담이 가지 않도록 하는 것은 여전히 프로그래머의 책임입니다. 예를 들어 1만여개의 Operation을 만들어서 하나의 Queue에 넣는다면, 메모리 할당이 지나치게 늘어나서 페이징이 일어나 성능이 저하될 수도 있는 것입니다.

동시성을 코드에 도입하기 이전에, 반드시 애플리케이션의 퍼포먼스에 대한 기준치를 설정해야만 합니다. 그래서 동시성을 도입한 후에 다시 애플리케이션을 검증하여 진짜로 성능향상이 있었는지를 확인해야 합니다. 만약 원하는 만큼 성능이 나오지 않는다면, 다른 퍼포먼스 툴을 사용하여 그 원인을 확인해야 합니다.

다른 기술들

코드를 모듈화하는 것은 동시성을 확보하는 가장 좋은 방법입니다. 하지만 모든 애플리케이션의 모든 경우에 적용하기는 어려울 수 있습니다. 작업의 종류에 따라, 동시성을 얻기 위해 사용할 수 있는 몇가지 기술들이 있습니다.

  1. OpenCL

    macOS에서 OpenCL은 GPU를 범용 목적으로 사용할 수 있게 해주는 표준기술입니다. 만약 거대한 데이터에 적용할 잘 정의된 연산이 있다면 OpenCL은 좋은 선택입니다. 하지만 완전 범용적인 연산에 사용하기에는 적절하지 않습니다. GPU에서 연산을 돌리기 위해 데이터와 작업을 가공하고, GPU로 보내는 것이 결코 간단하지 않기 때문입니다. 또 이렇게 계산된 결과들을 수합하는 것도 결코 쉽지 않고요. 결론적으로, 시스템과 상호작용하면서 이루어지는 작업에 대해서는 OpenCL이 적절하지 않습니다.

  2. 스레드 사용

    Queue계열의 기술들이 괜찮은 방법을 제공해주기는 하지만, 만능은 아닙니다. 때로는 커스텀 스레드를 만들어야 할 수도 있습니다. 이 때 스레드를 만들더라도 이를 최소화하고, 반드시 스레드를 사용해야만 하는 경우에만 제한적으로 사용해야 합니다.

    스레드는 시간이 중요한 작업에서는 여전히 유효합니다. DispatchQueue가 모든 작업을 가능한 빨리 처리하려하지만, 실시간으로 돌아야 한다는 제약은 없기 때문에 이런 요구사항을 만족하기 위해서는 스레드는 여전히 괜찮은 대안이 됩니다.


동시성의 기본적인 개념과 어떤 기술들이 있는지 알아보았습니다. 다음 포스트에서는 애플에서 제공하는 여러 동시성 기술들을 차례대로 알아보겠습니다.