Grand Central Dispatch

Marcio Rocha dos Santos

Trabalho de conclusão de curso Orientador: Prof. Dr. Alfredo Goldman vel Lejbman Co-Orientador: Emilio De Camargo Francesquini

São Paulo, Dezembro de 2010

Sumário

I Parte técnica1

1 Introdução3

2 História 5

3 Conceitos 9 3.1 Programação paralela...... 9 3.2 Speedup e eficiência...... 10 3.3 Programação concorrente...... 11

4 Grand Cental Dispatch 13 4.1 Block Objects...... 14 4.1.1 Sintaxe...... 15 4.2 Dispatch queues...... 15 4.3 Synchronization...... 16 4.4 Event sources...... 18 4.5 Leitura e construção do código...... 19 iv SUMÁRIO

5 Atividades realizadas 21 5.1 Explorando as funcionalidades...... 21 5.2 Testes de desempenho e Resultados obtidos...... 22 5.2.1 Tarefas crescentes...... 22 5.2.2 Tarefas curtas...... 24 5.2.3 Divisão do trabalho...... 26

6 Conclusões 29

II Parte subjetiva 31

7 Experiência pessoal 33 7.1 Estudos futuros...... 34 7.2 Disciplinas relevantes...... 34

A Códigos de comparação 39

B Código de implementação de concorrência 41 Parte I

Parte técnica

Capítulo 1

Introdução

A velocidade dos processadores, que vinham seguindo a Lei de Moore [9], não demorou a esbarrar num determinado conjunto de limitações físicas, das quais uma denominou-se por power wall ou “barreira de potência”, pois devido a um tráfego intenso de energia atraés de cada chip, bem como da dissipação dessa energia em forma de calor, de maneira que cada chip alcançasse a temperatura de fusão do que o compunha, dessa forma aruinando-o completamente. Os sistemas atuais de arrefecimento já estão com certa dificuldade de dissipar esse calor de um modo eficiente e barato. Até a década de 80, computadores com um único processador ainda tinham boa par- ticipação na lista dos top 500 [10], porém desde 1996 essa lista é preenchida apenas com computadores de arquitetura multicore. Como a demanda por computadores de alto desempenho continua crescendo, a indústria mudou para chips com múltiplos núcleos e clocks mais baixos, que podem fornecer maior poder de processamento, consumindo menos energia. Mas para tirar o máximo proveito desses processadores, um novo modo de programar deve acompanhar o desenvolvimento da nova arquitetura. Entretanto, mesmo com os avanços na área de hardware, a computação paralela não tem se desenvolvido rápido o suficiente. A computação enfrenta uma crise com a falta de programadores qualificados nessa área e de ferramentas que auxiliem a paralelização. Muitos dos programas atuais que se utilizam de dois ou quatro processadores não tirarão melhor proveito dos manycores do futuro com dezenas de núcleos. 4 Introdução

A programação paralela é difícil com a gestão manual de threads. No entanto, os me- lhores programadores do mundo são pressionados a criar grandes programas multithread em linguagens de baixo nível como ou C + +, garantindo exclusão mútua, sem ocorrência de deadlock, condição de corrida, e outros perigos inerentes à programação concorrente. Aplica- ções extremamente cuidadosas de bloqueio primitivo se fazem necessárias para evitar queda de desempenho e bugs, ao lidar com dados compartilhados. Portanto nosso problema é: o usuário impedido de usar todos recursos computacionais, mesmo que estejam ociosos, pois os programas não sabem lidar com esses recursos. Estudaremos uma solução da Apple, já implementada na versão Snow Leopard do MAC OS X, que promete tornar o trabalho dos desenvolvedores de software mais fácil, utili- zando os recursos do hardware ao máximo. Essa solução da Apple se chamada Grand Central Dispatch (GCD) [3], uma biblioteca C introduzida nos níveis mais baixos do sistema operaci- onal. Capítulo 2

História

A primeira geração de computadores estabeleceu os conceitos básicos de organização dos computadores eletrônicos, e na década de 50 apareceu o modelo computacional que se tornaria a base de todo o desenvolvimento subsequente, chamado "modelo de von Neumann"(figura 2.1). O modelo de von Neumann é composto basicamente por processador, memória e dis- positivos de E/S. O processador executa instruções sequencialmente, de acordo com a ordem ditada por uma unidade de controle.

Figura 2.1: Arquitetura de von Neumann

O problema dessa arquitetura chama-se gargalo de von Neumann, que é a limitação da taxa de transferência entre a memória e a CPU. Quando as arquiteturas eram mais simples, 6 História funcionava bem. Porém, foram surgindo necessidades de melhora, tais como o aumentar a velocidade dos dispositivos, o que depende do desenvolvimento tecnológico, e executar tarefas em paralelo. O avanço dos dispositivos foi acontecendo de forma gradual até os anos 80, com o surgi- mento da filosofia VLSI (Very-large-scale integration)[11]. VLSI é o processo de criação de circuitos integrados, combinando milhares de transistores em um único chip. Historicamente, a utilização de paralelismo começou pelo hardware. O aumento da velocidade do hardware, por mais rápida que possa ser, sempre esbarra no limite tecnológico de cada época. Dessa forma, desde o início do desenvolvimento dos computadores, percebeu-se que a utilização de paralelismo é essencial. A paralelização proporcionou grandes mudanças nas arquiteturas, que evoluíram ao longo dos anos com o surgimento de diversas características nas máquinas, tais como:

· em 1953, surgiu o conceito de palavra, levando à manipulação de vários bits em paralelo, ao invés da unidade;

· em 1958, processadores de E/S possibilitaram o paralelismo entre E/S e processamento, tornando a tarefa de E/S mais independente;

· em 1963, como memória também era o gargalo do sistema, surgiram técnicas como cache e interleave (que permite a obtenção de mais de uma palavra na memória, em paralelo);

· em 1970, aparecimento de pipelines, paralelizando a execução de instruções, operações aritméticas, etc;

· no período de 1953 a 1970, o paralelismo existente é transparente ao usuário, influen- ciando apenas na melhoria do desempenho da máquina, sendo denominado paralelismo de baixo nível.

· em 1975, nasceu o Illiac IV, com 64 processadores, primeira forma em que o paralelismo diferia do de baixo nível. Foi o computador mais rápido até o aparecimento do Cray; 7

· em 1984, surgimento da filosofia VLSI de projeto de microcomputadores, oferecendo maior facilidade e menor custo no projeto de microprocessadores cada vez mais comple- xos e menores (computadores pessoais).

A partir do aparecimento dos computadores pessoais e a redução dos custos dos compo- nentes, foi possível colocar dois ou mais processadores em máquinas paralelas. E enfim o surgimento, em 2005, dos processadores multicore no mercado, com o IntelrPentiumrD, deu início a era da arquitetura multicore. 8 História Capítulo 3

Conceitos

3.1 Programação paralela

A programação sequencial consiste na execução de várias tarefas, uma após a outra. A programação concorrente é definida como a execução simultânea de várias instruções. Pode também ser formada por várias instruções que foram iniciadas em determinado instante e ainda não finalizaram. A programação concorrente pode ser executada tanto em um só processador, explorando o que se chama de pseudoparalelismo, quanto em uma arquitetura paralela, com vários processadores. No caso de vários processadores, essa programação passa a incorporar a chamada programação paralela, que se caracteriza pela execução de várias tarefas em um mesmo instante. Embora relacionada com programação paralela, programação concorrente foca mais na interação entre as tarefas. Algumas definições de programação paralela:

- Almasi, G. S. [12]: "Coleção de elementos de processamento que se comunicam e coope- ram entre si e resolvem um problema mais rapidamente".

- Quinn, M. J. [13]: "Processamento de informação que enfatiza a manipulação concor- rente de dados que pertencem a um ou mais processos que resolvem um único problema".

- Hwang, K. [14]: "Forma eficiente do processamento de informações com ênfase na ex- ploração de eventos concorrentes no processo computacional" 10 Conceitos

As vantagens da utilização de programação paralela consistem em:

· Alto desempenho, minimizando com o chamado gargalo de von Neumann. É o objetivo principal da programação paralela. A divisão de um problema em diversas tarefas pode levar a uma redução considerável no tempo de execução;

· Substituição dos computadores de altíssimo custo. As arquiteturas paralelas apresentam um custo menor que uma arquitetura com um único processador e a mesma capacidade;

· Tolerância a falhas, pois a programação paralela facilita a implementação de mecanismos de tolerância a falhas;

· Modularidade, sendo que um programa longo pode ser subdividido em várias tarefas que executam independentemente e se comunicam entre si.

· Economia de energia. Reduzindo o clock dos processadores, mesmo com quantidades maiores de processadores ou núcleos, é possível ter maior poder de processamento gas- tando menos energia.

O uso de programação paralela também tem desvantagens. Podemos citar a programação mais complexa, pois acarreta em uma série de problemas inexistentes na programação sequen- cial, tais como comunicação, sincronismo, balanceamento de carga, etc. Também pode ser introduzido sobrecargas para suprir fatores como comunicação e sincronismo entre as tarefas em paralelo. Essas sobrecargas devem ser cuidadosamente analisadas, pois impedem que seja alcançado o speedup ideal.

3.2 Speedup e eficiência

Uma característica fundamental da computação paralela trata-se do aumento de veloci- dade de processamento através da utilização do paralelismo. Neste contexto, duas medidas importantes para a verificação da qualidade de algoritmos paralelos são speedup e eficiência. Uma definição largamente aceita para speedup é: aumento de velocidade observado quando se 3.3 Programação concorrente 11 executa um determinado processo em p processadores em relação à execução deste processo em 1 processador. Então, tem-se: T speedup = 1 Tp onde,

· T1 = tempo de execução em 1 processador;

· Tp = tempo de execução em p processadores.

Idealmente, o ganho de speedup deveria tender a p, que seria o seu valor ideal. Porém, três fatores podem ser citados que influenciam essa relação: sobrecarga da comunicação entre os processadores, partes do algoritmo não paralelizáveis e o nível de paralelismo inadequado na arquitetura de software. Outra medida importante é a eficiência, que trata da relação entre o speedup e o número de processadores.

speedup Eficiencia = p

No caso ideal (speedup = p), a eficiência seria máxima e teria valor 1 (100%).

3.3 Programação concorrente

Um programa sequencial é composto por um conjunto de instruções que são executadas sequencialmente, sendo que a execução dessas instruções é denominada um processo. Um programa concorrente especifica dois ou mais programas sequenciais que podem ser executados concorrentemente como processos paralelos [1]. A programação concorrente existe para que se forneçam ferramentas para a construção de programas paralelos, de maneira que se consiga melhor desempenho e melhor utilização do hardware paralelo disponível. A computação paralela apresenta vantagens importantes em relação à computação sequencial, como foi exposto acima, e todas essas vantagens podem ser citadas como pontos de incentivo para o uso da programação concorrente. Um programa sequencial é constituído basicamente de um conjunto de construções já bem 12 Conceitos dominadas pelo programador em geral, como por exemplo, atribuições, comandos de decisão (if... then... else), laços (for... do), entre outras. Um programa concorrente, além dessas primitivas básicas, necessita de novas construções que o permitam tratar aspectos decorrentes da execução paralela dos vários processos. A fase de definição da organização das tarefas paralelas é de extrema importância, pois o ganho de desempenho adquirido da paralelização depende fortemente da melhor configuração das tarefas a serem executadas concorrentemente. Na definição do algoritmo, é necessário um conjunto de ferramentas para que o progra- mador possa representar a concorrência, definindo quais partes do código serão executadas sequencialmente e quais serão paralelas. As construções para definir, ativar e encerrar a exe- cução de tarefas concorrentes são o foco da tecnologia a ser tratada nesta monografia. Capítulo 4

Grand Cental Dispatch

O nome faz referência ao Grand Central Terminal, um importante ferroviário e metroviário localizado em Manhattan, Nova Iorque, por onde milhares de pessoas circulam. Grand Central Dispatch ou GCD é um sistema de gerenciamento de pools de threads em nível de sistema operacional, desenvolvido pela Apple e lançado junto ao MAC OS X 10.6 Snow Leopard. A idéia é combinar um modelo fácil de usar com melhor aproveitamento dos recursos de hardware disponíveis, para isso o GCD transfere a responsabilidade do gerenciamento inteligente de threads para o sistema operacional. As vantagens, ao fazer uso desta tecnologia, são muito relevantes. Mesmo a aplicação mais bem escrita pode não ter o melhor desempenho possível, pois para obter tal desempenho é necessário ter pleno conhecimento sobre tudo o que acontece no sistema em tempo de execução. E desse princípio vem o diferencial do GCD, pois é muito mais simples para o programador escrever seus aplicativos sem se preocupar com a disponibilidade dos recursos no momento da execução. Os blocos de tarefas serão colocados em filas de execução e gerenciados pelo sistema operacional que tem controle total sobre os eventos do sistema. Sendo assim, o desenvolvedor de software só precisa escrever código compatível com o GCD, tentando paralelizar ao máximo seu código, para tirar o máximo proveito dos núcleos e processadores múltiplos, e confiar no sistema operacional para fazer a melhor distribuição das tarefas entre os processadores disponíveis. A Apple disponibilizou os códigos da biblioteca da API de espaço de usuário (libdispatch) 14 Grand Cental Dispatch

e das alterações realizadas no kernel XNU (o kernel de código aberto da Apple comum ao Darwin e ao OS X), sob os termos de Apache License, versão 2.0. Porém o GCD também depende de uma extensão da linguagem para fazer uso dos blocks (estrutura que será abordada mais adiante). O fato de que GCD é uma biblioteca C significa que ele pode ser usado por todos os derivados da linguagem C suportados no SO: Objective-C, C++ e Objective-C++. Apesar disso o suporte à extensão no compilador é um pré-requisito para os desenvolvedores de aplicativos que queiram tirar proveito do GCD. O GCD é semelhante a outras tecnologias, como OpenMP ou ao TBB da Intel. E as três trabalham com certo tipo de abstração à criação de threads, permitindo com isso que o desenvolvedor defina partes do código como "tarefas"a serem paralelizadas de alguma forma. Contudo, o GCD faz uso de "blocos"em vez das diretivas de pré-processador do OpenMP ou dos modelos do TBB. Como dito, o GCD é implementado com um conjunto de extensões da a linguagem C, uma nova API e mecanismo de runtime. O GCD permite programar várias unidades de trabalho usando as seguintes quatro abstrações:

· Block objects;

· Dispatch queues;

· Synchronization;

· Event sources.

4.1 Block Objects

Block Objects (informalmente, “blocos”) é uma extensão da linguagem C, assim como Objective-C e C++, que torna mais fácil para os programadores a definição de unidades independentes de trabalho. Esses blocos são semelhantes a ponteiros de funções tradicionais, mas com algumas características de objeto. As principais diferenças que distingue blocos de ponteiros de funções apresentam-se a seguir: 4.2 Dispatch queues 15

· Os blocos podem ser definidos em linha, como “funções anônimas”;

· Os blocos fazem cópias das variáveis do escopo pai com permissão somente para leitura, similar as closures em outras linguagens.

Esse tipo de funcionalidade é comum em linguagens interpretadas, dinamicamente tipadas, mas não o era para programadores C. A Apple publicou as especificações de blocos e suas implementações em código aberto sob a licença MIT, adicionou suporte a blocos através do projeto compiler-rt, GCC 4.2 e clang, e apresentou suas considerações como parte da próxima versão da linguagem C. [7]

4.1.1 Sintaxe

Uma variável block assemelha-se a um ponteiro para função, exceto pelo uso de acento circunflexo em vez de um asterisco. void (^meuBloco)(void);

A variável resultante pode ser instanciada, atribuindo-a um bloco literal com a mesma assinatura (argumentos e tipos de retorno). meuBloco = ^(void){ printf("hello world\n"); };

Esta variável pode ser invocada como um ponteiro de função: meuBloco(); // imprime "hello world\n"

4.2 Dispatch queues

Dispatch queues (filas de despacho) é a uma das principais abstrações do GCD, pois é atra- vés das filas que os desenvolvedores programam as unidades de trabalho para serem executadas de forma concorrente ou serial, com a vantagem de ser muito fácil de utilizar, acrescido da 16 Grand Cental Dispatch

promessa de maior eficiência. Dispatch queues pode ser usado em todas as tarefas que são executadas em threads diferentes. O Dispatch queues é a estrutura que serve de interface entre a aplicação e o mecanismo de execução. E as Tarefas (unidades independentes de trabalho) são incluídas e retiradas das filas com operações atômicas que garantem a confiabilidade dos dados. Assim sendo, dispatch queues é um caminho fácil para executar tarefas de forma concorrente ou serial numa aplicação, e todos os tipos de Dispatch queues são estruturas first-in, first-out (FIFO). Desta forma, o disparo da execução de uma tarefa na fila respeita a ordem em que foi adicionada. O GCD já fornece alguns tipos de dispatch queues pré-definidas, mas outros tipos podem ser criadas pelo próprio programador para propósitos específicos. Vamos ver os tipos de fila dispatch fornecidas pelo GCD e os respectivos funcionamentos. Serial queue (Fila serial) ou fila privada é geralmente usado para controlar o acesso a recursos específicos tal qual “regiões críticas”. Cada serial queue tem suas tarefas executadas uma de cada vez e em ordem como já mencionado acima, mas a execução de tarefas em serial queues distintos podem ser concorrentes. Concurrent queue (Fila de concorrência), ou global queue, tem suas tarefas executadas simultaneamente em quantidade variada que depende da disponibilidade dos recursos do sis- tema. A execução dessas tarefas ocorre em threads distintas, gerenciadas pelo dispatch queue. O GCD fornece três filas de concorrência ao aplicativo, e se diferem apenas pela prioridade. O programador não pode criar novas filas de concorrência. Main dispatch queue (Fila principal) se trata da fila serial que tem a execução de suas tarefas na thread principal do aplicativo, frequentemente utilizada como ponto chave para sincronização das tarefas.

4.3 Synchronization

Quatro mecanismos primários são fornecidos pelo Grand Central Dispatch para melhor controle da finalização das tarefas assíncronas, que são aquelas em que uma não depende do cumprimento de execusão da outra. 4.3 Synchronization 17

· Synchronous Dispatch - Quando algumas instruções precisam aguardar o término da execução de um bloco, antes que sejam executadas, a forma mais simples de fazer isso é adicionar o bloco na fila de maneira que as instruções seguintes aguardem a conclusão deste bloco:

dispatch_sync(uma_fila, ^{ espere_por_mim(); });

Todavia, esse recurso requer atenção do processo pai, que fica parado aguardando até que o bloco chamado finalize sua execução.

· Groups - Outra forma fácil, e muito útil, de acompanhar a conclusão de blocos é o uso da barreira de sincronização Groups. As execuções de vários blocos de tarefas ligadas a um certo grupo, independente das filas utilizadas, permite fácil controle da conclusão de todas as tarefas do grupo sem que o processo pai tenha que ficar parado aguardando até que os blocos finalizem suas execuções.

dispatch_group_t meu_grupo = dispatch_group_create(); dispatch_group_async(meu_grupo, fila_a, ^{ uma_tarefa(); }); dispatch_group_async(meu_grupo, fila_b, ^{ outra_tarefa(); }); dispatch_group_notify(meu_grupo, outra_fila, ^{ // este bloco é executado assim que // as tarefas do grupo terminarem faca_isto_quando_tudo_terminar(); }); dispatch_release(meu_grupo); // continua o trabalho sem esperar pelas tarefas do grupo

· Semaphores. Além das ferramentas já citadas, para controle de acesso a seções críticas e sincronização de tarefas, GCD também tem seu próprio semáforo. O uso é similar ao do semáforo já conhecido na linguagem C, mas como outras ferramentas do GCD, o semáforo dispatch não precisa fazer chamadas ao kernel. 18 Grand Cental Dispatch

4.4 Event sources

O Grand Central Dispatch também fornece uma ferramenta que permite agendar a execu- ção de tarefas. Quando o aplicativo tem que interagir com sistemas subjacentes, o programador deve prepará-lo para o caso da tarefa levar uma quantidade de tempo não trivial, pois esse tipo de interação envolve uma mudança de contexto que é razoavelmente cara em comparação a chamadas dentro do próprio processo. Por esse motivo, muitas bibliotecas fornecem interfaces assíncronas para permitir que o código submeta as requisições para o sistema e prossiga com outras tarefas enquanto a requisição é processada. O GCD usufrui desse comportamento permitindo que o código obtenha os resultados de suas requisições usando blocos e filas dispatch, substituindo as funções assíncronas normal- mente usadas para tratar os eventos do sistema. Ao configurar um dispatch source, especifi- camos o evento que queremos monitorar, bloco e a fila dispatch que queremos para processar este evento. Os blocos podem ser relacionados a event souces como timers, signals, sockets, file descriptors, process state changes e outros. Exemplo de source timer : dispatch_queue_t global_queue; global_queue = dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_source_t src_timer; src_timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, global_queue ); dispatch_source_set_timer(src_timer, 0, DURACAO, 0); dispatch_source_set_event_handler(src_timer, ^{ printf("Event source\n"); }); dispatch_resume(src_timer); 4.5 Leitura e construção do código 19

4.5 Leitura e construção do código

Com o uso da tecnologia Grand Central Dispatch, torna-se bem menos arduo transformar implementações seriais de tarefas independentes em implementações paralelizadas. Os “blocos” têm grande participação nesse tipo de tarefa: ao identificar um trecho de código que poderia ser executado assincronamente, movemos esse trecho para um bloco e disparamos sua execução assíncona, sem o acrescimo de muitas linhas de código e sem muitas modificações na estrutura do código já existente. A clareza, legibilidade dos códigos e até facilidade de distribuição das tarefas são pontos muito favoráveis ao GCD. No apêndice A.1e A.2 podemos observar códigos de um dos testes executados, e vemos algumas diferenças entre o GCD e a programação direta com threads. No código que utiliza o GCD, o tratamento da concorrência fica encapsulado, enquanto que no uso manual de threads o disparo e sincronização manual dos threads deixa o código poluído, pois mistura instruções feitas em diferentes níveis de abstração (realização da tarefa e código de sincronização). Para dar um exemplo da simplicidade de implementar tarefas paralelas e concorrentes, podemos observar no apêndice B.1 o código de uma pequena aplicação que simula um call center. Processos “atendentes” e processos “clientes” executando paralelamente, e concorrendo a recursos do aplicativo, como entrar na fila de atendimento e atender o primeiro cliente da fila. Podemos observar que o controle de exclusão mútua e disparo das tarefas são bem simples. 20 Grand Cental Dispatch Capítulo 5

Atividades realizadas

5.1 Explorando as funcionalidades

O Grand Central Dispatch é uma tecnologia bem recente com poucas fontes de documen- tação. Por esse motivo, suas funcionalidades foram testadas de diversas formas para que seu uso fosse bem entendido. Dessa forma, os benefícios que o GCD trouxe puderam ser melhor explorados. A máquina usada nos testes foi um MacBook white com processador Intel Core 2 Duo 2.26GHz, 2GB de memória rodando MAC OS X 10.6.5, e não houve problema algum ao deixar a máquina preparada para programação com a biblioteca libdispatch. O computador veio de fábrica com a versão Snow Leopard do MAC OS X, que contém suporte nativo ao GCD, e foi necessário instalar apenas o pacote de programação Xcode, para que o computador ficasse apto a compilar e executar os programas que fazem uso da biblioteca libdispatch e dos blocos. A primeira ferramenta a ser examinada foi o Block Object, e esse exame teve início nas variações de sua sintaxe, bem como variáveis de acesso ao escopo pai, suas variáveis de escopo local, e execução de suas operações. A partir disso uma abordagem do uso de blocos pôde ser feita. Em seguida, foi a vez das ferramentas de sincronização e bloqueio. Tarefas com tempo de funcionamento variado permitiram examinar ferramentas de sincronização, concorrências entre as tarefas e acesso exclusivo a regiões críticas. 22 Atividades realizadas

5.2 Testes de desempenho e Resultados obtidos

Após analisar as facilidades e utilidades abordadas pelas ferramentas disponíveis do Grand Central Dispatch, partimos para a avaliação do desempenho das aplicações constituídas com a tecnologia. O GCD foi feito pela Apple, que é uma empresa privada com fins lucrativos, por esse motivo a maioria das publicações que falam sobre o GCD são da própria Apple, com muitas propagandas prometendo melhor desempenho dos aplicativos, aproveitamento máximo dos núcleos ou dos processadores, tudo respaldado pela facilidade de uso. Segundo a própria Apple, o overhead gerado com a criação manual de inúmeros threads é evitado no GCD, pois as tarefas aguardam a disponibilidade de processadores lógicos para serem executadas. As instruções de encapsulamento de tarefas são curtas, portanto eficientes; incluir ou remover tarefas das filas são operações atômicas disponíveis no hardware, o que assegura a confiabilidade dos dados e a rapidez da operação, portanto, os testes feitos visavam verificar as reais vantagens e desvantagens do GCD como ferramenta usada pelos programadores para paralelizar seus códigos e criar eficientes aplicativos. O objetivo desses testes é fazer uma comparação entre o desempenho dos aplicativos constituidos com as ferramentas do Grand Central Dispatch e com com uso manual de threads. Os testes foram feitos com aplicativos que têm seu trabalho facilmente paralelizável, para não haver dúvidas quanto a qualidade da divisão de tarefas escolhida.

5.2.1 Tarefas crescentes

Nos primeiros testes, implementamos um algoritmo para o cálculo do produto de matrizes quadradas por se tratar da escolha mais natural e intuitiva. Cada linha da matriz resultante do produto foi gerada por uma tarefa independente, e matrizes do tipo double foram geradas dinamicamente com entradas adquiridas de uma função que gerava números aleatórios. No estudo das ferramentas do GCD, focamos em duas formas de disparar várias tarefas em paralelo e aguardar a conclusão das mesmas, usando a chamada dispatch_apply() e dis- patch_group_async(). Esse foco duplo é devido à observação de comportamentos distintos 5.2 Testes de desempenho e Resultados obtidos 23 entre as duas chamadas. Ao fazer uso da chamada dispatch_apply(), para várias tarefas con- tendo um “sleep(aleatório)”, o sistema permitia somente duas tarefas em execução por vez, provavelmente por estar sendo executado em um computador com dois núcleos. Já com o uso da chamada dispatch_group_async() todas as tarefas foram disparadas. Cada ferramenta foi executada inúmeras vezes, ou seja, produto de matrizes de mesma dimensão foram executadas várias vezes para cada ferramenta. Observe os resultados compa- rativos:

Figura 5.1: Gráfico dos resultados 24 Atividades realizadas

Figura 5.2: Resultados obtidos

Os resultados do teste de desempenho não mostraram vantagem do GCD sobre o uso manual de threads. Inclusive o aplicativo que usa os threads diretamente começou com um aumento no desvio padrão e seu tempo médio de execução não acompanhou o tamanho do trabalho a ser realizado, deixando o GCD em desvantagem. Resultados semelhantes foram obtidos ao aplicar testes no problema de encontrar números primos, em que cada tarefa inde- pendente procurava primos num intervalo distinto, mas de mesmo tamanho. Observe que esses problemas tinham uma característica comum. No produto de matrizes, o tamanho de cada unidade de trabalho dependia da dimensão da matriz, pois o tamanho da linha é proporcional a dimensão da matriz. Na busca por números primos, o tamanho das unidades de trabalho também crescem acompanhando o escopo, pois quanto mais distante um número está de 1, maior é o número de comparações que a tarefa deve executar.

5.2.2 Tarefas curtas

O GCD é um forte incentivo para que programadores compartilhem ao máximo o trabalho a ser realizado por seus aplicativos em tarefas menores, obtendo o melhor desempenho nos futuros processadores com número expressivo de núcleos. Mas como é o desempenho desses aplicativos nos processadores usados atualmente? Resolvemos testar o desempenho dos aplicativos com esse tipo de característica. Teríamos, 5.2 Testes de desempenho e Resultados obtidos 25 dessa forma, um grande número de tarefas curtas a serem executadas nos aplicativos. O teste que realizamos foi reproduzido com tarefas que executavam um número fixo de operações de soma e produtos de números “reais” gerados aleatoriamente. Assim, o tamanho de cada tarefa não tem relação com o número de tarefas. Observe os resultados do desempenho das aplicações com essa característica:

Figura 5.3: Gráfico dos resultados 26 Atividades realizadas

Figura 5.4: Resultados obtidos

Aqui podemos observar o crescimento mais comportado do aplicativo escrito com a ferra- menta estudada. Enquanto o uso manual de threads certamente paga um preço mais caro por disparar muitos threads simultâneos.

5.2.3 Divisão do trabalho

Muitas vezes, temos que conhecer o hardware em que o software será rodado para escrevê-lo de maneira a obter o melhor desempenho possível. Entretanto, sabemos que não é uma tarefa fácil escrever o software pensando o tempo todo em como se dará o comportamento do mesmo em um hardware específico. E ainda podemos perder em desempenho ao rodar o aplicativo em hardware diferente. O GCD promete ser uma arma muito poderosa para a solução desse tipo de problema, pois a administração dos threads, por conta do sistema operacional, permite programar sem se preocupar com a disponibilidade dos recursos. Nosso objetivo nesse teste foi verificar essa afirmação. No teste anterior foi possível observar certa vantagem do GCD com inúmeras tarefas, pois a grande quantidade de tarefas de tamanho fixo nos deu indícios fortes de que o uso manual de Thread pode ter desempenho menor que o GCD com overhead muito grande. Nesse novo teste, o trabalho total a ser realizado pelo aplicativo não muda de tamanho. Então, foi estudado o desempenho das tecnologias ao dividir o trabalho a ser realizado pelo aplicativo em tarefas cada vez menores para analisar o quanto perdemos em desempenho ao criar mais unidades de trabalho, mantendo o trabalho total para deixar mais clara essa diferença. 5.2 Testes de desempenho e Resultados obtidos 27

Criamos duas matrizes quadradas do tipo double de ordem 512. Para calcular uma matriz resultante do produto dessas duas matrizes são necessários 5122 produtos de vetores. Então o total desses produtos foram distribuídos entre 2 tarefas, depois 23, 25, 27, 28, 29, 211, 213, 214, 215 tarefas. Observe os resultados:

Figura 5.5: Gráfico dos resultados

Para melhor visualização no gráfico, só é mostrado até 215 tarefas, mas podemos observar a tabela de valores e verificar que mesmo com uma distribuição muito grande o GCD não aumenta segnificativamente seu tempo de execução. 28 Atividades realizadas

Figura 5.6: Resultados obtidos Capítulo 6

Conclusões

Nos testes que fizemos os aplicativos implementados com uso da tecnologia fornecida pelo Grand Central Dispatch obtiveram resultados favoráveis na divisão do trabalho total a ser realizado por várias outras tarefas menores. O seu mecanismo de gerenciamento de threads não mostrou vantagem quando o trabalho a ser realizado por uma tarefa independente cresce bastante. Mas podemos verificar que em todos os testes o GCD obteve menor desvio padrão, mostrando-se mais estável em suas execuções; e embora o GCD não resolva o problema de decidir quais as partes do trabalho podem ser realizadas em paralelo, o que ainda é um problema difícil, quando o programador resolve essa etapa, a participação do GCD traz muitos benefícios ao restante do trabalho. É importante reforçar, que o processamento paralelo permite uma grande economia de energia e consequentemente a diminuição da quantidade de calor liberado. Essa diminuição no consumo de energia e liberação de calor aumenta a vida útil dos componentes, permite a existência de notebooks robustos com baterias de longa duração, e ajuda a solucionar proble- mas de aquecimento nas centrais de processamento de dados etc. Todas essas vantagens nos levam a crer que o futuro dos processadores será com números muito grandes de núcleos com clocks não tão elevados. Nesse contexto, o Grand Central Dispatch se mostra uma tecnologia realmente preparada para o futuro, pois mesmo que os computadores de hoje ainda possuam poucos núcleos, os programas construídos com essa tecnologia hoje já têm um bom desempenho, e não terão 30 Conclusões que ser reescritos para obter melhor desempenho nos futuros processadores com centenas ou milhares de núcleos. E por se tratar de uma tecnologia disponibilizada à comunidade de software livre, as possibilidades de propagação de seu uso e possíveis aperfeiçoamentos nos fazem acreditar no sucesso da tecnologia. Parte II

Parte subjetiva

Capítulo 7

Experiência pessoal

Quando ingressei na universidade, no curso de licenciatura em matemática, após cursar Introdução à computação e lecionar aulas particulares de matemática, descobri, já no primeiro ano que seria na computação que eu aplicaria a matemática da forma que mais gosto. Dessa forma, meu primeiro desafio acadêmico, foi conseguir transferência para a computação. Após decidir fazer a transferência, tive que aguardar mais de um ano para conseguir, buscando cursar todas as disciplinas exigidas e manter a média ponderada acima do mínimo, também exigido para transferência. Ao longo do curso percebi que ter boa familiaridade com o inglês é muito importante na formação de um Bacharel em Ciência da computação, mas foi neste trabalho que o inglês se mostrou, para mim, um grande desafio que eu me propus a vencer. No mundo da tecnologia, qualquer novidade será apresentada ao mundo primeiro na língua inglesa, e como o assunto estudado neste trabalho é uma tecnologia nova, não ouve a opção de buscar por material em português para auxiliar o estudo. Um grande desafio neste trabalho foi a liberdade. Uma tecnologia nova, que levou-nos a decidir por qual abordagem escolher bem como a maneira pela qual trata-la. A falta de prática com pesquisa e confecção de texto acadêmico também foram dificuldades encontradas. Tive que aprender novos conceitos e não havia muitas fontes de informação a respeito da tecnologia estudada. 34 Experiência pessoal

A frustração que tenho, é de não ter testado o Grand Central Dispatch em outras pla- taformas, avaliar as dificuldades de instalação, comparação de desempenho e apresentar essa experiência neste trabalho. Sei que o FreeBSD foi o pioneiro, das plataformas open source, a implementar a tecnologia. O linux também está se movimentando a respeito, o Debian também tem testado implementações da tecnologia.

7.1 Estudos futuros

Para futuros estudos seria interessante testar a tecnologia em computadores com 4, 6 ou mais núcleos. Limitação de recursos e tempo me impediram de fazer tais testes. Dessa forma teremos informações mais precisas, com relação ao desempenho e mais dúvidas esclarecidas, como a limitação de disparos da ferramenta dispatch_apply(). Com a disseminação da tecnologia, outras plataformas terão implementações estáveis da mesma. Sendo assim, experimentos devem ser feitos nessas plataformas e as diferenças, seme- lhanças, facilidades e dificuldades apuradas e documentadas.

7.2 Disciplinas relevantes

MAC0110 — Introdução à computação O contato com os conceitos lógicos e aplicação da lógica matemática foram motivadores e essenciais para dar sequência ao minha for- mação.

MAC0323 — Estruturas de Dados Essa disciplina me mostrou a computação sob um panorama diferente. As diferentes estruturas lógicas e a eficiência no tratamento dos dados esteve muito presente no curso.

MAC0338 — Análise de Algoritmos Nessa disciplina, em que aprendemos a analisar a complexidade dos softwares, vimos a maneira correta e eficiente de implementar algo- ritmos “gulosos”. Aprendemos que soluções “ingênua” podem ser muito ruins, e como melhora-las.

MAC0412 — Organização de Computadores Saber o comportamento do hardware e suas limitações trás muitos benefícos no desenvolvimento de software, principalmente 7.2 Disciplinas relevantes 35

na programação de alto desempenho. Em Organização de Computadores pude entender melhor o funcionamento da máquina para aproveitar melhor seus recursos.

MAC0438 — Programação Concorrente Certamente umas das disciplinas mais intere- santes e importantes na minha formação. Desde de os estudos aos testes, este trabalho me exigiu bastante os conhecimentos que obtive nessa disciplina. Hoje vivemos vivemos a ascenção dos processadores multicore, e conhecer esses conceitos é estar um passo a frente.

MAC0342 — Laboratório de Programação Extrema A experiencia adquirida nessa dis- ciplina é de grande valia a um profissional da area de desenvolvimento de software. Os princípios e valores dos métodos ágeis, técnicas de refatoração, acompanhamento do processo de desenvolvimento, desenvolvimento dirigido por testes, tudo isso proporci- ona ao estudante aprender na prática, contribuindo com soluções de um problema real e envolvendo-se com pessoas e ideias trocadas ao longo do semestre. Deveria ser um disciplina obrigatória. 36 Experiência pessoal Referências Bibliográficas

[1] ANDREWS, G. R., Schineider, F. B., Concepts and Notations for Concurrent Program- ming, ACM Computing Survey, v. 15, no.1, pp. 3-43,1983. 11

[2] Computação Paralela - USP - São Carlos - R.H.C. Santana, M.J. Santana, M.A. Souza, P.S.L. Souza, A.E.T. Piekarski

[3] http://arstechnica.com/apple/reviews/2009/08/mac-os-x-10-6.ars/124

[4] http://libdispatch.macosforge.org/

[5] http://lwn.net/Articles/352978/

[6] http://developer.apple.com/technologies/mac/snowleopard/gcd.html

[7] http://developer.apple.com/library/mac/#featuredarticles/BlocksGCD/index.html 15

[8] http://developer.apple.com/ GCD /Reference/reference.html

[9] http://en.wikipedia.org/wiki/Moore’s_law3

[10] www.top500.org3

[11] http://en.wikipedia.org/wiki/Very-large-scale_integration6

[12] ALMASI, G. S., Gottlieb A., Highly Parallel Computing, 2a. ed., The Benjamin Cum- mings Publishing Company, Inc., 1994.9

[13] QUINN, M.J., Designing Efficient Algorithms for Parallel Computers, McGraw Hill, 1987. 9 38 REFERÊNCIAS BIBLIOGRÁFICAS

[14] HWANG, K., Briggs, F. A., Computer Architecture and Parallel Processing, McGraw-Hill International Editions, 1984.9 Apêndice A

Códigos de comparação

1 #include 2 #include "matrix.h" 3 4 int main(int argc, char *argv[]) { 5 dispatch_queue_t globalQueue; 6 globalQueue = dispatch_get_global_queue ( 7 DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 8 initialize(argc, argv); 9 10 dispatch_apply(dimension, globalQueue, ^(size_t line) { 11 int column; 12 for (column = 0; column < dimension; column ++) { 13 matrixProduct[line][column] = 14 scalar_product(matrixA[line], matrixB[column]); 15 } 16 }); 17 return 0; 18 }

Código A.1: Código usando Apply por linha da matriz 40 Códigos de comparação

1 #include 2 #include "matrix.h" 3 4 void *thread_func(void *argument) { 5 int line, column; 6 line = *((int *) argument); 7 for (column = 0; column < dimension; column++) 8 matrixProduct[line][column] = 9 scalar_product(matrixA[line], matrixB[column]); 10 return NULL; 11 } 12 int main(int argc, char *argv[]){ 13 int i, *indice; 14 pthread_t *threads; 15 16 initialize(argc, argv); 17 indice = (int *) malloc (dimension * sizeof(int)); 18 threads = (pthread_t *)malloc(dimension*sizeof(pthread_t)); 19 20 for(i = 0; i < dimension; i++){ 21 indice[i] = i; 22 pthread_create(&(threads[i]), NULL, thread_func, 23 &(indice[i])); 24 } 25 for(i = 0; i < dimension; i++) 26 pthread_join(threads[i], NULL); 27 return 0; 28 }

Código A.2: Código usando Thread por linha da matriz Apêndice B

Código de implementação de concorrência

1 #include 2 #include 3 #include 4 #include 5 6 #define MAX_CLIENTES 1000 7 #define MAX_ALEATORIO 5 8 #define VAZIO -1 9 #define PERIODOdeEXECUCAO 30 10 11 typedef dispatch_group_t Grupo; 12 13 bool esperando[MAX_CLIENTES]; 14 int filaDeClientes[MAX_CLIENTES]; 15 16 dispatch_queue_t lockQueue; 17 18 int n, m; /* numero de processos */ 19 int cabeca, clientesNaFila; 20 bool terminar = false; 42 Código de implementação de concorrência

21 void inicializa(int argc, char *argv[]); 22 23 void despachaTarefas(Grupo clienteGrp, Grupo atendenteGrp); 24 25 int aleatorio(); 26 27 void entraNaFila(int idCliente); 28 29 int atendeCliente(); 30 31 void Atendente(int id); 32 33 void Cliente(int id); 34 35 void mandaTerminar(Grupo clienteGrp, Grupo atendenteGrp); 36 37 int main(int argc, char *argv[]) { 38 Grupo clienteGrp, atendenteGrp; 39 40 inicializa(argc, argv); 41 42 clienteGrp = dispatch_group_create(); 43 atendenteGrp = dispatch_group_create(); 44 45 despachaTarefas(clienteGrp, atendenteGrp); 46 47 sleep(PERIODOdeEXECUCAO); 48 mandaTerminar(clienteGrp, atendenteGrp); 49 return 0; 50 } 43

51 void inicializa(int argc, char *argv[]){ 52 srand (time(NULL)); 53 n = atoi(argv[1]); 54 m = atoi(argv[2]); 55 cabeca = clientesNaFila = 0; 56 lockQueue = dispatch_queue_create("secao.critica", NULL); 57 } 58 /* Devolve valor aleatorio entre 0 e MAX_ALEATORIO */ 59 int aleatorio(){ 60 return (int)(rand()/RAND_MAX) * MAX_ALEATORIO; 61 } 62 63 void despachaTarefas(Grupo clienteGrp, Grupo atendenteGrp){ 64 int i; 65 dispatch_queue_t globalQueue; 66 67 globalQueue = dispatch_get_global_queue ( 68 DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 69 for (i = 0; i < n; i++) { 70 dispatch_group_async(clienteGrp, globalQueue, ^{ 71 Cliente(i); 72 }); 73 } 74 75 for (i = 0; i < m; i++) { 76 dispatch_group_async(atendenteGrp, globalQueue, ^{ 77 Atendente(i); 78 }); 79 } 80 } 44 Código de implementação de concorrência

81 /* Processo Cliente */ 82 void Cliente(int id){ 83 while (! terminar) { 84 sleep(aleatorio()); 85 dispatch_sync(lockQueue, ^{ 86 entraNaFila(id); 87 }); 88 printf("Cliente %d esperando\n", id); 89 while (esperando[id]) 90 sleep(1); 91 printf("Cliente %d atendido\n", id); 92 } 93 } 94 /* Processo Atendente */ 95 void Atendente(int id) { 96 __block int idCliente; 97 while (! terminar) { 98 dispatch_sync(lockQueue, ^{ 99 idCliente = atendeCliente(); 100 }); 101 if (idCliente != VAZIO){ 102 /* Atendeu o cliente: idCliente */ 103 sleep(aleatorio()); 104 esperando[idCliente] = false; 105 /* Atendimento finalizado */ 106 } 107 else 108 sleep(aleatorio()); 109 } 110 } 45

111 void entraNaFila(int idCliente){ 112 filaDeClientes[cabeca + clientesNaFila] = idCliente; 113 esperando[idCliente] = true; 114 clientesNaFila++; 115 } 116 117 int atendeCliente(){ 118 int idCliente; 119 if (clientesNaFila < 1) 120 return VAZIO; 121 idCliente = filaDeClientes[cabeca]; 122 cabeca = (cabeca + 1) % n; 123 clientesNaFila--; 124 return idCliente; 125 } 126 127 void mandaTerminar(Grupo clienteGrp, Grupo atendenteGrp){ 128 terminar = true; 129 dispatch_group_wait(clienteGrp, DISPATCH_TIME_FOREVER); 130 dispatch_group_wait(atendenteGrp, DISPATCH_TIME_FOREVER); 131 }

Código B.1: Concorrência no CallCenter