GCD 튜토리얼

지난 포스트에서 블록(block)의 동작에 대해서 분석해보았는데, 이런 블록들을 이용하여 멀티스레드 프로그래밍을 손쉽게 할 수 있는 방법을 제공하는 GCD (Grand Central Dispatch) 에대해 알아보도록 하자. 본 포스트의 내용은 1 의 내용을 요약하면서 추가적으로 유용한 내용들을 GCD 애플 문서2에서 발췌하여 정리한 내용임을 밝힌다.

디스패치 큐의 종류

  • 컨커런트 디스패치 큐 (concurrent dispatch queue) : 해당 큐 내의 작업들간에 실행 순서는 보장할 수 없다.

      dispatch_queue_t queue = dispatch_queue_create(“com.letmecompile.concurrentQueue”, DISPATCH_QUEUE_CONCURRENT);
  • 시리얼 디스패치 큐 (serial dispatch queue): 해당 큐 내의 작업들은 큐에 추가된 순서로 하나씩 수행됨을 보장한다. 때문에 시리얼큐는 하나 생성될때마다 스레드가 하나 더 생기기때문에 과도하게 많은숫자를 하면 성능에 문제가 있을 수 있다.

      dispatch_queue_t queue = dispatch_queue_create(“com.letmecompile.serialQueue”, NULL);

OS에서 제공되는 디스패치 큐의 종류

  • dispatch_get_main_queue(), 시리얼 큐, 메인스레드에서 실행
  • dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) , 컨커런트 큐, 중요도에 따라 high, default, low, background로 나뉘며 서브스레드에서 실행

디스패치 큐의 릴리즈 시점

OS에서 제공하는 디스패치 큐를 사용하지 않고 직접 만들어 사용하는경우 큐를 create 했으면 release 해주어야한다. 하지만 다음과 같이 async를 사용할경우 블록의 실행이끝나기 전에 myQueue를 릴리즈하게되는 형태가 된다. 이 경우에는 문제가 생길까?

dispatch_queue_t myQueue = dispatch_queue_create();
dispatch_async(myQueue, ^{NSLog(@“Will this be okay after myQueue is released?"});
dispatch_release(myQueue);

다행히도 async 함수에의해 블록이 추가될때, 해당 블록이 해당 큐에대해 소유권을 갖게되기 때문에(레퍼런스카운트가 하나 증가) 문제가 생기지 않는다. 따라서 async를 사용하는경우에도 특별히 신경써줄 필요는 없다.

디스패치 시간 조절

특정시간만큼 딜레이를 준 후에 블락을 실행하고 싶을경우 dispatch_after를 사용한다.

dispatch_time_t tm = dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC);
dispatch_after( tm, queue, ^{});

디스패치 그룹 (dispatch group)

여러 디스패치 작업들간에 그룹을 만들어 실행 순서 조절하는 것이 가능하다.
다음 소스코드와 주석을 살펴보자.

dispatch_group_t group = dispatch_group_create();

// 그룹내 작업은 해당 큐의 특성에따라 실행순서가 바뀔 수 있음
dispatch_group_async(group, anyQueue, ^{ NSLog(@“block 1");} );
dispatch_group_async(group, anyQueue, ^{ NSLog(@“block 2");} );
dispatch_group_async(group, anyQueue, ^{ NSLog(@“block 3");} );

// 해당 그룹의 모든 작업들이 끝난후에 메인큐에서 다음 작업을 실행
dispatch_group_notify(group, dispatch_get_main_queue(), @{ NSLog(@“Called after group jobs are done");});

// notify 대신 wait을 실행할 수도있는데 이경우에는 "설정된 시간이 지나기 전까지” 혹은 “그룹내 작업들이 모두 끝날때까지" 현재 스레드를 blocking 하여 기다린다.
dispatch_group_wait(group, time);

dispatch_release(group);

컨커런트 큐에서 async 작업간의 순서 정하기

시리얼큐가 아닌 컨커런트 큐를 사용할 경우 async 작업들에 대해 순서를 보장할 수 없다. 이때 dispatch_barrier_async를 사용하여 순서를 맞출 수 있다.
배리어를 다음과 같이 사용할경우, 블록 1, 2, 3 이후에 4, 5, 6 이 호출되는것을 보장할 수 있으나 배리어 전후로 1, 2, 3이 호출되는 순서는 보장되지 않는다.

dispatch_async(queue, ^{ NSLog(@“block 1");} );
dispatch_async(queue, ^{ NSLog(@“block 2");} );
dispatch_async(queue, ^{ NSLog(@“block 3");} );

dispatch_barrier_async(queue, ^{NSLog(@“this block is called after block 1, 2, 3"});

dispatch_async(queue, ^{ NSLog(@“block 4 called after barrier");} );
dispatch_async(queue, ^{ NSLog(@“block 5 called after barrier");} );
dispatch_async(queue, ^{ NSLog(@“block 6 called after barrier");} );

주의: 데드락을 일으키는 코드

dispatch_sync 를 잘못사용할 경우 데드락에 빠질 위험이 크므로 주의하여 사용해야 한다.

예제1

아래 코드를 메인스레드에서 실행시 데드락이 발생한다.

dispatch_sync(dispatch_get_main_queue(), ^{NSLog(@“This log will not be shown?”);} );

메인스레드에서 sync 형태로 블락을 추가하게되면, 해당 블록이 실행완료될때까지 메인스레드가 blocking wait 상태가 된다. 위 코드는 해당 블록을 메인 큐에 넣어 실행하도록 작성되었다. 하지만 이미 메인스레드가 blocking상태로 들어가있기 대문에 이 블록은 실행되지 않아서 계속 메인스레드는 blocking wait 상태이므로 데드락에 빠지게 된다.

예제2

시리얼 큐에서 실행될 블락내부에서 동일한 시리얼큐에대해 sync 블락을 추가할 경우 데드락 발생.

dispatch_async(serialQueue, ^{ 
          dispatch_sync(serialQueue, ^{NSLog(@“This log will not be shown”);} );
} );

디스패치 이터레이션(iteration)

배열등에 대해 dispatch를 이용한 작업을 적용하고 싶을경우 for-loop를 이용하여 dispatch를 여러번 호출하는 대신 dispatch_apply 를 활용하여 간편하게 이터레이션이 가능하다. 특히, dispatch_apply를 실행하는 큐가 컨커런트 큐일 경우, parallel for-loop를 실행하는 효과를 갖기때문에 멀티코어환경에서 실행속도 향상을 가져올 수있다[^apple_ref]. dispatch_apply의 경우 정해진 숫자만큼의 이터레이션(iteration)이 완료되기 전까지는 dispatch_apply가 실행된 해당 스레드를 blocking 하게되므로 주의해서 사용하자.

dispatch_async(queue, ^{

     dispatch_apply( [myArray count], queue, ^(size_t index) {
          NSLog(@“Object at %ld is %@“, index, [myArray objectAtIndex:index]);
     });
     dispatch_async(dispatch_get_main_queue(), ^{
          // update UI
     });
});

디스패치 실행 멈춤/재개

dispatch_suspend(queue), dispatch_resume(queue)를 이용하여 큐에 쌓여있는 블록의 실행을 멈추거나 재개하는 것이 가능하다.
이미 큐에서 빠져나와 실행된 블록은 멈출 수 없으며, 큐 내부에 쌓여있는 블록들만 제어가 가능하다.

디스패치 세마포어(semaphore)

디스패치 세마포어를 이용하면 디스패치간에 공유되는 변수에 여러 스레드가 동시에 접근하는것을 막을 수 있다. 다음 코드를 보자.

// 카운트가 1인 세마포어 생성
dispatch_semaphore_t sem = dispatch_semaphore_create(1);

for(int i = 0; i < 100; i++) {

     dispatch_async(queue, ^{
          // 레이스컨디션이 발생 할 수 있는 변수 변경 세마포어 구간 안에서 실행
          dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // wait에 의해 세마포어 카운트 1 감소
          [mutableArray addObject:@“object"];
          dispatch_semaphore_signal(sem); // signal에 의해 세마포어 카운트 1 증가
     });
}

어플리케이션 실행 후 딱 한번만 디스패치하기

dispatch_once를 이용하여 어플리케이션의 라이프타임동안 단 한번만 실행되는 코드를 작성할 수 있다. 첫 실행인지 체크하는 플래그를 두어 코드를 작성해야하는 번거로움을 해결할 수 있는데다, 멀티스레드 환경에서도 안전하게 한번만 실행되는것을 보장 할 수 있다.(플래그 방식으로 체크할 경우, 스레드간 공유되는변수에 락을 걸지 않으면 멀티스레드 환경에서 레이스 컨디션(race condition)이 발생할 가능성이 있다.)

이를 이용하면 싱글톤 패턴(singleton pattern)에서 싱글톤 객체가 생성될 때 멀티스레드 환경에서도 안전하게 동작하는 코드를 만들 수 있다.

// 싱글톤 생성
+ (instancetype)sharedInstance
{
    static dispatch_once_t once;
    static id sharedInstance;
    dispatch_once(&once, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

GCD에서 블록대신 함수 사용하기

이제까지 설명했던 dispatch 함수들에 _f가 postfix로 붙는 함수들이 있다. 이 함수들은 블락이 아닌 C의 함수포인터를 인자로 받기때문에 일반적인 함수에 대해서도 이터레이션을 실행할 수 있다.


  1. iOS와 OS X의 메모리 관리와 멀티스레딩 기법, 가즈키 사카모토, 도모히코 후루모토, 지앤선

  2. https://developer.apple.com/library/mac/documentation/performance/reference/gcd_libdispatch_ref/Reference/reference.html#//apple_ref/c/func/dispatch_apply