Introdução ao Grand Central Dispatch

January 30, 2011

O GCD (Grand Central Dispatch), ou libdispatch, é uma biblioteca criada pela Apple para facilitar o uso de múltiplos processadores. A libdispatch torna transparente para o desenvolvedor a criação e a manutenção de threads, facilitanto a paralelização de trechos do código. Em 2009 ela foi liberada sob a licença Apache 2.0 e funciona em diversos sistemas operacionais como o Mac OSX, iOS, FreeBSD e Linux. O GCD está disponível para C/C++, Obj-C e Ruby.

O GCD agrupa todas as terefas em uma fila concorrente e simultaneamente as distribui entre os processadores disponíveis. Pode-se também utilizar filas seriais, onde não existe concorrência entre as tarefas. Para enviar uma tarefa para a fila, é necessário que ela seja encapsulada dentro de um bloco. Um bloco (Block) é uma extensão da linguagem C, algo parecido com o trecho abaixo:

typedef void (^UmBlocoVoid)();

UmBlocoVoid bloco = ^{
    printf("Dentro de um Block\n");
    int i;
    for(i=0;i<100;i++){
        printf("Exemplo %d\n", i);
    }
};

Também é possível usar funções ao invés de Blocks. Para compilar uma aplicação usando estruturas de bloco como essa, é necessário usar o compilador Clang/LLVM com o parâmetro -fblocks, no Mac OSX o GCC suporta blocks com o uso do mesmo parâmetro. No Ubuntu eu precisei usar -fblocks -lBlocksRuntime para funcionar.

Existem 3 tipos de filas no GCD:

  • Concurrent: Também conhecida como global dispatch queue, o que é colocado nesta fila é processado paralelamente pelo GCD;
  • Serial: Também conhecida como private dispatch queue, o que é colocado nesta fila é processado sequencialmente (sem paralelismo);
  • main dispatch queue: O que é colocado nesta fila é processado na thread principal do programa, não em paralelo ao programa.

Instalando o GCD

O GCD já faz parte do Mac OSX, logo não é necessário a instalação. No FreeBSD basta usar o ports, acesse o diretório /usr/ports/devel/libdispatch e dê um make install clean, o Clang/LLVM também será instalado (no FreeBSD 9 o Clang/LLVM já vem no sistema). No Debian é possível instalar rodando um apt-get install libdispatch0

Criando filas no GCD

Para usar o GCD é necessário incluir o header dispatch.h no seu código e usar o parâmetro -ldispatch no compilador, no Mac OSX esse parâmetro é desnecessário.

#include <dispatch/dispatch.h>

O trecho de código abaixo cria 3 filas, uma concorrente, uma serial e uma main dispatch queue:

dispatch_queue_t queue1;
dispatch_queue_t queue2;
dispatch_queue_t queue3;

queue1 = dispatch_get_global_queue(0,0);
queue2 = dispatch_queue_create("minha fila", 0);
queue3 = dispatch_get_main_queue();

A função dispatch_get_global_queue() cria uma fila concorrente e aceita dois parâmetros, o primeiro é a prioridade da fila, que pode ser: DISPATCH_QUEUE_PRIORITY_LOW (valor -2), DISPATCH_QUEUE_PRIORITY_DEFAULT (valor 0) e DISPATCH_QUEUE_PRIORITY_HIGH (valor 2). Usei o valor ao invés do nome para caber em uma linha :P . O segundo parâmetro não tem função nenhuma, está reservado para implementação futura.

A função dispatch_queue_create() cria uma fila serial e aceita dois parâmetros também, o primeiro é um label para a fila e o segundo não tem função ainda.

A função dispatch_get_main_queue() cria uma fila que usa a thread principal do programa para a execução e não recebe parâmetros.

Enviando tarefas para as filas

Para enviar tarefas para a fila para serem executadas assincronamente você pode usar a função dispatch_async(), para executar de maneira síncrona use dispatch_sync(). Esta função recebe dois parâmetros, o primeiro é a fila e o segundo é um bloco que será executado. O código abaixo cria uma fila concorrente e envia duas tarefas para ela, neste caso converter dois arquivos MP3 (uma função hipotética):

#include <stdio.h>
#include <dispatch/dispatch.h>

int main(void)
{
    dispatch_queue_t queue;

    queue = dispatch_get_global_queue(0,0);

    dispatch_async(queue, ^{
        mp3_converter("/tmp/musica1.mp3");
    });

    dispatch_async(queue, ^{
        mp3_converter("/tmp/musica2.mp3");
    });

    return(0);
}

Este código irá distribuir esses dois procedimentos entre os processadores da máquina. Mas existe um problema aí, como a função usada é assíncrona, o programa vai terminar imediatamente, sem processar nada, pois em nenhum momento foi dado um join nas threads. Para resolver esse problema o GCD implementas os grupos, com os grupos é possível dar join nas threads e aguardar a execução dos procedimentos. Para isso precisamos criar um grupo, enviar as tarefas para a fila relacionando-as com o grupo e aguardar a execução usando respectivamente as funcões dispatch_group_create(), dispatch_group_async() e dispatch_group_wait()

É possível executar tarefas de maneira síncrona, com a função dispatch_sync(), a maneira de usar é a mesma da função dispatch_async(), a diferença é que agora o programa ficará parado esperando a execução terminar.

Usando grupos

O código abaixo é igual ao anterior, mas implementado usando grupos:

#include <stdio.h>
#include <dispatch/dispatch.h>

int main(void)
{
    dispatch_queue_t queue;

    queue = dispatch_get_global_queue(0,0);

    dispatch_group_t grupo;
    grupo = dispatch_group_create();

    dispatch_group_async(grupo, queue, ^{
        mp3_converter("/tmp/musica1.mp3");
    });

    dispatch_group_async(grupo, queue, ^{
        mp3_converter("/tmp/musica2.mp3");
    });

    dispatch_group_wait(grupo, DISPATCH_TIME_FOREVER);

    return(0);
}

A função dispatch_group_async() recebe três parâmetros, o primeiro é o grupo, o segundo é a fila e o terceiro é o bloco que será executado. A função dispatch_group_wait() recebe dois parâmetros, o primeiro é o grupo e o segundo o tempo de espera.

Paralelizando loops

É possível paralelizar a execução de um loop com a função dispatch_apply(), cada iteração do loop terá seu bloco enviado para um processador diferente, veja um exemplo de conversão de arquivos MP3:

dispatch_queue_t fila;
fila = dispatch_get_global_queue(0,0);

dispatch_apply(mp3_num(musicas), fila, ^(size_t i) {
    mp3_converter(musicas[i]);
});

Esta função recebe três parâmetros, o primeiro é a quantidade de iterações que serão executadas (a função hipotética mp3_num() devolve a quantidade de músicas na matriz), o segundo é a fila e o terceiro o bloco. O bloco necessita ser declarado junto com um iterador, que pode ser usado dentro do bloco, como no exemplo. Este código irá percorrer um vetor com nomes de músicas e irá distribuir o procedimento entre todos os processadores disponíveis.

Usando semáforos

O GCD também possui uma implementação de semáforos (usados para serializar alguma parte do código). O trecho de código abaixo cria um semáforo para proteger uma área crítica do código:

dispatch_queue_t fila;
dispatch_semaphore_t sem;

fila = dispatch_get_global_queue(0,0);
sem = dispatch_semaphore_create(1);

dispatch_apply(100, fila, ^(size_t i) {
    funcao_nao_critica();
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    funcao_com_codigo_critico();
    dispatch_semaphore_signal(sem);
});

O semáforo é iniciado com a função dispatch_semaphore_create() que recebe o tamanho do semáforo como parâmetro. A função dispatch_semaphore_wait() decrementa o contador do semáforo e recebe dois parâmetros, o primeiro é o semáforo e o segundo o tempo de espera. A função dispatch_semaphore_signal() libera o semáforo e recebe o próprio semáforo como parâmetro.

Por hora eu fico por aqui, num próximo artigo pretendo falar sobre os dispatch sources que facilitam o tratamento de eventos no sistema, como por exemplo quando existem dados prontos para serem lidos em um file descriptor, quando um processo recebe um sinal ou ainda criar rotinas periódicas no seu sistema.

Referências

libdispatch.macosforge.org
Introducing Blocks and Grand Central Dispatch
Concurrency Programming Guide
Grand Central Dispatch (GCD) Reference
Grand Central Dispatch (GCD) on FreeBSD

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.