티스토리 뷰

반응형

안녕하세요 Ick입니다!

 

이번 글에서는 iOS에서 동시성 프로그래밍에 사용되는 Dispatch Queue에 대해 알아보려고 합니다.

실제로 사용하는 방법은 여기를 참고해주세요!

참고한 문서는 언제나 그렇듯 공식문서입니다.

 

Apple Developer Document - Dispatch Queue

Dispatch Queues

Grand Central Dispatch(GCD) 디스패치 큐는 작업 수행을 위한 강력한 도구이다. 디스패치 큐를 사용하면 호출자에 대해 비동기적 또는 동기적으로 코드 블록을 실행할 수 있다. 디스패치 큐를 사용하면 별도의 스레드에서 사용한 모든 작업을 수행할 수 있다. 디스패치 큐는 사용하기 쉬우며 스레드 코드보다 작업을 실행하는데 훨씬 효율적이라는 장점이 있다.

 

이번 글에서는 디스패치 큐에 대한 소개와 이를 사용하여 애플리케이션에서 일반적인 작업을 실행하는 방법에 대한 정보를 제공하려고 한다. 만약 기존 스레드 코드를 디스패치 큐로 바꾸려면 Migrating Away from Threads를 참고하기 바란다.

About Dispatch Queues

디스패치 큐는 애플리케이션에서 작업을 비동기식으로 동시에 수행하는 쉬운 방법이다. 여기서 작업이란 단순히 애플리케이션에서 수행해야 하는 작업이라 보면 된다. 함수나 블록 객체 안에 작업할 코드를 배치하고 디스패치 큐에 추가하여 작업을 정의한다.

 

디스패치 큐는 추가된 작업을 관리하는 객체와 같은 구조이다. 모든 디스패치 큐는 선입 선출(FIFO)구조이다. 따라서 큐에 추가하는 작업은 항상 추가된 순서대로 시작된다. GCD는 일부 디스패치 큐를 자동으로 제공하지만 자동으로 제공하지 않는 것들은 특정 용도로 만들 수 있다.

Type Description
Serial Serial 큐 즉 직렬 큐는 큐에 추가된 순서대로 한 번에 하나의 작업을 실행한다. 현재 실행중인 작업은 디스패치 큐에서 관리하는 고유한 스레드에서 실행된다. 직렬 큐는 가끔 특정 리소스에 대한 접근을 동기화하는데 사용한다.
필요한만큼 직렬 큐를 만들 수 있고 각 큐는 다른 큐와 관련하여 동시에 작동한다. 예를 들어 4개의 직렬 큐를 생성하면 각각의 큐는 한 번에 하나의 작업을 실행하지만 결과적으로는 동시에 4개의 작업을 실행하게 된다. 직렬 큐를 만드는 방법은 잠시 후 나올 Creating Serial Dispatch Queue 섹션을 참고하기 바란다.
Concurrent Concurrent 큐 즉 동시큐는 하나 이상의 작업을 추가된 순서대로 실행하지만 하나 이상의 작업을 동시에 실행할 수 있다. 현재 실행중인 작업은 디스패치 큐에서 관리하는 고유한 스레드에서 실행되며 특정 시점에 실행되는 정확한 작업 수는 가변적이며 시스템 조건에 따라 다르다. iOS 5이상에서는 DISPATCH_QUEUE_CONCURRENT를 큐의 타입으로 지정하여 동시 디스패치 큐를 직접 만들 수 있다. 전역 동시 큐를 가져오는 방법에 대한 자세한 내용은 잠시후 나올 Global Concurrent Dispatch Queues 섹션을 참고하기 바란다.
Main dispatch queue 메인 디스패치 큐는 응용 프로그램의 기본 스레드에서 작업을 실행하는 전역적으로 사용가능한 직렬 큐이다. 메인 디스패치 큐는 애플리케이션의 실행 루프와 함께 작동하여 대기중인 작업의 실행과 실행 루프에 연결된 다른 이벤트 소스의 실행을 인터리브한다. 응용 프로그램의 메인 스레드에서 실행되기 때문에 메인 디스패치 큐는 응용 프로그램의 주요 동기화 지점으로 자주 사용된다. 메인 디스패치 큐를 만들 필요는 없지만 애플리케이션이 적절히 사용하는지 확인할 필요는 있다. 메인 디스패치 큐를 관리하는 방법에 대한 자세한 내용은 잠시후 나올 Performing Tasks on the Main Thread 섹션을 참고하기 바란다.

위의 표는 애플리케이션에서 사용할 수 있는 디스패치 큐의 종류와 사용법을 나타낸 표이다.

 

애플리케이션에 동시성을 추가 할 때 디스패치 큐는 스레드를 사용하는 방법에 비해 몇 가지 이점이 있다. 가장 큰 이점은 작업 큐 프로그래밍 모델의 단순성이다. 스레드를 사용하면 수행하려는 작업과 스레드 자체의 생성 및 관리를 위한 코드를 작성해야 한다. 하지만 디스패치 큐를 사용하여 스레드 생성 및 관리에 대한 걱정 없이 실제 수행하려는 작업에 집중할 수 있다. 대신 그러한 고민을 시스템에게 떠넘긴다. 이렇게 떠넘겼을 때의 장점은 시스템이 응용 프로그램보다 훨씬 효율적으로 스레드를 관리할 수 있다는 점이다. 시스템은 사용 가능한 리소스 및 현재 시스템 조건에 따라 동적으로 스레드 수를 확장할 수 있다. 또한 시스템은 일반적으로 스레드를 직접 만든 경우보다 더 빨리 작업을 시작할 수 있다.

 

디스패치 큐에 대한 코드를 작성하는 것이 어려울 것이라고 생각할 수 있는데, 스레드에 대한 코드를 작성하는 것 보다 쉽다. 코드 작성의 핵심은 독립적이고 비동기적으로 실행할 수 있는 작업을 설계하는 것이다. 하지만 스레드 대신 디스패치 큐를 사용했을 때의 장점은 예측 가능성이다. 동일한 공유 리소스에 접근하지만 서로 다른 스레드에서 실행되는 두 작업이 있는 경우, 두 스레드가 모두 리소스에 접근할 수 없도록 하나의 스레드는 Lock상태가 되어야한다. 디스패치 큐를 사용하면 두 작업을 직렬 디스패치 큐에 추가하여 주어진 시간에 하나의 작업만 리소스에 접근하도록 할 수 있다. 이러한 유형의 큐 기반 동기화는 Lock이 항상 경쟁상태, 비경쟁 상태에서 비용이 큰 Kernel Trap을 필요로하는 반면 디스패치 큐는 응용 프로그램의 프로세스 공간에서 작동하며 정말 필요할 때만 커널을 호출하기 때문에 Lock보다 효율적이다.

 

직렬 큐에서 실행되는 두 작업이 동시에 실행되지 않는다는 점을 지적할 수도 있지만 만약 두 개의 스레드가 동시에 Lock상태가 되면 스레드가 제공하는 동시성이 손실될 수 있다는 점도 알아야한다. 더 중요한 것은 스레드 모델이 커널과 사용자 공간 메모리를 모두 차지하는 두 개의 스레드를 생성해야 한다는 것이다. 디스패치 큐는 스레드에 대해 메모리 페널티를 지불하지 않고 사용하는 스레드는 늘 바쁘게 유지되며 Lock상태가 되지 않는다.

 

디스패치 큐에서 기억해야 할 다른 주요 사항은 다음과 같다

  • 디스패치 큐는 다른 디스패치 큐와 함께 작업을 동시에 실행한다. 작업 직렬화는 단일 디스패치 큐의 작업으로 제한된다.
  • 시스템은 한 번에 실행되는 총 작업 수를 결정한다. 따라서 만약 100개의 큐에 100개의 작업이 있더라도 동시에 실행하지 못할 수 있다.
  • 시스템은 시작할 새 작업을 선택할 때 우선순위를 고려한다. 직렬 큐에서 우선순위를 결정하는 방법에 대한 내용은 잠시후 나올 Providing a Clean Up Function For a Queue 섹션을 확인하자
  • 큐에 추가되는 작업은 실행할 준비가 되어 있어야 한다.
  • Private 디스패치 큐는 참조 카운트 객체이다. 자신의 코드에 큐를 유지하는 것 외에도 디스패치 소스를 큐에 연결할 수 있고 유지 횟수를 늘릴 수도 있다. 따라서 모든 디스패치 소스가 취소되고 보유한 호출이 릴리즈 호출과 균형을 이루는지 확인해야 한다. 이에 관한 내용은 Dispatch Source에서 확인할 수 있다.

Queue-Related Technologies

디스패치 큐 외에도 GCD는 큐를 사용하여 코드를 관리하는 여러 기술을 제공한다. 아래 표에서 이러한 기술을 살펴보자.

Technology Description
Dispatch groups 디스패치 그룹은 완료를 위해 블록 객체의 집합을 모니터링하는 방법이다. 그룹은 다른 작업의 완료에 따라 코드가 달라지는 유용한 동기화 메커니즘을 제공한다.
Dispatch semephores 디스패치 세마포어는 기존 세마포어와 유사하지만 일반적으로 더 효율적이다. 디스패치 세마포어는 세마포어를 사용할 수 없어서 스레드를 차단해야 하는 경우에만 커널로 호출한다. 
Dispatch sources 디스패치 소스는 특정한 유형의 시스템 이벤트에 대한 응답으로 알림을 생성한다. 디스패치 소스를 사용하여 프로세스 알림, 신호 및 디스크립터 이벤트와 같은 이벤트를 모니터링할 수 있다. 이벤트가 발생하면 디스패치 소스는 처리를 위해 작업 코드를 지정된 디스패치 큐에 비동기적으로 추가한다. 

Implementing Tasks Using Blocks

Block object는 C, Objective-C, C++ 코드에서 사용할 수 있는 C기반 언어 기능이다. 블록을 사용하면 독립적인 작업 단위를 쉽게 정의할 수 있다. 함수 포인터와 비슷해 보일 수 있지만 블록은 실제로 객체와 유사한 기본 자료 구조로 표현되며 컴파일러에 의해 생성되고 관리된다. 컴파일러는 사용자가 제공한 코드를 패키지화하고 캡슐화해서 힙에 존재하게 한다.

 

블록의 주요 장점 중 하나는 자체 범위 외부에서 변수를 사용할 수 있다는 점이다. 함수 또는 메서드 내부에 블록을 정의하면 블록은 어떤 방식으로든 기존 코드 블록처럼 작동한다. 예를 들어 블록은 상위 범위에 정의된 변수 값을 읽을 수 있고 이렇게 접근한 변수는 나중에 접근 할 수 있도록 힙의 블록 자료 구조에 복사된다. 만약 디스패치 큐에 블록이 추가되면 이러한 값은 일반적으로 읽기 전용으로 유지되어야 한다. 하지만 동기적으로 실행되는 블록은 __block 키워드가 앞에 추가된 변수를 사용하여 부모의 호출 범위로 다시 반환 될 수 있다.

 

함수 포인터에 사용되는 구문과 유사한 구문을 사용하여 코드와 함께 블록을 인라인으로 선언한다. 블록과 함수 포인터의 차이점은 블록은 이름 앞에 * 대신 ^를 사용한다는 점이다.

int x = 123;
int y = 456;

// Block declaration and assignment
void (^aBlock)(int) = ^(int z) {
    printf("%d %d %d\n", x, y, z);
};

// Execute the block
aBlock(789);   // prints: 123 456 789

위의 코드는 블록을 동기적으로 선언하고 실행하는 방법을 보여주는 코드이다.

 

다음은 블록을 설계할 때 고려해야 할 몇가지 지침을 요약한 것이다.

  • 디스패치 큐를 사용해서 비동기적으로 수행하려는 블록의 경우 부모 함수 또는 메서드에서 스칼라 변수를 캡처하여 블록에서 사용하는 것이 안전하다. 그러나 호출 컨텍스트에 의해 할당 및 삭제된 구조체나 기타 포인터 기반 변수를 캡처하려고 시도하면 안 된다. 블록이 실행될 때까지 해당 포인터가 참조하는 메모리가 사라질 수 있다. 물론 메모리를 직접 할당하고 해당 메모리의 소유권을 블록에 명시적으로 넘기는 것이 안전하다.
  • 디스패치 큐는 추가된 블록을 복사하고 실행이 완료되면 블록을 해제한다. 즉, 큐에 블록을 추가하기 전에 명시적으로 블록을 복사할 필요는 없다.
  • 큐가 작은 작업을 실행할 때는 원시 스레드 보다 효율적이지만 블록을 만들고 큐에서 실행하는 데에는 오버헤드가 발생한다. 블록이 너무 작은 작업을 수행하면 큐로 보내는 것 보다 인라인으로 실행하는 게 더 빠를 수 있다.
  • 기본 스레드와 관련된 데이터를 캐시하지 말고 다른 블록에서 데이터에 액세스 할 수 있을 것으로 예상해야 한다. 동일한 큐이 작업이 데이터를 공유해야 하는 경우 디스패치 큐의 컨텍스트 포인터를 사용하여 데이터를 대신 저장해야 한다.
  • 블록이 Objective-C 객체를 몇 개 이상 생성하는 경우 코드의 일부를 @autoerlease블록으로 묶어 해당 객체에 대한 메모리 관리를 처리할 수 있다. GCD 디스패치 큐에는 자체적으로 자동 해제 풀이 있지만 해당 풀이 언제 배출될지는 보장되지 않는다. 애플리케이션에 메모리가 제한된 경우 자체적으로 자동 해체 풀을 생성하면 보다 정기적인 간격으로 객체의 메모리를 확보할 수 있다.

Creating and Managing Dispatch Queues

작업을 큐에 추가하기 전에 사용할 큐의 종류와 어떻게 사용할 것인지를 결정해야 한다. 여기서 큐의 종류란 직렬, 병렬을 뜻하며 어떻게 사용할 것인지는 특정 용도로 사용할 경우에 고려해야 한다. 이번 장에서는 디스패치 큐를 만들고 사용하도록 구성하는 방법을 알아보자

Getting the Global Concurrent Dispatch Queues

Concurrent 디스패치 큐는 여러 개의 작업을 병렬로 실행할 수 있을 때 유용하다. 동시 큐는 이전 작업이 완료되기 전에 큐에서 다음 작업을 빼서 동시에 실행할 수 있다. 이렇게 실행하는 작업의 수는 가변적이며 조건이 변경되면 동적으로 바뀔 수 있다. 동시에 실행하는 작업의 수에 영향을 끼치는 것은 사용 가능한 코어 수, 다른 프로세스가 수행하는 작업량, 우선순위 등이 있다.

 

시스템은 각각의 애플리케이션에 4개의 동시 디스패치 큐를 제공한다. 이러한 큐는 응용 프로그램 전체에 적용되며 우선순위에 의해서 구분된다.

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

위의 코드처럼 dispatch_get_global_queue함수를 사용하여 하나의 큐를 요청할 수 있다. 이 방법으로 동시 큐를 가져오는 것 외에도 DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_PRIORITY_LOW 상수를 함수에 전달하여 우선 순위에 따라 큐를 가져오거나 DISPATCH_QUEUE_PRIORITY_BACKGROUND 상수를 전달하여 백그라운드 큐를 가져올 수도 있다. 디스패치 큐를 사용할 땐 따로 큐에 대한 참조를 저장할 필요는 없고 필요할 때마다 dispatch_get_global_queue함수로 가져오면 된다.

Creating Serial Dispatch Queues

직렬 큐는 작업을 순차적으로 실행하려는 경우에 유용하다. 직렬 큐는 한 번에 하나의 작업만 실행하며 가장 먼저 추가된 작업부터 실행한다. 공유 리소스, 변경 가능한 자료 구조를 보호하기 위해 Lock대신 직렬 큐를 사용할 수 있다. Lock과 달리 직렬 큐는 작업이 예측 가능한 순서로 실행되며 교착 상태에 빠지지 않는다.

 

사용자를 위해 생성되는 동시 큐와는 다르게 직렬 큐는 명시적으로 생성하고 관리해야 한다. 애플리케이션에 대해 여러 개의 직렬 큐를 만들 수 있지만 동시에 많은 작업을 실행하려는 의도로 많은 직렬 큐를 만들지는 않아야 한다. 많은 수의 작업을 동시에 실행하려면 동시 큐 중 하나에 작업을 추가하는 것이 낫다.

dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);

위의 코드는 직렬 큐를 만드는데 필요한 단계이다. dispatch_queue_create 함수는 매개변수로 큐의 이름과 큐의 속성 집합을 받는다. 큐의 속성은 향후 사용을 위해 예약되어 있으며 NULL이어야 한다. 사용자가 만든 큐 외에도 시스템은 자동으로 직렬 큐를 생성하여 애플리케이션의 메인 스레드에 바인딩한다. 이에 관한 내용은 바로 다음 섹션인 Getting Common Queues at Runtime에서 살펴보자.

Getting Common Queues at Runtime

Grand Central Dipatch는 애플리케이션에서 몇 가지 일반적인 디스패치 큐에 접근할 수 있는 기능을 제공한다.

  • 디버깅 목적으로 현재 큐의 ID를 테스트하려면 dispatch_get_current_queue함수를 사용하면 된다. 블록 객체 내부에서 함수를 호출하면 블록을 실행하는 큐가 반환된다. 블록 외부에서 호출을 사용하면 애플리케이션의 기본 동시 큐가 반환된다.
  • dispatch_get_main_queue함수를 사용하여 애플리케이션의 기본 스레드와 연결된 직렬 디스패치 큐를 가져온다. 이 큐는 Cocoa 애플리케이션과 dispatch_main함수를 호출하거나 메인 스레드에서 런 루프를 구성하는 애플리케이션을 위해 자동으로 생성된다.
  • dispatch_get_global_queue 함수를 사용하여 공유 중인 동시 큐를 가져온다.

Memory Management for Dispatch Queues

디스패치 큐 및 디스패치 객체들은 참조 카운트 데이터 타입이다. 직렬 디스패치 큐를 생성할 때 초기 참조 카운트는 1이다. dispatch_return,dispatch_release함수를 사용하여 참조 카운트를 증가, 감소시킬 수 있다. 큐의 참조수가 0이 되면 시스템은 큐를 비동기적으로 할당 해제한다.

 

디스패치 객체를 유지하고 해제하여 사용하는 동안 메모리에 남아 있는지 확인하는 것이 중요하다. 메모리 관리 Cocoa 객체와 마찬가지로 큐를 사용하기 전에 유지하고 더 이상 필요하지 않다면 해제해야 한다. 이러한 기본 패턴은 큐를 사용하는 동안에는 메모리에 남아있도록 한다.

동시 디스패치 큐나 기본 디스패치 큐를 포함하여 글로벌 디스패치 큐는 유지하거나 해제할 필요가 없다. 만약 이런 시도가 보인다면 알아서 무시된다.

 

가비지 컬렉터 애플리케이션을 구현하더라도 디스패치 큐, 디스패치 객체를 유지하고 해제해야 한다. GCD는 메모리 회수를 위한 가비지 컬렉션 모델을 지원하지 않는다.

Storing Custom Context Information with a Queue

모든 디스패치 객체를 사용하면 사용자 정의 컨텍스트 데이터를 객체와 연결할 수 있다. 주어진 객체에서 컨텍스트 데이터를 가져오려면 dispatch_set_context, dispatch_get_context 함수를 사용하면 된다. 시스템은 사용자 지정 데이터를 사용하지 않으며 적절한 시간에 데이터를 할당하고 해제하는 것도 사용자에게 달려있다.

 

큐의 경우 컨텍스트 데이터를 사용하여 Objective-C 객체 또는 큐나 코드의 용도를 식별하는데 도운이 되는 자료 구조에 대한 포인터를 저장할 수 있다. 큐의 finalizer함수를 사용하여 할당 해제되기 전에 큐에 컨텍스트 데이터를 할당 해제할 수 있다.

Providing a Clean Up Function For a Queue

직렬 디스패치 큐를 생성한 뒤 finalizer함수를 연결하여 큐의 할당이 취소될 때마다 정리 작업을 수행할 수 있다. 디스패치 큐는 참조 카운트 객체이며 dispatch_set_finalizer_f함수를 사용하여 큐의 참조 카운트가 0에 도달할 때 실행할 함수를 지정할 수 있다. 이 함수를 사용하여 큐와 연관된 컨텍스트 데이터를 정리하고 컨텍스트 포인터가 NULL이 아닌 경우에만 함수가 호출된다.

void myFinalizerFunction(void *context)
{
    MyDataContext* theData = (MyDataContext*)context;

    // Clean up the contents of the structure
    myCleanUpDataContextFunction(theData);

    // Now release the structure itself.
    free(theData);
}

dispatch_queue_t createMyQueue()
{
    MyDataContext*  data = (MyDataContext*) malloc(sizeof(MyDataContext));
    myInitializeDataContextFunction(data);

    // Create the queue and set the context data.
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
    dispatch_set_context(serialQueue, data);
    dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);

    return serialQueue;
}

위의 코드는 사용자 지정 finalizer함수와 큐를 만들고 해당 함수를 연결하는 코드이다. 큐는 finalizer함수를 사용하여 큐의 컨텍스트 포인터에 저장된 데이터를 해제한다.

 

Adding Tasks to a Queue

작업을 실행하려면 작업을 적절한 디스패치 큐에 디스패치 해야 한다. 작업을 동기식, 비동기식으로 디스패치 할 수 있고 단일 또는 그룹으로 디스패치 할 수 있다. 큐에 있으면 큐는 제약 조건과 열에 이미 있는 작업들을 고려하여 가능한 한 빨리 작업을 실행해야 한다. 이번 섹션에서는 작업을 큐에 디스 패치하는 몇 가지 방법과 각 방법의 장점에 대해 알아보자.

 

Adding a Single Task to a Queue

작업을 큐에 추가하는 방법에는 비동기식, 동기식의 두 가지 방법이 있다. 가능하면 dispatch_async, dispatch_async_f함수를 사용하는 비 동기 실행이 동기식 실행보다 선호된다. 블록 객체 또는 함수가 큐에 추가되면 해당 코드가 언제 실행될지 알 수 없다. 결과적으로 블록이나 함수를 비동기적으로 추가하면 코드 실행을 예약하고 호출 스레드에서 다른 작업을 계속할 수 있다. 이는 사용자 이벤트에 대한 응답으로 애플리케이션의 메인 스레드에 작업을 예약하는 경우 특히 중요하다.

 

가능할 때마다 작업을 비동기적으로 추가해야 하지만 경쟁 조건이나 기타 동기화 오류를 방지하기 위해 작업을 동기적으로 추가해야 하는 경우가 있을 수 있다. 이러한 경우 dispatch_sync, dispatch_sync_f 함수를 사용하여 작업을 큐에 추가할 수 있다. 이러한 함수는 지정된 작업이 실행을 완료할 때까지 현재 실행 스레드를 차단한다.

 

동일한 큐에서 실행 중인 작업에서 dispatch_sync, dispatch_sync_f 함수를 호출하면 안 된다. 이는 교착 상태가 보장되는 직렬 큐에서 특히 중요하고 동시 큐에서도 피하는 것이 좋다.

dispatch_queue_t myCustomQueue;
myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL);

dispatch_async(myCustomQueue, ^{
    printf("Do some work here.\n");
});

printf("The first block may or may not have run.\n");

dispatch_sync(myCustomQueue, ^{
    printf("Do some more work here.\n");
});
printf("Both blocks have completed.\n");

위의 코드는 작업을 비동기, 동기식으로 디스패치 하기 위해 블록 기반 변형을 사용하는 방법을 나타낸 코드이다.

 

Performing a Completion Block When a Task Is Done

큐에 디스 패치된 작업은 작업을 생성한 코드와 관계없이 실행된다. 이럴 때 작업이 완료되면 결과를 통합할 수 있도록 응용 프로그램에 해당 사실을 알려야 하는 경우가 있다. 전통적인 비동기 프로그래밍에서는 콜백 메커니즘을 사용하여 이를 수행할 수 있지만 디스패치 큐에서는 완료 블록을 사용할 수 있다.

 

완료 블록은 원래 작업이 끝날 때 큐에 전달하는 또 다른 코드이다. 일반적으로 호출 코드는 작업을 시작할 때 완료 블록을 매개 변수로 제공한다. 작업 코드는 작업이 완료될 때 지정된 블록, 함수를 지정된 큐에 제출하기만 하면 된다.

void average_async(int *data, size_t len,
   dispatch_queue_t queue, void (^block)(int))
{
   // Retain the queue provided by the user to make
   // sure it does not disappear before the completion
   // block can be called.
   dispatch_retain(queue);

   // Do the work on the default concurrent queue and then
   // call the user-provided block with the results.
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      int avg = average(data, len);
      dispatch_async(queue, ^{ block(avg);});

      // Release the user-provided queue when done
      dispatch_release(queue);
   });
}

위의 코드는 블록을 사용하여 구현된 평균을 계산하는 함수이다. 위에 정의된 함수의 마지막 두 매개변수를 사용하면 호출자가 결과를 보고할 때 사용할 큐와 블록을 지정할 수 있다. 함수는 결과를 계산한 뒤 결과를 지정된 블록으로 전달하고 큐로 보낸다. 이런 과정에서 큐가 조기에 해제되는 것을 방지하기 위해 큐를 완료 블록이 발송된 후에 해제하는 것이 중요하다.

 

Performing Loop Iterations Concurrently

동시 디스패치 큐가 성능을 향상할 수 있는 곳은 고정된 횟수로 반복하는 루프가 잇는 곳이다.

for (i = 0; i < count; i++) {
   printf("%u\n",i);
}

위와 같은 루프가 있다고 가정하자. 각 반복이 수행 중 다른 모든 반복에 수행할 작업과 독립적이며 순서가 중요하지 않은 경우 루프를 dispatch_apply, dispatch_apply_f 함수에 대한 호출로 대체할 수 있다. 이러한 함수는 루프 반복마다 한 번씩 지정된 블록 또는 함수를 큐에 추가한다. 따라서 동시 큐로 디스 패치되면 동시에 여러 개의 루프 반복을 수행할 수 있다.

 

dispatch_apply,dispatch_apply_f를 호출할 때 직렬 큐와 동시 큐 중 어느 것에 추가할지 고를 수 있다. 동시 큐에 추가하면 루프 반복을 동시에 수행할 수 있고 이렇게 사용하는 것이 일반적이다. 직렬 큐를 사용할 수도 있지만 아무런 이점은 없다.

 

일반적인 for 루프와 마찬가지로 dispatch_apply,dispatch_apply_f함수는 루프가 완료될 때까지 반환되지 않는다. 따라서 큐의 컨텍스트에서 이미 실행 중인 코드에서 이 함수들을 호출할 때 주의해야 한다. 함수에 매개변수로 전달된 큐가 직렬큐이고 현재 코드를 실행하는 큐와 동일하는 경우 큐가 교착상태에 빠지게된다. 또한 현재 스레드를 효과적으로 차단하기 때문에 메인 스레드에서 호출할 때도 주의해야한다. 루프가 실행되는 동안 응답을 하지 못할 수 있기 때문에 루프를 실행하는데 필요한 시간이 길 경우 다른 스레드에서 이 함수들을 호출해야 한다.

 

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {
   printf("%u\n",i);
});

위의 코드는 for 루프를 dispatch_apply 구문으로 대체하는 방법을 보여준다. dispatch_apply에 전달하는 블록에는 현재 루프 반복을 식별하는 매개변수가 포함되어야 한다. 예를 들어 처음 실행되면 0, 한 번 반복 후엔 1이 된다.

 

이렇게 루프를 실행하면 큐로 진행하기 때문에 오버헤드가 발생할 수 있다. 이러한 오버헤드로 인해 그냥 루프를 실행했을 때보다 비용이 더 클 수 있다. 이럴 때 striding을 사용하여 루프 반복중 수행하는 작업의 양을 늘릴 수 있다. 예를 들어 100번의 작업을 반복해야 할 때 100번의 작업을 매번 큐에 추가하는 것보다 stride를 10으로 설정하면 10번의 작업을 묶어 진행할 수 있다. 즉 10번의 작업을 큐에 추가하는 것과 동일한 오버헤드가 발생한다.

Performing Tasks on the Main Thread

GCD는 애플리케이션의 메인 스레드에서 작업을 실행하는 데 사용할 수 있는 특수한 디스패치 큐를 제공한다. 이 큐는 모든 애플리케이션에 자동으로 제공되며 메인 스레드에서 런 루프를 설정하는 모든 애플리케이션에 의해 자동으로 드레인 된다. Cocoa 애플리케이션을 만들지 않고 런 루프를 명시적으로 설정하지 않으려면 dispatch_main함수를 호출하여 메인 디스패치 큐를 명시적으로 비워야 한다. 작업은 여전히 큐에 추가할 수 있지만 이 함수를 호출하지 않으면 작업이 실행되지는 않는다.

 

dispatch_get_main_queue함수를 호출하여 애플리케이션의 메인 스레드에 대한 디스패치 큐를 가져올 수 있다. 이 큐에 추가된 작업은 메인 스레드 자체에서 순차적으로 실행된다. 따라서 이 큐를 애플리케이션의 다른 부분에서 수행 중인 작업의 동기화 지점으로 사용할 수 있다.

Using Objective-C Objects in Your Tasks

GCD는 Cocoa 메모리 관리 기술에 대한 내장 지원을 제공하므로 디스패치 큐에 제출하는 블록에서 Objective-C 객체를 자유롭게 사용할 수 있다. 각 디스패치 큐는 autorelease pool을 유지하여 자동 해제된 객체가 특정 시점에 해제되도록 한다. 실제로 큐는 객체를 해제하는 시기를 보장하진 않는다.

 

프로그램에 메모리가 제한되어 있고 블록이 자동 해제된 객체를 몇 개 이상 생성하는 경우 자체적으로 자동 해체 풀을 만드는 것이 객체를 적시에 해제되도록 하는 유일한 방법이다. 블록이 수백 개의 객체를 생성하는 경우 둘 이상의 자동 해제 풀을 생성하거나 일정한 간격으로 풀을 비울 수 있다.

Suspending and Resuming Queues

큐를 일시적으로 중단하여 블록 객체를 실행하지 못하도록 할 수 있다. dispatch_suspend함수를 사용하여 디스패치 큐를 일시 정지하고 dispatch_resume함수로 다시 작동할 수 있다. dispatch_suspend 함수를 호출하면 큐의 일시 정지 참조수가 증가하고 dispatch_resume함수를 호출하면 참조 수가 감소한다. 참조 횟수가 0보다 크면 큐는 일시 정지된 상태로 유지된다.

 

일시 정지 및 다시 작동하는 호출은 비동기식이며 블록 실행 사이에서만 적용된다. 큐를 일시 정지한다고 해서 이미 실행 중인 블록이 중지되는 것은 아니다.

Using Dispatch Semaphores to Regulate the Use of Finite Resources

디스패치 큐에 추가할 작업이 일부 리소스에 접근하는 경우 디스패치 세마포어를 사용하여 해당 리소스에 동시에 접근할 수 있는 작업 수를 조절할 수 있다. 디스패치 세마포어는 일반적인 세마포어와 한 가지 예외를 제외하면 동일하게 작동하며 시간이 덜 걸린다. 이는 특정 경우에 GCD가 커널을 호출하지 않기 때문인데, GCD가 커널을 호출하는 유일한 시간은 리소스를 사용할 수 없고 세마포어가 신호를 받을 때까지 시스템이 스레드를 파킹 해야 할 때이다.

 

디스패치 세마포어를 사용하는 절차는 다음과 같다.

  1. 세마포어를 만들 때 사용 가능한 리소스의 수를 정한다.
  2. 작업에서 dispatch_semaphore_wait함수를 호출하여 세마포어를 대기한다.
  3. wait 호출이 반환될 때 리소스 접근이 허용되고 작업을 수행한다.
  4. 리소스를 사용하는 작업이 끝날 경우 dispatch_semaphore_signal함수를 호출하여 리소스 접근을 해제하고 세마포어에 신호를 보낸다.
// Create the semaphore, specifying the initial pool size
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);

// Wait for a free file descriptor
dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);

// Release the file descriptor when done
close(fd);
dispatch_semaphore_signal(fd_sema);

위의 코드는 세마포어를 사용하여 파일 처리 코드에서 한 번에 사용 중인 파일 설명자의 수를 제한하는 코드이다.

Waiting on Groups of Queued Tasks

디스패치 그룹은 하나 이상의 작업 실행이 완료될 때까지 스레드를 차단하는 방법이다. 지정된 모든 작업이 완료될 때까지 실행되면 안 되는 작업이 있을 때 이러한 방법을 사용할 수 있다. 디스패치 그룹을 사용하는 또 다른 이유는 스레드 조인의 대안이기 때문이다. 여러 하위 스레드를 시작한 뒤 각각에 조인하는 대신 작업들을 디스패치 그룹에 추가하고 전체 그룹을 기다릴 수 있다.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

// Add a task to the group
dispatch_group_async(group, queue, ^{
   // Some asynchronous work
});

// Do some other work while the tasks execute.

// When you cannot make any more forward progress,
// wait on the group to block the current thread.
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// Release the group when it is no longer needed.
dispatch_release(group);

위의 코드는 그룹 설정, 작업 추가, 결과를 기다리는 과정을 나타낸 코드이다. dispatch_async함수를 사용하여 작업을 큐에 디스 패치하는 대신 dispatch_group_async 함수를 대신 사용한다. 이 함수는 작업을 그룹과 연결하고 실행을 위해 큐에 넣는다. 작업 그룹이 완료될 때까지 기다리려면 dispatch_group_wait함수를 사용하여 적절한 그룹을 기다릴 수 있다.

Dispatch Queues and Thread Safety

디스패치 큐에서 스레드 안정성은 여전히 관련된 주제이다. 애플리케이션에서 동시성을 구현할 때 알아야 할 몇 가지 사항을 알아보자

  • 디스패치 큐 자체는 스레드로부터 안전하다. 즉 lock을 설정하거나 큐에 대한 접근을 동기화하지 않아도 시스템의 모든 스레드에서 작업을 디스패치 큐에 추가할 수 있다.
  • 함수 호출을 전달하는 것과 동일한 큐에서 실행 중인 작업에서 dispatch_sync함수를 호출하면 안 된다. 사용하는 경우 큐가 교착 상태에 빠질 수 있다. 현재 큐로 디스 패치해야 하는 경우 dispatch_async함수를 사용하여 비동기적으로 수행해야 한다.
  • 디스패치 큐에 추가하는 작업에 Lock을 사용하면 안 된다. 작업에서 Lock을 사용하는 것은 안전하지만 직렬 큐를 완전히 차단할 위험이 있다. 동시 큐에서는 Lock으로 인해 다른 작업이 실행되지 않을 수 있다. 코드의 일부를 동기화해야 하는 경우 Lock대신 직렬 디스패치 큐를 사용하자.
  • 작업을 실행하는 메인 스레드에 대한 정보를 얻을 수 있지만 그렇게 하지 않는 것이 좋다. 디스패치 큐와 스레드에 호환성에 대한 내용은 Compatibility with POSIX Threads를 참고하자.

구현해 둔 스레드를 사용하는 코드를 디스패치 큐를 사용하는 코드로 바꾸는 방법은 Migrating Away from Threads를 참고하자.

 

실제로 사용하는 방법은 여기를 참고해주세요!

감사합니다!

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함