본문 바로가기

프로그래밍/iOS

Concurrency Programming Guide (5) - Dispatch Sources

시스템과 상호 작용할 때마다 해당 태스크에 적은 시간이 걸리도록 준비해야 한다. 커널 또는 기타 시스템 계층으로 호출하려면 자체 프로세스 내에서 발생하는 호출에 비해 상당히 많은 비용이 드는 컨텍스트 변경이 포함된다. 따라서 대부분의 시스템 라이브러리는 코드가 시스템에 요청을 제출하고 해당 요청이 처리되는 동안 다른 작업을 계속할 수 있도록 비동기 인터페이스를 제공한다. Grand Central Dispatch는 리퀘스트를 보내고 결과를 블록 및 Dispatch Queue를 사용하여 코드에 다시 전달 할 수 있도록 하는 일반적인 동작으로 구성된다.

 

About Dispatch Sources

 

dispatch source는 특정 로우 레벨 시스템 이벤트의 처리를 조정하는 기본 데이터 유형이다. Grand Central Dispatch는 다음 유형의 dispatch source를 지원한다.

 

  • Timer dispatch source는 주기적인 알림을 생성한다.
  • Signal dispatch source는 UNIX 시그널이 도착하면 알려준다.
  • Descriptor source는 다음과 같은 다양한 파일 및 소켓 기반 operation을 알려준다.
    • 데이터를 읽을 수있는 경우
    • 데이터를 쓸 수있는 경우
    • 파일 시스템에서 파일이 삭제, 이동 또는 이름이 변경된 경우
    • 파일 메타 정보가 변경되는 경우
  • Process dispatch source는 다음과 같은 프로세스 관련 이벤트를 알려준다.
    • 프로세스가 종료 될 때
    • 프로세스가 fork 또는 exec 타입의 호출을 할 때
    • 프로세스에 시그널이 전달 될 때
  • Mach port dispatch source는 Mach 관련 이벤트를 알려준다.
  • Custom dispatch source는 사용자가 직접 정의하고 트리거하는 소스이다.

 

dispatch source는 일반적으로 시스템 관련 이벤트를 처리하는 데 사용되는 비동기 콜백 함수를 대체한다. dispatch source를 설정 할 때 모니터링 할 이벤트와 해당 이벤트를 처리하는 데 사용할 dispatch queue 및 코드를 지정한다. 블록 오브젝트 또는 함수를 사용하여 코드를 지정할 수 있다. 관심있는 이벤트가 도착하면 dispatch source는 블록 또는 함수의 실행을 위해 지정된 dispatch queue에 보낸다.

 

수동으로 큐에 제출하는 태스크와 달리 dispatch source는 애플리케이션에 대한 지속적인 이벤트 소스를 제공한다. dispatch source는 명시적으로 취소 할 때까지 dispatch queue에 연결된 상태로 유지된다. 연결되는 동안 해당 이벤트가 발생할 때마다 dispatch queue에 관련 태스크 코드를 보낸다. 타이머 이벤트와 같은 일부 이벤트는 정기적으로 발생하지만 대부분은 특정 조건이 발생할 때 간헐적으로 발생한다. 이러한 이유로 dispatch source는 이벤트가 보류 중인 동안 조기에 릴리즈되는 것을 방지하기 위해 연관된 dispatch queue를 유지한다.

 

이벤트가 dispatch queue에 백로그되는 것을 방지하기 위해 dispatch source는 이벤트 통합 체계를 구현한다. 이전 이벤트의 이벤트 핸들러가 dequeue 되고 실행되기 전에 새 이벤트가 도착하면 dispatch source는 새 이벤트 데이터의 데이터를 이전 이벤트의 데이터와 병합한다. 이벤트 유형에 따라 병합은 이전 이벤트를 대체하거나 보유한 정보를 업데이트 할 수 있다. 예를 들어 signal-based dispatch source는 가장 최근 시그널에 대한 정보만 제공하지만 이벤트 핸들러의 마지막 호출 이후 전달 된 총 시그널 수도 보고한다.

 

Creating Dispatch Sources

 

dispatch source를 생성하려면 이벤트 소스와 dispatch source 자체를 모두 생성해야 한다. 이벤트의 소스는 이벤트를 처리하는 데 필요한 기본 데이터 구조이다. 예를 들어 descriptor-based dispatch source의 경우 descriptor를 열어야 하고 process-based source의 경우 대상 프로그램의 프로세스 ID를 가져와야 한다. 이벤트 소스가 있으면 다음과 같이 해당 dispatch source를 만들 수 있다.

 

  1. dispatch_source_create 함수를 사용하여 dispatch source를 생성한다.
  2. dispatch source를 설정한다.
    • dispatch source에 이벤트 핸들러를 할당한다.
    • timer source의 경우 dispatch_source_set_timer 함수를 사용하여 타이머 정보를 설정한다.
  3. 선택적으로 dispatch source에 cancellation handler를 할당한다.
  4. dispatch_resume 함수를 호출하여 이벤트 처리를 시작한다.

 

dispatch source를 사용하려면 추가 설정이 필요하기 때문에 dispatch_source_create 함수는 일시 중지 된 상태의 dispatch source를 반환한다. 일시 중지 된 동안 dispatch source는 이벤트를 수신하지만 처리하지는 않는다. 이렇게하면 이벤트 핸들러를 설치하고 실제 이벤트를 처리하는 데 필요한 추가 설정을 수행할 수 있다.

 

다음 섹션에서는 dispatch source의 다양한 측면을 구성하는 방법을 보여준다.

 

Writing and Installing an Event Handler

 

dispatch source에서 생성 된 이벤트를 처리하려면 해당 이벤트를 처리 할 이벤트 핸들러를 정의해야 한다. 이벤트 핸들러는 dispatch_source_set_event_handler 또는 dispatch_source_set_event_handler_f 함수를 사용하여 dispatch source에 설치하는 함수 또는 블록 오브젝트이다. 이벤트가 도착하면 dispatch source는 처리를 위해 지정된 dispatch queue에 이벤트 핸들러를 전달한다.

 

이벤트 핸들러의 본문은 도착하는 모든 이벤트를 처리합니다. 이벤트 핸들러가 이미 큐에 있고 새 이벤트가 도착할 때 이벤트 처리를 기다리는 경우 dispatch source는 두 이벤트를 병합한다. 이벤트 핸들러는 일반적으로 가장 최근 이벤트에 대한 정보만 볼 수 있지만 dispatch source의 유형에 따라 발생하고 병합된 다른 이벤트에 대한 정보를 가져올 수도 있다. 이벤트 핸들러가 실행을 시작한 후 하나 이상의 새 이벤트가 도착하면 dispatch source는 현재 이벤트 핸들러가 실행을 완료 할 때까지 해당 이벤트를 유지한다. 이 시점에서 새로운 이벤트와 함께 이벤트 핸들러를 큐에 다시 전달한다.

 

함수 기반 이벤트 핸들러는 dispatch source object를 포함하는 single context pointer를 사용하고 값을 반환하지 않는다. 블록 기반 이벤트 핸들러는 매개 변수를 사용하지 않으며 반환 값도 없다.

 

// Block-based event handler
void (^dispatch_block_t)(void)

// Function-based event handler
void (*dispatch_function_t)(void *)

 

이벤트 핸들러 내에서는 dispatch source 자체에 지정된 이벤트에 대한 정보를 얻을 수 있다. 함수 기반 이벤트 핸들러는 매개 변수로 dispatch source에 대한 포인터를 전달하지만 블록 기반 이벤트 핸들러는 포인터 자체를 캡처해야 한다. 일반적으로 dispatch source를 포함하는 변수를 참조하여 블록에 대해 이를 수행 할 수 있다. 하단의 코드는 블록의 스코프 밖에서 선언 된 source의 변수를 캡처하는 예를 보여준다.

 

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
                                 myDescriptor, 0, myQueue);
dispatch_source_set_event_handler(source, ^{
   // Get some data from the source variable, which is captured
   // from the parent context.
   size_t estimated = dispatch_source_get_data(source);

   // Continue reading the descriptor...
});
dispatch_resume(source);

 

일반적으로 블록 내부의 변수를 캡처하면 유연성과 역동성이 향상된다. 물론 캡처된 변수는 기본적으로 블록 내에서 읽기 전용이다. 블록의 기능 중에는 특정 상황에서 캡처 된 변수를 수정할 수 있도록 지원하지만, dispatch source와 관련된 이벤트 핸들러에서는 수정할 수 없다. dispatch source는 항상 이벤트 핸들러를 비동기적으로 실행하므로 캡처한 변수의 정의 스코프는 이벤트 핸들러가 실행될 때 사라질 가능성이 높다.

 

하단의 표는 이벤트에 대한 정보를 얻기 위해 이벤트 핸들러 코드에서 호출할 수 있는 함수를 나열했다.

 

함수 설명
dispatch_source_get_handle 이 함수는 dispatch source가 관리하는 기본 시스템 데이터 유형을 반환한다.

descriptor dispatch source의 경우 이 함수는 dispatch source와 관련된 descriptor를 포함하는 int 유형을 반환한다.

signal dispatch source의 경우 이 함수는 가장 최근 이벤트에 대한 signal 번호를 포함하는 int 유형을 반환한다.

process dispatch source의 경우 이 함수는 모니터링 중인 프로세스에 대한 pid_t 데이터 구조를 반환한다.

Mach port dispatch source의 경우 이 함수는 mach_port_t 데이터 구조를 반환한다.

다른 dispatch source의 경우 이 함수에서 반환하는 값은 정의되지 않았다.
dispatch_source_get_data 이 함수는 이벤트와 관련 된 보류 중인 데이터를 반환한다.

파일에서 데이터를 읽는 descriptor dispatch source의 경우 이 함수는 읽을 수 있는 바이트 수를 반환한다.

파일에 데이터를 쓰는 descriptor dispatch source의 경우 쓰기에 사용 할 수 있는 공간이 있으면 이 함수는 양의 정수를 반환한다.

파일 시스템 활동을 모니터링하는 descriptor dispatch source의 경우 이 함수는 발생한 이벤트 유형을 나타내는 상수를 반환한다. 상수 목록은 dispatch_source_vnode_flags_t 열거형을 참조하시오.

process dispatch source의 경우이 함수는 발생한 이벤트 유형을 나타내는 상수를 반환한다. 상수 목록은 dispatch_source_proc_flags_t 열거형을 참조하시오.

Mach port dispatch source의 경우 이 함수는 발생한 이벤트 유형을 나타내는 상수를 반환한다. 상수 목록은 dispatch_source_machport_flags_t 열거형을 참조하시오.

custom dispatch source의 경우 이 함수는 기존 데이터에서 생성 된 새 데이터 값과 dispatch_source_merge_data 함수에 전달 된 새 데이터를 반환한다.
dispatch_source_get_mask 이 함수는 dispatch source를 만드는 데 사용된 이벤트 플래그를 반환한다.

process dispatch source의 경우 이 함수는 dispatch source가 수신하는 이벤트 마스크를 반환한다. 상수 목록은 dispatch_source_proc_flags_t 열거형을 참조하시오.

전송 권한이 있는 Mach port dispatch source의 경우 이 함수는 원하는 이벤트의 마스크를 반환한다. 상수 목록은 dispatch_source_mach_send_flags_t 열거형을 참조하시오.

custom OR dispatch source의 경우 이 함수는 데이터 값을 병합하는 데 사용되는 마스크를 반환한다.

 

Installing a Cancellation Handler

 

Cancellation handler는 릴리즈 되기 전에 dispatch source를 정리하는 데 사용된다. 대부분의 dispatch source 유형에서 cancellation handler는 선택 사항이며 업데이트 해야 하는 dispatch source에 연결된 일부 사용자 지정 동작이있는 경우에만 필요하다. 그러나 descriptor 혹은 Mach port를 사용하는 dispatch source의 경우 descriptor를 닫거나 Mach port를 해제하려면 cancellation handler를 제공해야 한다. 그렇게 하지 않으면 코드나 시스템의 다른 부분에서 의도치 않게 재사용되는 구조로 인해 코드에 미묘한 버그가 발생할 수 있다.

 

언제든지 cancellation handler를 설치할 수 있지만 일반적으로 dispatch source를 만들 때 설치한다. 구현에서 블록 오브젝트를 사용할 것인지 함수를 사용할지에 따라 dispatch_source_set_cancel_handler 또는 dispatch_source_set_cancel_handler_f 함수를 사용하여 cancellation handler를 설치한다. 하단의 코드는 dispatch source에 대해 열린 descriptor를 닫는 간단한 cancellation handler를 보여준다. fd 변수는 descriptor를 포함하는 캡처 된 변수이다.

 

dispatch_source_set_cancel_handler(mySource, ^{
   close(fd); // Close a file descriptor opened earlier.
});

 

Changing the Target Queue

 

dispatch source를 생성 할 때 이벤트 및 cancellation handler를 실행할 큐를 지정하더라도 dispatch_set_target_queue 함수를 사용하여 언제든지 해당 큐를 변경할 수 있다. 이렇게 하면 dispatch source의 이벤트가 처리되는 우선 순위를 변경할 수 있다.

 

dispatch source의 큐를 변경하는 것은 비동기 작업이며 dispatch source는 가능한 한 빨리 변경하기 위해 최선을 다한다. 이벤트 핸들러가 이미 큐에 있고 처리 대기 중이면 이전 큐에서 실행된다. 그러나 변경을 수행 할 즈음에 도착하는 다른 이벤트는 어느 큐에서든 처리 될 수 있다.

 

Associating Custom Data with a Dispatch Source

 

Grand Central Dispatch의 다른 많은 데이터 유형과 마찬가지로 dispatch_set_context 함수를 사용하여 사용자 정의 데이터를 dispatch source와 연결할 수 있다. 컨텍스트 포인터를 사용하여 이벤트 핸들러가 이벤트를 처리하는 데 필요한 데이터를 저장할 수 있다. 컨텍스트 포인터에 사용자 정의 데이터를 저장하는 경우 dispatch source가 더 이상 필요하지 않을 때 해당 데이터를 릴리즈하기 위해 cancellation handler (Installing a Cancellation Handler에서 설명한 대로)도 설치해야 한다.

 

블록을 사용하여 이벤트 핸들러를 구현하는 경우 로컬 변수를 캡처하여 블록 기반 코드 내에서 사용할 수도 있다. 이렇게하면 dispatch source의 컨텍스트 포인터에 데이터를 저장할 필요가 줄어들 수 있지만 항상 이 기능을 신중하게 사용해야 한다. dispatch source는 애플리케이션에서 오래 지속될 수 있으므로 포인터가 포함 된 변수를 캡처 할 때 주의해야 한다. 포인터가 가리키는 데이터가 언제든지 할당 해제 될 수 있는 경우 데이터를 복사하거나 유지하여 이러한 일이 발생하지 않도록 해야 한다. 두 경우 모두 나중에 데이터를 릴리즈하기 위해 cancellation handler를 제공해야 한다.

 

Memory Management for Dispatch Sources

 

다른 dispatch object와 마찬가지로 dispatch source는 참조 카운트 데이터 유형이다. dispatch source는 초기 참조 횟수가 1이며 dispatch_retain 및 dispatch_release 함수를 사용하여 리테인 및 릴리즈 할 수 있다. 큐의 참조 수가 0에 도달하면 시스템은 dispatch source 데이터 구조를 자동으로 할당 해제한다.

 

사용되는 방식 때문에, dispatch source의 소유권은 dispatch source 자체에 대해 내부적 또는 외부적으로 관리 될 수 있다. 외부 소유권을 사용하면 다른 오브젝트 또는 코드가 dispatch source의 소유권을 가져와 더 이상 필요하지 않을 때 이를 릴리즈하는 역할을 한다. 내부 소유권을 가진 dispatch source는 자체 소유이며 적절한 시간에 스스로를 릴리즈하는 역할을 한다. 외부 소유권을 사용하는 것이 매우 일반적이지만 자율적인 dispatch source를 만들고 추가 상호 작용 없이 코드의 일부 동작을 관리하도록 하려는 경우 내부 소유권을 사용할 수 있다. 예를 들어 dispatch source가 싱글 전역 이벤트에 응답하도록 설계된 경우 해당 이벤트를 처리한 다음 즉시 종료 할 수 있다.

 

Dispatch Source Examples

 

다음 섹션에서는 보다 일반적으로 사용되는 dispatch source를 만들고 구성하는 방법을 보여준다. 특정 유형의 dispatch source 설정에 대한 자세한 내용은 GCD (Grand Central Dispatch) 레퍼런스를 참조하시오.

 

Creating a Timer

 

Timer dispatch source는 정기적 인 시간 기반 간격으로 이벤트를 생성합니다. 타이머를 사용하여 정기적으로 수행해야 하는 특정 작업을 시작할 수 있다. 예를 들어 게임 및 기타 그래픽을 많이 사용하는 애플리케이션은 타이머를 사용하여 화면 또는 애니메이션 업데이트를 시작할 수 있다. 타이머를 설정하고 결과 이벤트를 사용하여 자주 업데이트되는 서버의 새로운 정보를 확인할 수도 있다.

 

모든 timer dispatch source는 time-based interval 이다. 즉, 일단 생성되면 지정한 간격으로 정기적인 이벤트를 전달한다. timer dispatch source를 생성할 때 지정해야 하는 값 중 하나는 타이머 이벤트에 대해 원하는 정확도를 시스템에 제공하기 위한 leeway value이다. Leeway value는 시스템이 전력을 관리하고 코어를 깨우는 방법에 약간의 유연성을 제공한다. 예를 들어, 시스템은 leeway value를 사용하여 이벤트 트리거의 시작 시간을 앞당기거나 지연시키고 다른 시스템 이벤트와 더 잘 맞출 수 있다. 따라서 가능한 한 자신의 타이머에 대해 leeway value를 지정해야 한다.

 

참고 : leeway value을 0으로 지정하더라도 요청한 정확한 나노초에 타이머가 작동될 것으로 예상해서는 안된다. 시스템은 당신의 요구를 수용하기 위해 최선을 다하지만 정확한 트리거의 시작 시간을 보장 할 수는 없다.

 

컴퓨터가 절전 모드로 전환되면 모든 timer dispatch source가 일시 중단된다. 컴퓨터가 절전 모드에서 해제되면 timer dispatch source도 자동으로 재시작된다. 타이머 설정에 따라 이러한 특성의 일시 중지는 다음 번에 타이머가 실행되는 시기에 영향을 미칠 수 있다. dispatch_time 함수 또는 DISPATCH_TIME_NOW 상수를 사용하여 timer dispatch source를 설정하는 경우 timer dispatch source는 기본 시스템 시계를 사용하여 실행되는 시기를 결정한다. 그러나 컴퓨터가 절전 모드인 동안에는 기본 타이머가 진행되지 않는다. 반대로 dispatch_walltime 함수를 사용하여 timer dispatch source를 설정하면 timer dispatch source는 실행 시간을 wall clock time까지 추적한다. 후자의 옵션은 일반적으로 이벤트 시간 사이에 너무 많은 드리프트가 발생하는 것을 방지하기 때문에 실행 간격이 상대적으로 큰 타이머에 적합하다.

 

하단의 코드는 30초마다 한 번씩 실행되고 leeway value가 1초인 타이머의 예를 보여준다. 타이머 간격이 상대적으로 크기 때문에 dispatch_walltime 함수를 사용하여 dispatch source를 생성한다. 타이머의 첫 번째 트리거는 즉시 발생하고 이후 이벤트는 30초마다 도착한다. MyPeriodicTask 및 MyStoreTimer 함수는 타이머 동작을 구현하고 애플리케이션의 데이터 구조에 타이머를 저장하기 위해 작성하는 사용자 지정 함수를 나타낸다.

 

dispatch_source_t CreateDispatchTimer(uint64_t interval,
              uint64_t leeway,
              dispatch_queue_t queue,
              dispatch_block_t block)
{
   dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                                     0, 0, queue);
   if (timer)
   {
      dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
      dispatch_source_set_event_handler(timer, block);
      dispatch_resume(timer);
   }
   return timer;
}

void MyCreateTimer()
{
   dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC,
                               1ull * NSEC_PER_SEC,
                               dispatch_get_main_queue(),
                               ^{ MyPeriodicTask(); });

   // Store it somewhere for later use.
    if (aTimer)
    {
        MyStoreTimer(aTimer);
    }
}

 

timer dispatch source를 생성하는 것이 시간 기반 이벤트를 수신하는 주요 방법이지만 사용할 수 있는 다른 옵션도 있다. 지정된 시간 간격 후에 한 번 블록을 수행하려면 dispatch_after 또는 dispatch_after_f 함수를 사용할 수 있다. 이 함수는 블록을 큐에 제출할 시간 값을 지정할 수 있다는 점을 제외하면 dispatch_async 함수와 매우 유사하게 작동한다. 시간 값은 필요에 따라 상대 또는 절대 시간 값으로 지정할 수 있다.

 

Reading Data from a Descriptor

 

파일 또는 소켓에서 데이터를 읽으려면 파일 또는 소켓을 열고 DISPATCH_SOURCE_TYPE_READ 유형의 dispatch source를 만들어야 한다. 지정한 이벤트 핸들러는 file descriptor의 내용을 읽고 처리 할 수 있어야 한다. 파일의 경우 파일 데이터(또는 해당 데이터의 하위 집합)를 읽고 애플리케이션에 적합한 데이터 구조를 만드는 것과 같다. 네트워크 소켓의 경우 새로 수신 된 네트워크 데이터 처리가 포함된다.

 

데이터를 읽을 때마다 항상 nonblocking operation을 사용하도록 descriptor를 설정해야 한다. dispatch_source_get_data 함수를 사용하여 읽을 수 있는 데이터의 양을 확인할 수 있지만 해당 함수에서 반환하는 숫자는 호출한 시간과 실제로 데이터를 읽는 시간 사이에 변경 될 수 있다. 기본 파일이 잘리거나 네트워크 오류가 발생하는 경우 현재 스레드를 차단하는 descriptor에서 읽으면 실행 중 이벤트 처리기가 중단되고 dispatch queue가 다른 태스크를 dispatch 하지 못할 수 있다. serial queue의 경우 큐를 교착 상태로 만들 수 있으며 concurrent queue의 경우에도 시작할 수 있는 새 태스크의 수가 줄어든다.

 

하단의 코드는 파일에서 데이터를 읽도록 dispatch source를 설정하는 예를 보여준다. 이 예제에서 이벤트 핸들러는 지정된 파일의 전체 내용을 버퍼로 읽고 데이터를 처리하기 위해 사용자 정의 함수(사용자 고유 코드에서 정의)를 호출한다. (이 함수의 호출자는 읽기 작업이 완료된 후 반환된 dispatch source를 사용하여 이를 취소한다.) 읽을 데이터가 없을 때 dispatch queue가 불필요하게 차단되지 않도록 하기 위해 이 코드에서는 fcntl 함수를 사용하여 nonblocking operation을 수행하기 위한 file descriptor를 구성한다. dispatch source에 설치된 cancellation handler는 데이터를 읽은 후 file descriptor가 닫히도록 한다.

 

dispatch_source_t ProcessContentsOfFile(const char* filename)
{
   // Prepare the file for reading.
   int fd = open(filename, O_RDONLY);
   if (fd == -1)
      return NULL;
   fcntl(fd, F_SETFL, O_NONBLOCK);  // Avoid blocking the read operation

   dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
   dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
                                   fd, 0, queue);
   if (!readSource)
   {
      close(fd);
      return NULL;
   }

   // Install the event handler
   dispatch_source_set_event_handler(readSource, ^{
      size_t estimated = dispatch_source_get_data(readSource) + 1;
      // Read the data into a text buffer.
      char* buffer = (char*)malloc(estimated);
      if (buffer)
      {
         ssize_t actual = read(fd, buffer, (estimated));
         Boolean done = MyProcessFileData(buffer, actual);  // Process the data.

         // Release the buffer when done.
         free(buffer);

         // If there is no more data, cancel the source.
         if (done)
            dispatch_source_cancel(readSource);
      }
    });

   // Install the cancellation handler
   dispatch_source_set_cancel_handler(readSource, ^{close(fd);});

   // Start reading the file.
   dispatch_resume(readSource);
   return readSource;
}

 

위의 코드에서 custom MyProcessFileData 함수는 충분한 파일 데이터를 읽은 후 dispatch source를 취소 할 수 있는 시기를 결정한다. 기본적으로 설명자에서 읽도록 구성된 dispatch source는 읽을 데이터가 있는 동안 이벤트 핸들러를 반복적으로 예약한다. 소켓 연결이 닫히거나 파일 끝에 도달하면 dispatch source가 이벤트 핸들러 예약을 자동으로 중지한다. dispatch source가 필요하지 않다는 것을 알고 있다면 직접 취소 할 수 있다.

 

Writing Data to a Descriptor

 

파일이나 소켓에 데이터를 쓰는 프로세스는 데이터를 읽는 프로세스와 매우 유사하다. 쓰기 operation에 대한 descriptor를 설정한 후 DISPATCH_SOURCE_TYPE_WRITE 유형의 dispatch source를 생성한다. dispatch source가 생성되면 시스템은 이벤트 핸들러를 호출하여 파일 또는 소켓에 데이터 쓰기를 시작할 수 있는 기회를 제공한다. 데이터 쓰기가 끝나면 dispatch_source_cancel 함수를 사용하여 dispatch source를 취소하시오.

 

데이터를 쓸 때마다 항상 nonblocking operation을 사용하도록 file descriptor를 설정해야 한다. dispatch_source_get_data 함수를 사용하여 쓰기에 사용할 수 있는 공간을 확인할 수 있지만 해당 함수에서 반환하는 값은 권장 사항일 뿐이며 호출하는 시간과 실제로 데이터를 쓰는 시간 사이에 변경 될 수 있다. 오류가 발생한 상황에서 blocking file descriptor에 데이터를 쓰면 실행 중에 이벤트 핸들러가 중단되고 dispatch queue가 다른 태스크를 dispatch 하지 못할 수 있다. serial queue의 경우 큐를 교착 상태로 만들 수 있으며 concurrent queue의 경우에도 시작할 수있는 새 태스크의 수가 줄어든다.

 

하단의 코드는 dispatch source를 사용하여 파일에 데이터를 쓰는 기본 접근 방식을 보여준다. 새 파일을 생성한 후 이 함수는 resulting file descriptor를 이벤트 처리기에 전달한다. 파일에 넣는 데이터는 MyGetData 함수에 의해 제공되며 파일에 대한 데이터를 생성하는 데 필요한 코드로 대체한다. 데이터를 파일에 쓴 후 이벤트 핸들러는 dispatch source를 취소하여 다시 호출되지 않도록 한다. 그러면 dispatch source의 소유자가이를 릴리즈 할 책임이 있다.

 

dispatch_source_t WriteDataToFile(const char* filename)
{
    int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,
                      (S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
    if (fd == -1)
        return NULL;
    fcntl(fd, F_SETFL); // Block during the write.

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE,
                            fd, 0, queue);
    if (!writeSource)
    {
        close(fd);
        return NULL;
    }

    dispatch_source_set_event_handler(writeSource, ^{
        size_t bufferSize = MyGetDataSize();
        void* buffer = malloc(bufferSize);

        size_t actual = MyGetData(buffer, bufferSize);
        write(fd, buffer, actual);

        free(buffer);

        // Cancel and release the dispatch source when done.
        dispatch_source_cancel(writeSource);
    });

    dispatch_source_set_cancel_handler(writeSource, ^{close(fd);});
    dispatch_resume(writeSource);
    return (writeSource);
}

 

Monitoring a File-System Object

 

파일 시스템 오브젝트의 변경 사항을 모니터링하려면 DISPATCH_SOURCE_TYPE_VNODE 유형의 dispatch source를 설정할 수 있다. 이 유형의 dispatch source를 사용하여 파일이 삭제 혹은 작성되거나 이름이 변경될 때 알림을 받을 수 있다. 파일에 대한 특정 유형의 메타 정보 (예 : 크기 및 링크 수)가 변경 될 때 경고를 받는 데 사용할 수도 있다.

 

참고 : dispatch source에 대해 지정한 file descriptor는 소스 자체가 이벤트를 처리하는 동안 열려 있어야 한다.

 

하단의 코드는 파일의 이름 변경을 모니터링하고 변경시 사용자 지정 동작을 수행하는 예를 보여준다. (예제에서 호출 된 MyUpdateFileName 함수 대신 실제 동작을 제공해야 한다.) dispatch source에 대해 특별히 descriptor가 열리기 때문에 dispatch source에는 descriptor를 닫는 cancellation handler가 포함된다. 예제에서 만든 file descriptor가 기본 파일 시스템 개체와 연결되어 있기 때문에 동일한 dispatch source를 사용하여 파일 이름 변경을 감지 할 수 있다.

 

dispatch_source_t MonitorNameChangesToFile(const char* filename)
{
   int fd = open(filename, O_EVTONLY);
   if (fd == -1)
      return NULL;

   dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
   dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,
                fd, DISPATCH_VNODE_RENAME, queue);
   if (source)
   {
      // Copy the filename for later use.
      int length = strlen(filename);
      char* newString = (char*)malloc(length + 1);
      newString = strcpy(newString, filename);
      dispatch_set_context(source, newString);

      // Install the event handler to process the name change
      dispatch_source_set_event_handler(source, ^{
            const char*  oldFilename = (char*)dispatch_get_context(source);
            MyUpdateFileName(oldFilename, fd);
      });

      // Install a cancellation handler to free the descriptor
      // and the stored string.
      dispatch_source_set_cancel_handler(source, ^{
          char* fileStr = (char*)dispatch_get_context(source);
          free(fileStr);
          close(fd);
      });

      // Start processing events.
      dispatch_resume(source);
   }
   else
      close(fd);

   return source;
}

 

Monitoring Signals

 

UNIX signal을 사용하면 도메인 외부에서 애플리케이션을 조작 할 수 있다. 애플리케이션은 복구 할 수 없는 오류 (예 : 불법 명령)부터 중요한 정보에 대한 알림(예 : 하위 프로세스가 종료되는 경우)에 이르기까지 다양한 유형의 signal을 수신 할 수 있다. 전통적으로 애플리케이션은 signal이 도착하는 즉시 동기식으로 signal을 처리하는 signal handler 함수를 설치하기 위해 sigaction 함수를 사용한다. signal의 도착에 대한 알림만 받고 실제로 signal을 처리하고 싶지 않은 경우 signal dispatch source를 사용하여 signal을 비동기적으로 처리 할 수 있다.

 

signal dispatch source는 sigaction 함수를 사용하여 설치한 synchronous signal handler를 대체하지 않는다. synchronous signal handler는 실제로 signal을 포착하여 애플리케이션이 종료되는 것을 방지 할 수 있다. signal dispatch source을 사용하면 signal의 도착만 모니터링 할 수 있다. 또한 signal dispatch source을 사용하여 모든 유형의 signal을 검색 할 수는 없다. 특히 SIGILL, SIGBUS 및 SIGSEGV signal을 모니터링하는 데 사용할 수 없다.

 

signal dispatch source는 dispatch queue에서 비동기적으로 실행되기 때문에 synchronous signal handler와 동일한 제한이 없다. 예를 들어 signal dispatch source의 이벤트 핸들러에서 호출 할 수 있는 함수에는 제한이 없다. 이러한 유연성 증가의 단점은 signal가 도착하는 시간과 dispatch source의 이벤트 핸들러가 호출되는 시간 사이에 약간의 지연 시간이 증가 할 수 있다는 것이다.

 

하단의 코드는 SIGHUP signal을 처리하기 위해 signal dispatch source을 설정하는 방법을 보여준다. dispatch source의 이벤트 핸들러는 MyProcessSIGHUP 함수를 호출한다. 이 함수는 애플리케이션에서 signal을 처리하는 코드로 대체한다.

 

void InstallSignalHandler()
{
   // Make sure the signal does not terminate the application.
   signal(SIGHUP, SIG_IGN);

   dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
   dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGHUP, 0, queue);

   if (source)
   {
      dispatch_source_set_event_handler(source, ^{
         MyProcessSIGHUP();
      });

      // Start processing signals
      dispatch_resume(source);
   }
}

 

custom framework 용 코드를 개발하는 경우 signal dispatch source 사용의 장점은 코드가 연결된 모든 애플리케이션과 독립적으로 signal을 모니터링 할 수 있다는 것이다. signal dispatch source는 애플리케이션에 설치되었을 수 있는 다른 dispatch source 또는 synchronous signal handler를 방해하지 않는다.

 

Monitoring a Process

 

process dispatch source를 사용하면 특정 프로세스의 동작을 모니터링하고 적절하게 대응할 수 있다. 상위 프로세스는 process dispatch source를 사용하여 생성하는 모든 하위 프로세스를 모니터링 할 수 있다. 예를 들어, 상위 프로세스는 이를 사용하여 하위 프로세스의 사망을 감시 할 수 있다. 마찬가지로 하위 프로세스는 이를 사용하여 상위 프로세스를 모니터링하고 상위 프로세스가 종료되면 종료 할 수 있다.

 

하단의 코드는 상위 프로세스의 종료를 모니터링하기 위해 dispatch source를 설치하는 단계를 보여준다. 상위 프로세스가 종료되면 dispatch source는 하위 프로세스가 종료해야 함을 알 수 있도록 내부 상태 정보를 설정한다. (당신의 애플리케이션은 MySetAppExitFlag 함수를 구현하여 종료를 위한 적절한 플래그를 설정해야 한다.) dispatch source는 자율적으로 실행되어 자체 소유가 되기 때문에 프로그램 종료를 예상하여 스스로를 취소하고 릴리즈한다.

 

void MonitorParentProcess()
{
   pid_t parentPID = getppid();

   dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
   dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
                                                      parentPID, DISPATCH_PROC_EXIT, queue);
   if (source)
   {
      dispatch_source_set_event_handler(source, ^{
         MySetAppExitFlag();
         dispatch_source_cancel(source);
         dispatch_release(source);
      });
      dispatch_resume(source);
   }
}

 

Canceling a Dispatch Source

 

dispatch_source_cancel 함수를 사용하여 명시적으로 취소 할 때까지 dispatch source는 활성 상태로 유지된다. dispatch source를 취소하면 새 이벤트 전달이 중지되고 취소 할 수 없다. 따라서 일반적으로 다음과 같이 dispatch source를 취소 한 다음 즉시 릴리즈한다.

 

void RemoveDispatchSource(dispatch_source_t mySource)
{
   dispatch_source_cancel(mySource);
   dispatch_release(mySource);
}

 

dispatch source의 취소는 비동기 작업이다. dispatch_source_cancel 함수를 호출 한 후 새 이벤트가 처리되지 않더라도 dispatch source에서 이미 처리중인 이벤트는 계속 처리된다. 최종 이벤트 처리를 완료 한 후 dispatch source는 cancellation handler가 있는 경우 이를 실행한다.

 

cancellation handler는 메모리 할당을 해제하거나 dispatch source를 대신하여 획득 한 리소스를 정리할 수 있는 기회이다. dispatch source가 descriptor 또는 mach port를 사용하는 경우 cancellation handler를 제공하여 descriptor를 닫거나 취소가 발생할 때 포트를 제거해야 한다. 다른 유형의 dispatch source에는 cancellation handler가 필요하지 않지만 메모리나 데이터를 dispatch source와 연결하는 경우에는 여전히 제공해야 한다. 예를 들어 dispatch source의 컨텍스트 포인터에 데이터를 저장하는 경우 하나를 제공해야 한다.

 

Suspending and Resuming Dispatch Sources

 

dispatch_suspend 및 dispatch_resume 메서드를 사용하여 dispatch source event의 전달을 일시 중단하고 재개 할 수 있다. 이러한 메서드는 dispatch object의 일시 중단 횟수를 늘리거나 줄인다. 따라서 이벤트 전달이 다시 시작되기 전에 dispatch_suspend에 대한 호출과 dispatch_resume에 대한 호출이 일치하도록 균형을 맞춰야 한다.

 

dispatch source를 일시 중지하면 해당 dispatch source가 일시 중지된 동안 발생하는 모든 이벤트는 큐가 재개 될 때까지 누적된다. 큐가 다시 시작되면 모든 이벤트를 전달하지 않고 전달하기 전에 이벤트가 단일 이벤트로 병합된다. 예를 들어 파일에서 이름 변경을 모니터링하는 경우 전달 된 이벤트에 마지막 이름 변경만 포함된다. 이러한 방식으로 이벤트를 병합하면 작업이 재개 될 때 이벤트가 큐에 쌓이고 애플리케이션이 오버되는 것을 방지 할 수 있다.

 

[원문]

 

Dispatch Sources

Dispatch Sources Whenever you interact with the underlying system, you must be prepared for that task to take a nontrivial amount of time. Calling down to the kernel or other system layers involves a change in context that is reasonably expensive compared

developer.apple.com