GCD (Grand Central Dispatch) dispatch queue는 태스크 수행을 위한 강력한 도구이다. dispatch queue를 사용하면 호출자에 대해 비동기 또는 동기적으로 임의의 코드 블록을 실행할 수 있다. dispatch queue를 사용하여 별도의 스레드에서 수행하는 데 사용했던 거의 모든 태스크를 수행 할 수 있다. dispatch queue의 장점은 해당 스레드 코드보다 사용하기가 더 간단하고 실행 시 훨씬 효율적이라는 것이다.
이 챕터에서는 dispatch queue에 대한 소개와 이를 사용하여 애플리케이션에서 일반적인 태스크를 실행하는 방법에 대한 정보를 제공한다.
About Dispatch Queues
dispatch queue는 애플리케이션에서 비동기적으로 동시에 태스크를 수행할 수 있는 쉬운 방법이다. 태스크는 단순히 애플리케이션에서 수행해야하는 작업이다. 예를 들어, 일부 계산을 수행하고, 데이터 구조를 생성 또는 수정하고, 파일에서 읽은 일부 데이터를 처리하는 태스크를 정의 할 수 있다. 함수 또는 블록 객체 안에 해당 코드를 배치하고 dispatch queue에 추가하여 태스크를 정의한다.
dispatch queue는 제출 한 태스크를 관리하는 객체와 같은 구조이다. 모든 dispatch queue는 선입선출 데이터 구조입니다. 따라서 큐에 추가한 태스크는 항상 추가된 순서대로 시작된다. GCD는 일부 dispatch queue를 자동으로 제공하지만 특정 목적을 위해 작성할 수 있는 다른 큐를 만들 수 있다. 하단의 표에는 애플리케이션에서 사용할 수 있는 dispatch queue의 유형과 사용 방법이 나열되어 있다.
타입 | 설명 |
Serial | Serial queue(private dispatch queue라고도 함)는 큐에 추가 된 순서대로 한 번에 하나의 태스크를 실행한다. 현재 실행중인 태스크는 dispatch queue에서 관리하는 별개의 스레드 (태스크마다 다를 수 있음)에서 실행된다. Serial queue는 종종 특정 리소스에 대한 액세스를 동기화하는 데 사용된다. 필요한만큼 Serial queue를 만들 수 있으며 각 큐는 다른 모든 큐와 동시에 작동한다. 즉, 4개의 Serial queue를 생성하면 각 큐는 한 번에 하나의 태스크만 실행되지만 각 큐에서 하나씩 최대 4개의 태스크를 동시에 실행할 수 있다는 의미이다. |
Concurrent | Concurrent queue (global dispatch queue 유형이라고도 함)은 하나 이상의 태스크를 동시에 실행하지만 태스크는 여전히 큐에 추가 된 순서대로 시작된다. 현재 실행중인 태스크는 dispatch queue에서 관리하는 별개의 스레드에서 실행된다. 특정 시점에서 실행되는 정확한 태스크의 수는 가변적이며 시스템 조건에 따라 다르다. iOS 5 이상에서는 DISPATCH_QUEUE_CONCURRENT를 큐 유형으로 지정하여 concurrent dispatch queue를 직접 만들 수 있다. 또한 애플리케이션에서 사용할 미리 정의 된 global concurrent queues가 4개 있다. |
Main dispatch queue | main dispatch queue는 애플리케이션의 메인 스레드에서 태스크를 실행하는 전역적으로 사용 가능한 serial queue이다. 이 큐는 애플리케이션의 Run Loop(있는 경우)와 함께 작동하여 대기중인 태스크의 실행과 Run Loop에 연결된 다른 이벤트 소스의 실행을 상호 연결한다. 애플리케이션의 메인 스레드에서 실행되기 때문에 메인 큐는 애플리케이션의 주요 동기화 지점으로 자주 사용된다. main dispatch queue를 만들 필요는 없지만 애플리케이션이 적절하게 비워주는지 확인해야 한다. |
애플리케이션에 동시성을 추가 할 때 dispatch queue는 스레드에 비해 몇 가지 이점을 제공한다. 가장 직접적인 이점은 work-queue programming model을 단순하게 할 수 있다. 스레드를 사용하면 수행하려는 작업과 스레드 자체의 생성 및 관리를 위한 코드를 작성해야 한다. dispatch queue를 사용하면 스레드 생성 및 관리에 대해 걱정할 필요 없이 실제로 수행하려는 작업에 집중할 수 있다. 대신 시스템이 모든 스레드 생성 및 관리를 처리한다. 장점은 시스템이 싱글 애플리케이션보다 훨씬 효율적으로 스레드를 관리 할 수 있다는 것이다. 시스템은 사용 가능한 리소스와 현재 시스템 조건에 따라 동적으로 스레드 수를 확장 할 수 있다. 또한 시스템은 일반적으로 스레드를 직접 만든 경우보다 더 빠르게 태스크 실행을 시작할 수 있다.
dispatch queue에 대한 코드를 다시 작성하는 것이 어려울 것이라고 생각할 수 있지만 스레드에 대한 코드를 작성하는 것보다 dispatch queue에 대한 코드를 작성하는 것이 더 쉽다. 코드 작성의 핵심은 독립적이고 비동기적으로 실행할 수 있는 태스크를 설계하는 것이다. (실제로는 스레드와 dispatch queue 모두에 해당된다.) 그러나 dispatch queue의 장점은 예측 가능성이다. 동일한 공유 리소스에 액세스하지만 다른 스레드에서 실행되는 두 개의 태스크가 있는 경우 두 스레드 중 하나가 먼저 리소스를 수정할 수 있으며 두 태스크가 동시에 해당 리소스를 수정하지 않도록 잠금을 사용해야 한다. dispatch queue를 사용하면 두 태스크를 serial dispatch queue에 추가하여 한 번에 하나의 태스크만 리소스를 수정하도록 할 수 있다. 큐 기반 동기화는 잠금보다 효율적이다. 왜냐하면 잠금은 항상 경쟁, 비경쟁의 경우 모두 비용이 많이 드는 커널 트랩을 필요로 하는 반면, dispatch queue는 주로 애플리케이션의 프로세스 공간에서 작동하고 꼭 필요한 경우에만 커널을 호출하기 때문이다.
serial queue에서 실행되는 두 태스크가 동시에 실행되지 않는다는 점을 지적하는 것이 옳지만, 두 스레드가 동시에 잠금을 수행하면 스레드에서 제공하는 동시성이 손실되거나 현저히 감소한다는 점을 기억해야 한다. 더 중요한 것은 스레드 모델은 커널과 사용자 공간 메모리를 모두 차지하는 두 개의 스레드를 생성해야 한다는 것이다. dispatch queue는 스레드에 대해 동일한 메모리 패널티를 지불하지 않으며 사용하는 스레드는 사용량이 많고 차단되지 않는다.
dispatch queue와 관련하여 기억해야 할 기타 주요사항은 다음과 같다.
- dispatch queue은 다른 dispatch queue을 고려하지 않고 태스크를 동시에 실행한다. 태스크의 직렬화는 single dispatch queue의 태스크로 제한된다.
- 시스템은 한 번에 실행되는 총 태스크의 수를 결정한다. 따라서 100개의 서로 다른 큐에 100개의 태스크가 있는 애플리케이션은 이러한 모든 태스크를 동시에 실행하지 못할 수 있다. (100 개 이상의 유효한 코어가 있는 경우에는 제외)
- 시스템은 시작할 새 태스크를 선택할 때 큐의 우선 순위 수준을 고려한다.
- 큐의 태스크는 큐에 추가 될 때 실행할 준비가 되어 있어야 한다. (이전에 Cocoa Operation Object를 사용한 적이 있는 경우, 이 동작이 model operation 사용과 다르다는 점에 유의하라.)
- Private dispatch queue는 참조 카운트 오브젝트이다. 자신의 코드에 큐를 리테인하는 것 외에도 dispatch source를 큐에 연결할 수 있으며 리테인 횟수를 늘릴 수도 있다. 따라서 모든 dispatch source가 취소되고 모든 리테인 콜과 적절한 릴리스 콜이 균형을 이루는 지 확인해야 한다.
dispatch queue을 조작하는 데 사용하는 인터페이스에 대한 자세한 내용은 GCD (Grand Central Dispatch) 레퍼런스를 참조하시오.
Queue-Related Technologies
dispatch queue 외에도 Grand Central Dispatch는 큐를 사용하여 코드를 관리하는 여러 기술을 제공한다. 하단의 표는 이러한 기술을 나열한다.
기술 | 설명 |
Dispatch groups | dispatch group은 블록 오브젝트 집합을 모니터링하여 환료할 수 있는 방법이다. (필요에 따라 블록을 동기 또는 비동기적으로 모니터링 할 수 있다.) 그룹은 다른 태스크의 완료에 따라 달라지는 코드에 대한 유용한 동기화 메커니즘을 제공한다. |
Dispatch semaphores | Dispatch semaphore는 전통적인 세마포어와 유사하지만 일반적으로 더 효율적이다. Dispatch semaphore는 세마포어를 사용할 수 없어서 호출 스레드를 차단해야 하는 경우에만 커널로 호출합니다. 세마포어를 사용할 수있는 경우 커널 호출이 수행되지 않는다. |
Dispatch sources | Dispatch source는 특정 유형의 시스템 이벤트에 대한 응답으로 notification을 생성한다. Dispatch source를 사용하여 process notification, 시그널 및 descriptor events와 같은 이벤트를 모니터링 할 수 있다. 이벤트가 발생하면 Dispatch source는 처리를 위해 태스크 코드를 지정된 dispatch queue에 비동기식으로 보낸다. |
Implementing Tasks Using Blocks
블록 오브젝트는 C, Objective-C 및 C++ 코드에서 사용할 수있는 C 기반의 언어 기능이다. 블록을 사용하면 독립적인 작업 단위를 쉽게 정의 할 수 있다. 함수 포인터와 비슷해 보일 수 있지만 블록은 실제로 객체와 유사한 기본 데이터 구조로 표현되며 컴파일러에 의해 생성되고 관리된다. 컴파일러는 사용자가 제공하는 코드 (관련 데이터와 함께)를 패키지화하고 힙에 있고 애플리케이션 주위에 전달할 수 있는 형식으로 캡슐화한다.
블록의 주요 장점 중 하나는 own lexical scope 외부에서 변수를 사용할 수 있다는 것이다. 함수 또는 메서드 내부에 블록을 정의하면 블록은 어떤 방식으로든 기존 코드 블록처럼 작동한다. 예를 들어 블록은 상위 범위에 정의된 변수의 값을 읽을 수 있다. 블록이 액세스한 변수는 블록이 나중에 액세스 할 수 있도록 힙의 블록 데이터 구조에 복사된다. 블록이 dispatch queue에 추가 될 때 이러한 값은 일반적으로 읽기 전용 형식으로 유지되어야 한다. 그러나 동기식으로 실행되는 블록은 __block 키워드가 앞에 추가된 변수를 사용하여 데이터를 상위 호출 범위로 다시 리턴 할 수도 있다.
함수 포인터에 사용되는 구문과 유사한 구문을 사용하여 코드와 함께 블록을 인라인으로 선언한다. 블록과 함수 포인터의 주요 차이점은 블록 이름 앞에 별표 (*) 대신 캐럿 (^)이 붙는다는 것이다. 함수 포인터처럼 인수를 블록에 전달하고 리턴 값을 받을 수 있다. 하단의 코드에서 블록을 동기적으로 선언하고 실행하는 방법을 보여준다. 변수 aBlock은 하나의 정수 매개 변수를 사용하고 값을 리턴하지 않는 블록으로 선언되었다. 그 프로토 타입과 일치하는 실제 블록이 aBlock에 할당되고 인라인으로 선언된다. 마지막 줄은 블록을 즉시 실행하여 지정된 정수를 표준 출력으로 출력한다.
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
다음은 블록을 설계 할 때 고려해야 할 몇 가지 주요 지침을 요약 한 것이다.
- dispatch queue를 사용하여 비동기적으로 수행하려는 블록의 경우 상위 함수 또는 메서드에서 스칼라 변수를 캡처하여 블록에서 사용하는 것이 안전하다. 그러나 호출 컨텍스트에 의해 할당 및 삭제 된 대형 구조 또는 기타 포인터 기반 변수를 캡처하려고 시도해서는 안 된다. 블록이 실행될 때 해당 포인터가 참조하는 메모리가 사라질 수 있기 때문이다. 물론 메모리(또는 오브젝트)를 직접 할당하고 해당 메모리의 소유권을 블록에 명시적으로 넘기는 것은 안전하다.
- dispatch queue는 추가 된 블록을 복사하고 실행이 끝나면 블록을 해제한다. 즉, 큐에 블록을 추가하기 전에 명시적으로 블록을 복사 할 필요가 없다.
- 작은 태스크를 실행할 때 큐가 원시 스레드보다 더 효율적이지만, 블록을 만들고 큐에서 실행하는 데는 여전히 오버헤드가 발생한다. 블록이 너무 적은 작업을 수행하면 큐로 보내는 것보다 인라인으로 실행하는 것이 더 저렴할 수 있다. 블록이 너무 적은 작업을 수행하는지 확인하는 방법은 성능 도구를 사용하여 각 경로에 대한 메트릭을 수집하고 비교하는 것이다.
- 기본 스레드와 관련된 데이터를 캐시하지 말고 다른 블록에서 데이터에 액세스 할 수 있어야 한다. 동일한 큐의 태스크가 데이터를 공유해야하는 경우, dispatch queue의 컨텍스트 포인터를 사용하여 데이터를 대신 저장하시오.
- 블록이 Objective-C Object를 몇 개 이상 생성하는 경우 블록 코드의 일부를 @autorelease 블록으로 묶어 해당 오브젝트에 대한 메모리 관리를 처리 할 수 있다. GCD dispatch queue에는 own autoreleased pool이 있지만 해당 풀이 언제 비워지는 보장하지 않는다. 애플리케이션에 메모리가 제한되어있는 경우 own autoreleased pool을 생성하면 보다 정기적으로 autoreleased object의 메모리를 확보 할 수 있다.
Creating and Managing Dispatch Queues
태스크를 큐에 추가하기 전에 사용할 큐의 유형과 사용 방법을 결정해야 한다. dispatch queue는 태스크를 직렬 또는 동시에 실행할 수 있다. 또한 특정 큐의 사용을 염두에 두고 있는 경우 그에 따라 큐의 속성을 설정 할 수 있다. 다음 섹션에서는 dispatch queue를 만들고 사용하도록 설정하는 방법을 보여준다.
Getting the Global Concurrent Dispatch Queues
concurrent dispatch queue는 병렬로 실행할 수 있는 여러 태스크가 있을 때 유용하다. concurrent queue는 선입선출 순서로 태스크를 큐에서 빼기 때문에 여전히 큐라고 할 수 있다. 그러나 concurrent queue는 이전 태스크가 완료되기 전에 추가 태스크를 큐에서 뺄 수 있다. 특정 순간에 concurrent queue에 의해 실행되는 실제 태스크의 수는 가변적이며 애플리케이션의 상태가 변경되면 동적으로 변경 될 수 있다. 사용 가능한 코어 수, 다른 프로세스에서 수행하는 작업량, 다른 serial dispatch queue의 작업 수 및 우선 순위를 포함하여 여러 요소가 concurrent queue에서 실행되는 태스크의 수가 영향을 준다.
시스템은 각 애플리케이션에 4개의 concurrent dispatch queue를 제공한다. 이러한 큐는 애플리케이션에서 글로벌이며 우선 순위 수준에 의해서만 구분된다. 글로벌이므로 명시적으로 생성하지 않는다. 대신 하단의 코드와 같이 dispatch_get_global_queue 함수를 사용하여 큐 중 하나를 요청하면 된다.
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
default concurrent queue을 가져 오는 것 외에도 DISPATCH_QUEUE_PRIORITY_HIGH 및 DISPATCH_QUEUE_PRIORITY_LOW 상수를 함수에 전달하여 우선 순위가 높고 낮은 수준의 큐를 가져 오거나 DISPATCH_QUEUE_PRIORITY_BACKGROUND 상수를 전달하여 백그라운드 큐를 가져올 수도 있다. 예상한 대로 우선 순위가 높은 concurrent queue의 큐는 default 및 우선 순위가 낮은 큐의 태스크보다 먼저 실행된다. 마찬가지로 default queue의 태스크는 우선 순위가 낮은 큐의 태스크보다 먼저 실행된다.
참고: dispatch_get_global_queue 함수에 대한 두 번째 파라미터는 이후 확장을 위해 제공된다. 현재로서는 해당 파라미터에 대해 항상 0을 넣어야 한다.
dispatch queue는 참조 카운트 오브젝트이지만 global concurrent queues을 리테인하고 릴리즈할 필요는 없다. 이러한 큐에 대한 리테인 및 릴리즈 호출은 애플리케이션에서 글로벌이므로 무시된다. 따라서 이러한 큐에 대한 참조를 저장할 필요는 없다. 그들 중 하나에 대한 참조가 필요할 때마다 dispatch_get_global_queue 함수를 호출하면 된다.
Creating Serial Dispatch Queues
Serial queue는 태스크를 특정 순서로 실행하려는 경우에 유용하다. serial queue는 한 번에 하나의 태스크만 실행하고 항상 큐의 맨 앞에서 태스크를 가져온다. 공유 리소스 또는 변경 가능한 데이터 구조를 보호하기 위해 잠금 대신 serial queue를 사용할 수 있다. 잠금과 달리 serial queue는 태스크가 예측 가능한 순서로 실행되도록 한다. 태스크를 serial queue에 비동기적으로 전달하는 한 큐는 교착 상태가 될 수 없다.
자동으로 생성되는 concurrent queue와 달리 사용하려는 serial queue는 명시적으로 생성하고 관리해야 한다. 애플리케이션에 대해 임의의 수의 serial queue를 생성 할 수 있지만 가능한 한 많은 태스크를 동시에 실행하기 위한 수단으로만 많은 수의 serial queue를 생성하는 것은 피해야 한다. 많은 수의 태스크를 동시에 실행하려면 global concurrent queue 중 하나에 전달하십시오. serial queue를 만들 때 리소스 보호 또는 애플리케이션의 일부 주요 동작 동기화와 같은 각 큐의 용도를 식별하시오.
하단의 코드는 custom serial queue를 생성하는 데 필요한 단계를 보여준다. dispatch_queue_create 함수는 큐의 이름과 큐의 속성 집합의 두 가지 매개 변수를 사용한다. 디버거 및 성능 도구는 큐의 이름을 표시하여 태스크가 실행되는 방식을 추적하는 데 도움이 된다. 큐의 속성은 향후 사용을 위해 예약되어 있으며 NULL이어야 한다.
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);
사용자가 만든 custom queue 외에도 시스템은 자동으로 serial queue를 생성하여 애플리케이션의 메인 스레드에 바인딩한다.
Getting Common Queues at Runtime
Grand Central Dispatch는 애플리케이션에서 몇 가지 일반적인 dispatch queue에 액세스 할 수 있는 기능을 제공한다.
- 디버깅 목적으로 또는 현재 큐의 ID를 테스트하려면
dispatch_get_current_queue함수(현재 이 함수는 사용되지 않는 함수이다.)를 사용하라. 블록 오브젝트 내부에서 이 함수를 호출하면 블록이 전달 된 (그리고 현재 실행중인 것으로 추정되는) 큐가 반환된다. 블록 외부에서 이 함수를 호출하면 애플리케이션의 default concurrent queue가 반환된다. - dispatch_get_main_queue 함수를 사용하여 애플리케이션의 메인 스레드와 연결된 serial dispatch queue를 가져올 수 있다. 이 큐는 Cocoa 애플리케이션과 dispatch_main 함수를 호출하거나 메인 스레드에서 run loop(CFRunLoopRef 유형 또는 NSRunLoop 객체 사용)를 구성하는 애플리케이션을 위해 자동으로 생성된다.
- dispatch_get_global_queue 함수를 사용하여 공유 된 concurrent queue를 가져올 수 있다.
Memory Management for Dispatch Queues
Dispatch queue 및 기타 dispatch object는 참조 카운트 데이터 유형이다. serial dispatch queue를 생성 할 때 초기 참조 횟수는 1이다. dispatch_retain 및 dispatch_release 함수를 사용하여 필요에 따라 해당 참조 횟수를 늘리거나 줄일 수 있다. 큐의 참조 수가 0에 도달하면 시스템은 큐를 비동기적으로 메모리에서 할당 해제한다.
큐와 같은 dispatch object를 리테인하고 릴리즈하여 사용되는 동안 메모리에 남아있는지 확인하는 것이 중요하다. 메모리 관리에 있어서 Cocoa 오브젝트와 마찬가지로, 일반적인 규칙은 코드에 전달 된 큐를 사용하려는 경우 큐를 사용하기 전에 리테인하고 더 이상 필요하지 않을 때 릴리즈해야 한다는 것이다. 이 기본 패턴은 큐를 사용하는 동안 메모리에 남아 있도록 한다.
참고: concurrent dispatch queue 또는 main dispatch queue를 포함하여 global dispatch queue를 리테인하거나 릴리즈 할 필요는 없다. 큐를 리테인하거나 릴리즈하려는 모든 시도는 무시 된다.
garbage-collected 애플리케이션을 구현하더라도 dispatch queue 및 기타 dispatch object를 리테인하고 릴리즈해야 한다. Grand Central Dispatch는 메모리 회수를 위한 가비지 컬렉션 모델을 지원하지 않는다.
Storing Custom Context Information with a Queue
모든 dispatch object (dispatch queue 포함)를 사용하면 사용자 지정 컨텍스트 데이터를 오브젝트와 연결할 수 있다. 주어진 오브젝트에서 이 데이터를 설정하거나 가져오려면 dispatch_set_context 및 dispatch_get_context 함수를 사용하면 된다. 시스템은 사용자 지정 데이터를 어떤 방식으로도 사용하지 않으며 적절한 시간에 데이터를 할당하고 할당 해제하는 것은 사용자에게 달려 있다.
큐의 경우 컨텍스트 데이터를 사용하여 Objective-C object 또는 기타 데이터 구조에 대한 포인터를 저장하여 큐 또는 코드 사용을 식별할 수 있다. 큐의 finalizer 함수를 사용하여 할당이 해제되기 전에 큐에서 컨텍스트 데이터를 할당 해제 (또는 연결 해제) 할 수 있다. 큐의 컨텍스트 데이터를 지우는 finalizer 함수를 작성하는 방법의 예가 하단에 나와있다.
Providing a Clean Up Function For a Queue
serial dispatch queue를 생성 한 후 finalizer 함수를 연결하여 큐의 할당이 취소 될 때 사용자 지정 정리를 수행 할 수 있다. dispatch queue는 참조 카운트 오브젝트이며 dispatch_set_finalizer_f 함수를 사용하여 큐의 참조 카운트가 0에 도달 할 때 실행할 함수를 지정할 수 있다. 이 함수를 사용하여 큐와 연관된 컨텍스트 데이터를 정리한다. 컨텍스트 포인터가 NULL이 아닌 경우에만 함수가 호출된다.
하단의 코드는 custom finalizer 함수와 큐를 만들고 해당 finalizer를 설치하는 함수를 보여준다. 큐는 finalizer 함수를 사용하여 큐의 컨텍스트 포인터에 저장된 데이터를 해제한다. (코드에서 참조되는 myInitializeDataContextFunction 및 myCleanUpDataContextFunction 함수는 데이터 구조 자체의 내용을 초기화하고 정리하기 위해 제공하는 사용자 지정 함수이다.) finalizer 함수에 전달 된 컨텍스트 포인터에는 큐와 연결된 데이터 개체가 포함된다.
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;
}
Adding Tasks to a Queue
태스크를 실행하려면 해당 태스크를 적절한 dispatch queue로 dispatch 해야 한다. 태스크를 동기 또는 비동기적으로 dispatch 할 수 있으며 단일 또는 그룹으로 dispatch 할 수 있다. 큐에 들어오면, 큐는 제약 조건과 이미 큐에 존재하는 기존 태스크를 고려하여 가능한 한 빨리 태스크를 실행할 책임이 있다. 이 섹션에서는 태스크를 큐로 dispatch 하는 몇 가지 기술을 보여주고 각각의 장점을 설명한다.
Adding a Single Task to a Queue
태스크를 큐에 추가하는 방법에는 비동기식 또는 동기식 두 가지가 있다. 가능하다면, 동기식보다 dispatch_async 및 dispatch_async_f 함수를 사용하는 비동기 실행 선호된다. 블록 오브젝트 또는 함수를 큐에 추가하면 해당 코드가 언제 실행되는지 알 수 없다. 결과적으로 블록 또는 함수를 비동기적으로 추가하면 코드 실행을 예약하고 호출 스레드에서 다른 작업을 계속할 수 있다. 이는 아마도 일부 사용자 이벤트에 대한 응답으로 애플리케이션의 메인 스레드에서 태스크를 스케줄링 하는 경우 특히 중요하다.
가능할 때마다 태스크를 비동기적으로 추가해야 하지만 경쟁 조건이나 기타 동기화 오류를 방지하기 위해 태스크를 동기적으로 추가해야 하는 경우가 있을 수 있다. 이러한 경우 dispatch_sync 및 dispatch_sync_f 함수를 사용하여 작업을 큐에 추가 할 수 있다. 이러한 함수는 지정된 태스크가 실행을 완료 할 때까지 현재 실행 스레드를 차단한다.
중요 : 함수에 전달하려는 계획을 가진 큐와 동일한 큐에서 실행 중인 태스크에서는 dispatch_sync 또는 dispatch_sync_f 함수를 호출해서는 안 된다. 이것은 교착 상태가 가능한 serial queue의 경우 특히 중요하다. 하지만 concurrent queue에서는 피할 수 있다.
하단의 예제는 태스크를 비동기 및 동기적으로 dispatch 하기 위해 블록 기반 변수를 사용하는 방법을 보여준다.
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
본질적으로 큐로 dispatch 된 태스크는 해당 태스크를 생성한 코드와 독립적으로 실행된다. 그러나 태스크가 완료되면 결과를 통합 할 수 있도록 애플리케이션에 해당 사실을 알려야 할 수 있다. 전통적인 비동기 프로그래밍에서는 콜백 메커니즘을 사용하여 이를 수행 할 수 있지만 dispatch queue에서는 completion block을 사용할 수 있다.
completion block은 원래 태스크가 끝날 때 큐에 전달하는 또 다른 코드입니다. 일반적으로 호출 코드는 태스크를 시작할 때 completion block을 매개 변수로 제공한다. 태스크 코드는 작업이 완료 될 때 지정된 블록 또는 함수를 지정된 큐에 보내기만 하면 된다.
하단의 코드는 블록을 사용하여 구현 된 평균화 함수를 보여준다. 평균화 함수에 대한 마지막 두 개의 매개 변수를 사용하면 호출자가 결과를 보고 할 때 사용할 큐와 블록을 지정할 수 있다. 평균화 함수가 값을 계산 한 후 결과를 지정된 블록으로 전달하고 큐로 보낸다. 큐가 조기에 릴리즈되는 것을 방지하려면 해당 큐를 처음에 리테인하고 completion block이 발송 된 후에 릴리즈하는 것이 중요하다.
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
concurrent dispatch queue가 성능을 향상 시킬 수 있는 한가지 방법은 고정된 반복 횟수를 수행하는 반복문에 있다. 예를 들어, 반복문을 통해 일부 작업을 수행하는 for 루프가 있다고 가정해보자.
for (i = 0; i < count; i++) {
printf("%u\n",i);
}
각 반복 중에 수행되는 작업이 다른 모든 반복 중에 수행 된 작업과 구별되고 각 반복문의 완료되는 순서가 중요하지 않은 경우 반복되는 부분을 dispatch_apply 또는 dispatch_apply_f 함수에 대한 호출로 대체 할 수 있다. 이러한 함수는 반복문마다 한 번씩 지정된 블록 또는 함수를 큐에 보낸다. 따라서 concurrent queue에 dispatch 되면 동시에 여러 반복문을 수행 할 수 있다.
dispatch_apply 또는 dispatch_apply_f를 호출 할 때 serial queue 또는 concurrent queue을 지정할 수 있다. concurrent queue로 전달하면 여러 반복문을 동시에 수행 할 수 있으며 이러한 기능을 사용하는 가장 일반적인 방법이다. serial queue을 사용하는 것이 허용되고 코드에 적합한 작업을 수행하지만, 이러한 큐를 사용하는 것은 기존의 반복문을 사용하는 것보다 실질적인 성능 이점이 없다.
중요 : 일반 for 문과 마찬가지로 dispatch_apply 및 dispatch_apply_f 함수는 모든 반복문이 완료 될 때까지 반환되지 않는다. 따라서 큐의 컨텍스트에서 이미 실행중인 코드를 호출 할 때 주의해야 한다. 함수에 매개 변수로 전달한 큐가 serial queue이고 현재 코드를 실행하는 큐와 동일한 경우 이러한 함수를 호출하면 큐가 교착 상태가 된다.
이 기능은 현재 스레드를 효과적으로 차단하기 때문에 메인 스레드에서 이러한 함수를 호출 할 때도 주의해야 한다. 이벤트 처리 루프가 이벤트에 적시에 응답하지 못하게 할 수 있다. 루프 코드에 상당한 처리 시간이 필요한 경우 다른 스레드에서 이러한 함수를 호출할 수 있다.
하단의 코드는 앞의 for 문을 dispatch_apply 구문으로 바꾸는 방법을 보여준다. dispatch_apply 함수에 전달하는 블록에는 현재 반복문을 식별하는 하나의 매개 변수가 포함되어야 한다. 블록이 실행되면 이 매개 변수의 값은 첫 번째 반복의 경우 0, 두 번째 반복의 경우 1이 된다. 마지막 반복에 대한 매개 변수의 값은 count-1이며 여기서 count는 총 반복 횟수이다.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n",i);
});
태스크 코드가 반복될 때마다 적절한 양의 작업을 수행하는지 확인해야 한다. 큐에 보내는 모든 블록 또는 함수와 마찬가지로 실행을 위해 해당 코드를 스케줄링 하는 데 오버헤드가 발생한다. 루프를 반복할 때마다 발생하는 작업량이 작으면 코드를 스케줄링하는 오버헤드가 코드를 큐로 dispatch하여 얻을 수있는 성능 이점보다 클 수 있다. 테스트 중에 이것이 사실인 경우 striding을 사용하여 각 반복문 중에 수행되는 작업의 양을 늘릴 수 있다. striding을 사용하면 원래 루프를 여러번 반복하여 하나의 블록으로 그룹화하고 반복 횟수를 비례적으로 줄인다. 예를 들어 처음에 100회 반복을 수행했지만 striding을 4번 사용하기로 결정한 경우 이제 각 블록에서 4번의 반복문을 수행하고 반복 횟수는 25회가 된다.
Performing Tasks on the Main Thread
Grand Central Dispatch는 애플리케이션의 메인 스레드에서 태스크를 실행하는 데 사용할 수 있는 special dispatch queue을 제공한다. 이 큐는 모든 애플리케이션에 자동으로 제공되며 메인 스레드에서 런 루프(CFRunLoopRef 타입 또는 NSRunLoop 오브젝트에 의해 관리 됨)를 설정하는 애플리케이션에 의해 자동으로 비워진다. Cocoa 애플리케이션을 생성하지 않고 명시적으로 런 루프를 설정하지 않으려면 dispatch_main 함수를 호출하여 main dispatch queue를 명시적으로 비워야한다. 여전히 태스크를 큐에 추가 할 수 있지만 이 함수를 호출하지 않으면 해당 태스크는 실행되지 않는다.
dispatch_get_main_queue 함수를 호출하여 애플리케이션의 메인 스레드에 대한 dispatch queue를 가져올 수 있다. 이 큐에 추가 된 태스크는 메인 스레드 자체에서 순차적으로 수행된다. 따라서 이 큐를 애플리케이션의 다른 부분에서 수행되는 작업의 동기화 지점으로 사용할 수 있다.
Using Objective-C Objects in Your Tasks
GCD는 Cocoa 메모리 관리 기술에 대한 내장 지원을 제공하므로 dispatch queue에 전달하는 블록에서 Objective-C Object를 자유롭게 사용할 수 있다. 각 dispatch queue는 own autoreleased pool을 유지하여 autoreleased object가 특정 시점에 릴리즈 되도록 한다. 큐는 실제로 해당 오브젝트를 릴리즈하는 시기를 보장하지 않는다.
애플리케이션의 메모리가 제한되어 있고 블록이 autoreleased object를 몇 개 이상 생성하는 경우 own autoreleased pool을 만드는 것이 오브젝트가 적시에 릴리즈 되도록 하는 유일한 방법이다. 블록이 수백 개의 오브젝트를 생성하는 경우 하나 이상의 autoreleased pool을 생성하거나 일정한 간격으로 풀을 비울 수 있다.
Suspending and Resuming Queues
큐를 일시 중단하여 일시적으로 블록 오브젝트를 실행하지 못하도록 방지 할 수 있다. dispatch_suspend 함수를 사용하여 dispatch queue를 일시 중지하고 dispatch_resume 함수를 사용하여 재개한다. dispatch_suspend를 호출하면 큐의 일시 중단 참조 수가 증가하고 dispatch_resume을 호출하면 참조 수가 감소한다. 참조 횟수가 0보다 크면 큐는 일시 중단 된 상태로 유지된다. 따라서 처리중인 블록을 재개하려면 모든 일시 중단 호출과 일치하는 재개 호출의 균형을 맞춰야 한다.
중요 : 일시 중지 및 재개 호출은 비동기적이며 블록 실행 간에만 적용된다. 큐를 일시 중단해도 이미 실행중인 블록이 중지되는 것은 아니다.
Using Dispatch Semaphores to Regulate the Use of Finite Resources
dispatch queue에 전달하는 태스크가 일부 finite resource에 액세스하는 경우 dispatch semaphore를 사용하여 해당 리소스에 동시에 액세스하는 작업 수를 조절할 수 있다. dispatch semaphore는 한 가지 예외를 제외하고는 일반 semaphore처럼 작동한다. 리소스를 사용할 수 있는 경우 기존 시스템 semaphore를 획득하는 것보다 dispatch semaphore를 획득하는 데 시간이 덜 걸린다. 이는 Grand Central Dispatch가 이 특정 경우에 대해 커널을 호출하지 않기 때문이다. 커널을 호출하는 유일한 시간은 리소스를 사용할 수 없을 때이며 시스템은 semaphore가 시그널 될 때까지 스레드를 고정해야 한다.
dispatch semaphore를 사용하는 방법은 다음과 같다.
- semaphore를 만들 때 (dispatch_semaphore_create 함수 사용) 사용 가능한 리소스 수를 나타내는 양의 정수를 지정할 수 있다.
- 각 태스크에서 dispatch_semaphore_wait를 호출하여 semaphore를 기다린다.
- 대기 호출이 반환되면 리소스를 확보하고 작업을 수행하십시오.
- 리소스 사용이 끝나면 dispatch_semaphore_signal 함수를 호출하여 리소스를 릴리즈하고 semaphore에 시그널을 보낸다.
이러한 단계의 작동 방식에 대한 예를 들면, 시스템에서 file descriptors를 사용하는 것을 고려하시오. 각 애플리케이션에는 사용할 수 있는 제한된 수의 file descriptors가 제공된다. 많은 수의 파일을 처리하는 태스크가 있는 경우 file descriptors가 부족할 정도로 많은 파일을 한 번에 열지 않아야 한다. 대신 semaphore를 사용하여 파일 처리 코드에서 한 번에 사용중인 file descriptors 수를 제한 할 수 있다. 태스크에 통합할 기본 코드는 다음과 같다.
// 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);
semaphore를 만들 때 사용 가능한 리소스 수를 지정한다. 이 값은 semaphore의 초기 카운트 변수가 된다. semaphore에서 기다릴 때마다 dispatch_semaphore_wait 함수는 해당 카운트 변수를 1씩 감소시킨다. 결과 값이 음수이면 이 함수는 커널에 스레드를 차단하도록 지시한다. 반면에 dispatch_semaphore_signal 함수는 자원이 릴리즈되었음을 나타내기 위해 count 변수를 1씩 증가시킨다. 차단된 태스크가 있고 리소스를 기다리는 경우 태스크 중 하나가 이후에 차단 해제되고 작업을 수행 할 수 있다.
Waiting on Groups of Queued Tasks
Dispatch group은 하나 이상의 태스크 실행이 완료 될 때까지 스레드를 차단하는 방법이다. 지정된 모든 태스크가 완료 될 때까지 진행할 수 없는 곳에서 이 동작을 사용할 수 있다. 예를 들어 일부 데이터를 계산하기 위해 여러 태스크를 dispatch 한 후 그룹을 사용하여 해당 태스크를 기다린 다음 완료되면 결과를 처리 할 수 있다. dispatch group을 사용하는 또 다른 방법은 스레드 조인의 대체제이다. 여러 하위 스레드를 시작한 다음 각 스레드와 결합하는 대신 dispatch group에 해당 태스크를 추가하고 전체 그룹을 기다릴 수 있다.
하단의 코드는 그룹을 설정하고 태스크를 dispatch하고 결과를 기다리는 기본 프로세스를 보여준다. dispatch_async 함수를 사용하여 태스크를 큐에 dispatch 하는 대신 dispatch_group_async 함수를 사용한다. 이 함수는 태스크를 그룹과 연결하고 실행을 위해 큐에 넣는다. 태스크 그룹이 완료 될 때까지 기다리려면 dispatch_group_wait 함수를 사용하여 적절한 그룹을 전달한다.
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 Queues and Thread Safety
dispatch queue 문서에서 thread safety에 대해 이야기하는 것은 이상하게 보일 수 있지만 thread safety 또한 관련 주제이다. 애플리케이션에서 동시성을 구현할 때마다 알아야 할 몇 가지 사항이 있다.
- Dispatch queue 자체는 thread safe 한다. 즉, 먼저 잠금을 설정하거나 큐에 대한 액세스를 동기화하지 않고 시스템의 모든 스레드에서 태스크를 dispatch queue에 전달할 수 있다.
- 함수 호출에 전달하는 것과 동일한 큐에서 실행중인 태스크에서 dispatch_sync 함수를 호출하지 마시오. 그렇게하면 큐가 교착 상태가 된다. 현재 큐로 dispatch 해야하는 경우 dispatch_async 함수를 사용하여 비동기적으로 수행해야 한다.
- Dispatch queue에 전달하는 태스크에서 잠금을 사용하지 마시오. 태스크에서 잠금을 사용하는 것이 안전하지만 잠금을 획득 할 때 해당 잠금을 사용할 수 없는 경우 serial queue를 완전히 차단할 위험이 있다. 마찬가지로 concurrent queue의 경우 잠금 대기로 인해 다른 태스크가 대신 실행되지 않을 수 있다. 코드의 일부를 동기화 해야하는 경우 잠금 대신 serial dispatch queue를 사용하시오.
- 태스크를 실행하는 스레드에 대한 정보를 얻을 수 있지만 그렇게 하지 않는 것이 좋다.
[원문]
Dispatch Queues
Dispatch Queues Grand Central Dispatch (GCD) dispatch queues are a powerful tool for performing tasks. Dispatch queues let you execute arbitrary blocks of code either asynchronously or synchronously with respect to the caller. You can use dispatch queues t
developer.apple.com
'프로그래밍 > iOS' 카테고리의 다른 글
Concurrency Programming Guide (6) - Migrating Away from Threads (0) | 2020.12.27 |
---|---|
Concurrency Programming Guide (5) - Dispatch Sources (0) | 2020.12.26 |
Concurrency Programming Guide (3) - Operation Queue (0) | 2020.12.24 |
Concurrency Programming Guide (2) - Concurrency and Application Design (0) | 2020.12.23 |
Concurrency Programming Guide (1) - 소개 (0) | 2020.12.23 |