5. PARADIGMA CONCORRENTE
Algumas vezes é conveniente estruturar os sistemas como um conjunto de unidades concorrentes que são
executadas em paralelo. Isto ocorre quando o programa é executado em um computador com múltiplos
processadores ou em um sistema distribuído composto de vários computadores que cooperam entre si. Um
programa concorrente especifica dois ou mais processos concorrentes, onde cada um executa um programa
seqüencial. Tais processos interagem através da comunicação, o que traz a necessidade de sincronização. A
concorrência é uma importante área da Ciência da Computação estudada em diferentes contextos: arquitetura das
máquinas, sistemas operacionais, sistemas distribuídos, banco de dados, etc.
Há pelo menos duas razões para se estudar concorrência. Primeiro, ela fornece uma solução diferente para
resolução dos problemas. Diversos tipos de problemas levam à estruturação de um sistema como um conjunto de
unidades concorrentes que executam em paralelo, da mesma maneira que a recursão é uma forma natural de
projetar a solução para determinados problemas. Além disso a execução em paralelo, na maioria das vezes,
aumenta a performance dos sistemas. Muitos programas são escritos para simular entidades físicas e atividades, e
freqüentemente o sistema que está sendo simulado inclui mais do que uma entidade que realizam ações
simultaneamente. A segunda razão para o estudo da concorrência é que os computadores com múltiplos
processadores estão sendo muito utilizados, criando a necessidade de desenvolvimento de software para fazer uso
efetivo das capacidades de hardware. Por isso, facilidades para implementação da concorrência devem ser
desenvolvidas e incluídas nas linguagens de programação.
Hoje em dia, várias arquiteturas de computadores possuem mais do que um processador e podem suportar a
execução concorrente de programas. Nos primeiros computadores que possuíam múltiplos processadores, um
processador era usado para funções genéricas e os outros eram usados apenas para operações de entrada e saída.
Assim, enquanto um programa era executado, outros podiam executar operações de entrada e saída. Na década de
60, as máquinas possuíam processadores que eram usados através de um organizador de jobs (ou tarefas), que
distribuía a execução entre os processadores. Sistemas com esta estrutura suportavam a programação concorrente.
Em meados da década de 60 surgiram computadores multiprocessados providos de diversos processadores que
executavam apenas um conjunto de instruções. Por exemplo, algumas máquinas tinham dois ou mais
multiplicadores de ponto-flutuante, enquanto outras tinham duas ou mais unidades aritméticas de ponto-flutuante
completas. Neste caso, os compiladores tinham que determinar quais instruções poderiam ser executadas
concorrentemente e em quais processadores.
Atualmente existem vários tipos de computadores multiprocessados, sendo que os mais comuns são SIMD
e MIMD. O primeiro, Single-Instruction Multiple-Data, possui múltiplos processadores, cada um com sua própria
memória local, que executam a mesma instrução simultaneamente com dados diferentes. Neste caso um
processador controla a operação dos outros processadores. Talvez as máquinas SIMD mais populares pertençam à
categoria de processadores vetoriais. O segundo tipo, Multiple-Instruction Multiple-Data, possui múltiplos
processadores que operam independentemente, mas cujas operações podem ser sincronizadas. Cada processador em
um computador MIMD executa seu próprio conjunto de instruções. Eles podem aparecer em duas configurações
distintas: sistemas distribuídos e de memória compartilhada.
A concorrência é naturalmente dividida nos seguintes níveis:
§
Instrução: execução de duas ou mais instruções de máquina simultaneamente (não envolve questões de
linguagens de programação);
§
Comando: execução de dois ou mais comandos simultaneamente;
§
Unidade: execução de duas ou mais unidades, ou subprogramas, simultaneamente;
§
Programa: execução de dois ou mais programas simultaneamente (não envolve questões de linguagens de
programação).
Métodos de controle da concorrência a nível de subprogramas aumentam a flexibilidade de programação.
Estes métodos foram criados para serem usados em problemas específicos de sistemas operacionais, mas também
podem ser utilizados em uma variedade de outras aplicações. Por exemplo, muitas aplicações foram projetadas para
simular sistemas físicos, e muitos deles consistem de múltiplos subsistemas concorrentes. Já a concorrência a nível
de comando consiste em especificar como os dados devem ser distribuídos em memórias múltiplas e quais
comandos podem ser executados concorrentemente [SEB 99].
Unidades concorrentes podem ser vistas como outra forma de abstração procedural. A principal
característica de distinção é que uma vez que são invocadas, a unidade que fez a invocação pode prosseguir com
sua execução sem esperar pelo término da unidade invocada (figura 4.1). Consequentemente, unidades
concorrentes requerem todas as mesmas considerações das outras abstrações procedurais, nome, definição,
invocação e compartilhamento de dados. Outras considerações incluem a necessidade de comunicação e
sincronização entre as execuções concorrentes.
Unidade que fez a invocação
Continuação da execução
Chamada
Explícita
Unidade invocada
Figura 5.1 – Processamento concorrente
Existem dois modelos para a definição de unidades de programa concorrentes: o primeiro segue a
construção da definição de um procedimento e o segundo de um tipo. No primeiro caso, quando a unidade
concorrente é declarada, uma amarração é feita entre o nome o corpo executável. A unidade pode então ser
invocada concorrentemente através da referência apropriada ao seu nome. No segundo modelo a definição cria um
tipo e variáveis que são declaradas como sendo daquele tipo são então amarradas aos nomes das unidades
executáveis, e múltiplas execuções do mesmo tipo de processo podem ser referenciadas pelos seus nomes distintos.
A invocação de unidades concorrentes pode ser implícita ou explícita. A primeira assume que o processo
concorrente pertence a unidade de programa na qual ele foi definido. Sempre que uma unidade de programa
começa a execução, todas as unidades concorrentes que pertencem a esta unidade (são definidas nesta unidade)
começam suas execuções simultaneamente. Quando a unidade "mestre" termina, ela espera até que todas as
unidades concorrentes terminem, antes de retornar para a unidade que fez a invocação. Este processo de invocação
implícita é ilustrado na figura 4.2. Já as unidades concorrentes que são invocadas explicitamente, são invocadas
como procedimentos, como uma abstração de comandos através de uma referência ao nome ao qual a unidade é
amarrada através de sua definição. Isto permite a invocação da unidade concorrente em qualquer ponto dentro da
unidade de programa na qual foi definida [DER 90].
Também deve-se considerar que a execução de unidades de programa concorrentes pode ocorrer
fisicamente em processadores separados, ou logicamente em fatias de tempo diferentes em um único processador.
Na concorrência física, assume-se que mais de um processador está disponível e vários subprogramas de um
mesmo programa estão literalmente executando simultaneamente. Na concorrência lógica, tanto o programador
como o sistema assumem que existem múltiplos processadores fornecendo concorrência, quando, na verdade, a
execução atual do programa é feita em intervalos de tempos diferentes no mesmo processador. Isto é similar a
ilusão de execução simultânea que é fornecida a usuários diferentes de sistemas multiprogramáveis. Do ponto de
vista do programador, a concorrência lógica é igual à física [SEB 99].
procedure P;
concurrent unit C1;
concurrent unit C2;
concurrent unit C3;
begin
...
end;
P
Invocação
de P
C1
C2
C3
Término
de P
Execução de P
Execução de C1
Execução de C2
Término
de C1
Retorno para a
unidade que fez
a invocação
Espera pelo término
de todas unidades
concorrentes
Término
de C2
Execução de C3
Término
de C3
Figura 5.2 – Processamento concorrente
Freqüentemente é necessário que as unidades concorrentes compartilhem dados, sendo que os dois modelos
geralmente usados são: memória compartilhada e troca de mensagens. No primeiro caso, existem dados que são
globais a todas unidades no sentido que várias unidades possuem acesso a estes dados. Tais dados estão na forma
de variáveis que todas as unidades podem acessar, ou em estruturas de dados mantidas em memória para todas
unidades usarem. Isto é ilustrado no próximo exemplo em Pascal Concorrente, onde "P1" e "P2" usam a variável
global "N", com um modelo de invocação explícita. Os resultados da execução do programa podem variar
dependendo dos tempos das execuções das unidades concorrentes. Se "P1" é executado exatamente no mesmo
tempo de "P2", "N" resulta em 4. Por outro lado, se a execução de "P1" é seguida pela execução de "P2", "N"
resulta em 6.
program Concurrent;
var N: integer;
concurrent unit P1;
begin
N := N + 1;
end;
concurrent unit P2;
begin
N := N + 2;
end;
begin
N := 3;
cobegin
P1;
P2;
coend;
writeln(N);
end.
A segunda forma de compartilhar dados, através da troca de mensagens de uma unidade para outra, é
tipicamente referenciada como comunicação entre processos. Sendo assim, é necessário um emissor e um receptor
de mensagens, o que resulta em dois modelos para troca de informações entre unidades: mail e telefone. A
diferença entre eles está em quando ou não a unidade receptora precisa atender a mensagem antes da unidade
emissora prosseguir. No primeiro modelo, a mensagem é enviada da unidade "S" para "R" e colocada em um
mailbox. A unidade "S" então prossegue com a execução e a unidade "R" recupera a mensagem mais tarde. Existem
duas formas de troca de informações através do modelo de telefone que variam na espera: o emissor espera
somente pela notificação do receptor de que a mensagem foi recebida e continua a executar; ou o emissor espera
que a mensagem seja recebida e processada pelo receptor.
Uma técnica útil para visualizar o fluxo de execução através de um programa é imaginar uma thread para
cada um dos comandos do programa fonte. Cada instrução atingida em uma execução particular é coberta pela
thread que representa essa execução. Seguir visualmente a thread ao longo do programa fonte possibilitará rastrear
o fluxo de execução pela versão executável do programa. Uma thread de controle em um programa consiste numa
seqüência de pontos do programa atingidos à medida que o controle flui por ele.
Programas que possuem co-rotinas, que as vezes são chamados de "quase concorrentes", possuem uma
única thread de controle. Programas executados com concorrência física podem ter múltiplas threads de controle,
onde cada processador pode executar uma das threads. Apesar de que programas com concorrência lógica podem
possuir apenas uma thread de controle, tais programas podem somente ser projetados e analisados imaginando-se
que eles possuem múltiplas threads de controle. Quando um programa multithreaded executa numa máquina com
um único processador, suas threads são mapeadas numa única thread, tornando-se, neste caso, um programa
multithreaded virtual.
A concorrência a nível de comando é um conceito relativamente simples. Laços que incluem comandos que
operam em vetores são desfeitos para que o processamento possa ser distribuído em múltiplos processadores. Por
exemplo, um laço que executa 500 repetições e inclui um comando que opera em um dos 500 elementos do vetor
pode ser desfeito de tal maneira que cada um dos dez processadores diferentes possa simultaneamente processar 50
elementos do vetor [SEB 99].
Download

5. PARADIGMA CONCORRENTE