Issuu on Google+

E

N

G

E

N

H

A

R

I

A

D

E

P

R

O

C

E

S

S

A

M

E

N

T

O

D

I

G

I

T

A

L

I I

7. Programação com Threads 7.1. Introdução Uma aplicação multi-thread é uma aplicação que contém várias vias simultâneas de execução. Algumas considerações sobre a programação com threads:  Cada thread supões uma carga adicional ao sistema.  O desenho de aplicações multi-thread é complexo e difícil.  As threads devem ser coodenadas de alguma forma.

POR QUE USAR THREADS? O uso de threads (de paralelismo em geral) proporciona uma série de vantagens frenta às limitações dos sistemas monotarefa. Por exemplo, suponha que a aplicação tenha que se ocupar da realização de cópias de segurança dos dados com que se trabalha. Com uma única thread teriamos que programar as cópias de segurança fora do horário habitual de trabalho. E se o programa tiver que funcionar 24 horas do dia? Com várias threads pode se aproveitar os períodos de inactividade do sistema.

APROVEITAMENTO DOS RECURSOS DO SISTEMA Quando se utiliza uma única thread o programa deve deter completamente a sua execução enquanto espera que se realize cada tarefa. A CPU permanece ocupada completamente (ou inativa) até que o processo atual termine a sua execução. Se forem utilizadas várias threads, o sistema pode ser usado para realizar várias tarefas simultaneamente (ex. reprodução de MP3 no background).

ESTABELECIMIENTO DE PRIORIDADES Como é lógico, se designa maior prioridade às tarefas mais importantes, ou seja, àquelas que requeiram uma resposta mais rápida.

MULTIPROCESAMENTO REAL Em um sistema multiprocessador, se a aplicação se decompõe em várias threads, o sistema operacional poderá designar cada uma a uma das CPU do sistema.

159


L

U

I

S

F

E

R

N

A

N

D

O

E

S

P

I

N

O

S

A

C

O

C

I

A

N

ESTRUTURAÇÃO: PARALELISMO IMPLÍCITO Em muitas ocasiões um programa pode ser projetado como vários processos paralelos que funcionam de forma independente. A decomposição de uma aplicação em várias threads pode resultar beneficiosa em termos de desacoplamento de código (tarefas independentes se implementam por separado) e de qualidade do projeto (frente a futuras ampliações, por exemplo). É importante destacar que o objetivo principal do uso de threads é melhorar o rendimento do sistema. O programador deverá decidir até que ponto devem utilizar-se.

7.2. A Classe Tthread O C++ Builder proporciona uma classe abstrata TThread para a criação de threads. Para definir uma thread deve se criar um descendente de TThread (classe derivada). Para facilitar o trabalho o C++ Builder inclui um assistente.

EXEMPLO – CRIAR UMA THREAD Usando a opção de menu File + New selecionar o elemento Thread Object.

Ao selecionar o botão OK, será solicitado o nome da classe derivada. Usar o nome TThreadBola à classe derivada de TThread.

160


E

N

G

E

N

H

A

R

I

A

D

E

P

R

O

C

E

S

S

A

M

E

N

T

O

D

I

G

I

T

A

L

I I

Com isto foi criada a primeira thread em uma unidade independente que serão armazenados nos arquivos UBola.cpp e UBola.h. A classe TThread é abstrata porque possui um método virual puro denominado Execute(), que será recheado com o código associado à thread. Como em qualquer outra classe, em uma classe derivada de TThread se podem adicionar todos os membros (propriedades e métodos) que se precisem. Obrigatoriamente, só há que implementar:  O construtor da classe que permitirá definir a incialização da thread.  O método Execute() (que é como se fosse a função main da thread), que conterá o código associado à tarefa que deve ser executada pela thread.

7.3. Inicialização dos Threads A inicialização de uma thread se realiza no seu construtor, onde se estabelecem os valores iniciais de quantas propriedades forem necessárias. Antes de invocar o construtor de uma classe derivada de TThread, se invoca o construtor da sua classe base (TThread): __fastcall TThread(bool CreateSuspended);

Esse recebe como parâmetro false se desejarmos que a thread seja executada imediatamente após ser criada ou true se desejar que fique suspensa inicialmente. Duas características das threads convêm estabelecê-las no construtor: a sua prioridade e quando deve ser liberada.

7.3.1. Designação de uma Prioridade Default A prioridade de uma thread indica o grado de preferência que ela terá quando o sistema operacional distribuir o tempo da CPU entre as distintas threads que estejam sendo executadas pelo sistema. Para indicar a prioridade do objeto da thread há de fixar o valor da propriedade Priority como mostra a tabela a seguir. Valor

Prioridade

161


L

U

I

F

S

E

R

N

A

N

D

O

E

S

P

I

N

O

S

A

C

O

C

I

A

N

tpIdle

Se executa quando o sistema estiver inativo.

tpLowest

Dois pontos abaixo do valor normal.

tpLower

Um ponto abaixo do valor normal.

tpNormal

A thread possui prioridade normal.

tpHigher

Um ponto acima do valor normal.

tpHighest

Dois pontos acima do valor normal.

tpTimeCritical

A thread possui a mais alta prioridade.

Se for aumentado em muito a prioridade de uma thread que requeira muitos recursos (ex. CPU) pode ocorrer que as demais threads da aplicação (e do sistema) não cheguem a processar-se na velocidade adequada. Por exemplo, no caso de um processo que consome uma quantidade considerável de CPU, pode ser interessante dar a possibilidade ao usuário de cancelar o processo durante a sua execução. Isto pode ser conseguido criando uma thread com prioridade normal para esse processo e uma outra thread de alta prioridade que fique à espera de que o usuário pulse um botão de cancelamento. A thread de alta prioridade não consumirá tempo de CPU porque estará bloqueada mas, no caso de que o usuário pulse o botão, responderá rapidamente.

7.3.2. Liberação das Threads É frequente que as threads de uma aplicação sejam executadas uma única vez. Se este for o caso, o mais simples é deixar que o objeto thread se libere a si mesmo estebelecendo a propriedade FreeOnTerminate = true. Entretanto, existem casos em que o objeto thread representa uma tarefa que se deve realizar várias vezes. Se a aplicação requer várias instâncias do mesmo objeto thread (para executar a thread várias vezes), pode se aumentar o rendimento da aplicação armazenando as threads em um cache para utilizá-las mais adiante no lugar de criá-las e destruí-las a cada vez. Para isto basta fixar o valor da propriedade FreeOnTerminate a false. Neste caso, será responsabilidade do programador a liberação da thread. É importante colocar que antes de liberar uma thread há de ter certeza de que ela não está sendo executada (será visto a seguir). Ver o exemplo a seguir. Em UBola.h: #include "ObjGraf.h"

Na classe TThreadBola: … public:

162


E

N

G

E

N

H

A

R

I

A

D

E

P

R

O

C

E

S

S

A

M

E

N

T

O

D

I

G

I

T

A

L

I I

TBola *Bola; __fastcall TThreadBola(TPaintBox *PaintBox); __fastcall ~TThreadBola(void); …

Em UBola.cpp: #include <stdlib.h> … //--------------------------------------------------------------------------__fastcall TThreadBola::TThreadBola (TPaintBox *PaintBox) : TThread(false) { Priority = tpNormal; FreeOnTerminate = true; Bola = new TBola ( // Objeto bola... PaintBox, // ...aleatório. (TColor)(random(0xFF) | (random(0xFF)*0x100) | (random(0xFF)*0x10000)), random(PaintBox->Width), random(PaintBox->Height), random(20)+1, EnDirecao((random(2)+1) | (random(2)+1)*4)); } //--------------------------------------------------------------------------__fastcall TThreadBola::~TThreadBola(void) { delete Bola; } //---------------------------------------------------------------------------

7.4. Execução das Threads O método Execute é a função principal de uma thread. Uma thread pode ser concebida com sendo um programa que se executa dentro da aplicação. Uma thread não é um programa independente porque compartilha o mesmo espaço com as demais threads da aplicação. Portanto é necessário assegurar-se de não escrever na memória que utilizam outras threads. Por outro lado pode se usar a memória compartilhada entre as threads para estabelecer comunicação entre elas. Por exemplo: Em UBola.cpp: //--------------------------------------------------------------------------void __fastcall TThreadBola::Execute() { //---- Place thread code here ---while (true) { // Código da thread. try { Bola->Mover(); } catch (Exception &error) { } Sleep(100); } }

163


L

U

I

S

F

E

R

N

A

N

D

O

E

S

P

I

N

O

S

C

A

O

C

I

A

N

//---------------------------------------------------------------------------

7.4.1. Variáveis locais às Threads Com a palavra chave __thread pode se definir variáveis globais que são locais a uma thread. Haverá uma cópia da variável para cada uma das threads que forem criadas: a variável é global para as funções de uma thread mas não é compartilhada com outras threads. Ver o exemplo a seguir. int __thread x;

As variáveis __thread devem ser estáticas (não podem ser ponteiros) e não podem ser inicializadas em tempo de execução. Não é recomendável usar este tipo de variáveis, já que pode se conseguir a mesma funcionalidade adicionando uma variável de instância à classe da thread.

7.5. Controle da execução de uma Thread Uma thread pode encontrar-se em dois estados:  Sendo executada.  Estando suspensa

As threads podem ser iniciadas e suspensas tantas vezes for necessário antes

que

seja

finalizada

a

sua

execução.

Para

deter

uma

thread

temporariamente há que realizar uma chamada do método Suspend(). Quando resultar conveniente reiniciar a execução da thread faz-se chamada ao método Resume(). Os métodos Suspend() e Resume() usam um contador interno, de forma que é possível aninhar as chamadas a Suspend() e Resume(). A thread não reinicia a sua execução até que todas as suspensões tenham tido a sua correspondente chamada a Resume(). A execução de uma thread termina normalmente quando termina a execução do método Execute(). Para fazer que a execução de uma thread finalize de forma prematura há que realizar uma chamada ao método Terminate(). O método Terminate() altera o valor da propriedade Terminated a true, indicando assim que a thread deve finalizar a sua execução enquanto seja possível. Por tanto, método Execute() deve verificar constantemente o valor da propriedade Terminated e deter a execução da thread quanto o valor seja true. Ver o exemplo a seguir. Em UBola.cpp:

164


E

N

G

E

N

H

A

R

I

A

D

E

P

R

O

C

E

S

S

A

M

E

N

T

O

D

I

G

I

T

A

L

I I

//--------------------------------------------------------------------------void __fastcall TThreadBola::Execute() { //---- Place thread code here ---while (!Terminated) { // Código da thread. try { Bola->Mover(); } catch (Exception &error) { } Sleep(100); } } //---------------------------------------------------------------------------

Quando se cria uma thread, o estado inicial desta dependerá do parâmetro que se passe ao seu construtor: executando (false) ou suspensa (true). Se for criada uma threa inicialmente suspensa deve se invocar o método Resume() para começar a sua execução. Em UPrincipal.h para poder usar a unidade UBola: #include "UBola.h" ... private: // User declarations TThreadBola **Objs; ...

Em UPrincipal.cpp: #include <stdlib.h> ... //--------------------------------------------------------------------------void __fastcall TFrmPrincipal::FormCreate(TObject *Sender) { Objs = new TThreadBola*[4]; randomize(); for (int i=0; i<4; ++i){ Objs[i] = new TThreadBola (PaintBox); } } //--------------------------------------------------------------------------void __fastcall TFrmPrincipal::FormDestroy(TObject *Sender) { for (int i=0; i<4; ++i){ Objs[i]->Terminate(); } Sleep(3000); // Esperar três segundos... // ...a que se liberem todas as threads delete[] Objs; } //---------------------------------------------------------------------------

Remover as declarações antigas de TBola *Bola e a TObjGraf *Objs. Remover também o gerenciador de eventos do Timer1 apagando a linha Bola->Mover();

A linha Sleep(3000) é uma “enjambra” para evitar que a aplicação “pendure” ao liberar as threads. Mais adiante veremos como solucionar este problema de forma mais correta.

165


L

U

I

S

F

E

R

N

A

N

D

O

E

S

P

I

N

O

S

C

A

O

C

I

A

N

7.6. Coordenação entre as Threads O código executado por uma thread deve ter em conta a possível existência de outras que estejam sendo executadas de foram concorrente. Há de ter cuidado para evitar que duas threads acessem um recurso compartilhado (por exemplo, um objeto ou uma variável global) ao mesmo tempo. Além disso, a execução de uma thread pode depender do resultado das tarefas executadas por outras threads. Portanto, as distintas threads de uma aplicação devem ter uma execução coordenada.

7.7. Uso de Recursos Compartilhados Quando várias threads compartilham o uso de um recurso, podem ocorrer situações indesejadas se duas ou mais threads acessam (ou tentam acessar) o mesmo recurso simultaneamente). Para evitar conflitos com outras threads pode ser necessário bloquear a execução de outras threads ao acessar objetos ou variáveis

compartilhadas.

que

ter

cuidado

de

não

bloquear

desnecessariamente a execução de outras threads para não diminuir o rendimento de uma aplicação. Se executamos o exemplo anterior, mesmo que o funcionamento da aplicação seja correto, a visualização do PaintBox sofre alguns erros de forma aleatória. Isto se deve a que os métodos Mostrar() e Apagar() das distintas threads se entrelaçam no aceeso ao Canvas do PaintBox. Deve se evitar o acesso simultâneo das threads ao componente Canvas do PainBox.

7.7.1. A Thread Principal da VCL Não existe nenhuma garantia que os métodos dos componentes do C++ Builder funcionem corretamente quando são compartilhados por várias threads. Isto é, ao aceder às propiedades ou ao executar métodos podem ser efetaudas algumas operações que utilizem

memória não protegida das ações de outras

threads. Por este motivo se reserva uma thread, a “thread principal da VCL” para o acesso aos objetos da VCL. Esta é a thread que gerencia todas as mensagens do sistema operacional Windows que recebem os componentes da aplicação. Se todos os objetos acedem a suas propriedades e invocam os seus métodos dentro de uma única thread não há de que se preocupar. Para usar a thread da VCL principal há que criar um método que realize as ações necessárias

166


E

N

G

E

N

H

A

R

I

A

D

E

P

R

O

C

E

S

S

A

M

E

N

T

O

D

I

G

I

T

A

L

I I

e chamá-lo utilizando o método Synchronize(). Ver o exemplo a seguir. Em ObjGraf.h e ObjGraf.cpp o método Synchronize() recebe como parâmetro um método do tipo void __fastcall <método> (void) pelo que há de alterar a definição do método Mover() de TBola. Em ObjGraf.h: ... void __fastcall Mover (void); ...

Em ObjGraf.cpp: ... void __fastcall TBola::Mover (void) ...

Em UBola.cpp: //--------------------------------------------------------------------------void __fastcall TThreadBola::Execute() { //---- Place thread code here ---while (!Terminated) { Synchronize(Bola->Mover); Sleep(100); } } //---------------------------------------------------------------------------

Nem sempre é necessário usar a thread VCL principal (de fato, em alguns casos não se pode usar e deverão ser tentados outros mecanismos como as seções críticas). Alguns componentes estão preparados para funcionar com threads (são thread-safe) e não há necessidade de usar o método Synchronize(). Isto permite aumentar o rendimento da aplicação pois não é necessário esperar que a thread VCL principal entre no seu laço de mensagens. Os componentes de acesso aos dados funicionam corretamente com threads se pertencem a distintas seções. Os objetos gráficos funcionam corretamente com threads. Não é necessário utilizar a thread VCL principal para acessar a TFont, TPen, TBrush, TBitmap, TMetaFile nem TIcon. Para compartilhar o uso de objetos canvas (TCanvas e seus descendentes) há de bloqueá-los antes. Alguns componentes como o TList possuem uma versão adequada para funcionar com várias threads (TThreadList).

7.7.2. Bloqueo de objetos Alguns componentes contam com mecanismos de bloqueio para que possam ser compartilhados por várias threads. Por exemplo, os objetos do tipo

167


L

U

I

S

F

E

R

N

A

N

D

O

E

S

P

I

N

O

S

A

C

O

C

I

A

N

canvas (TCanvas e os seus descendentes) contam com um método Lock() que impede que as outras threads acessam o objeto até que seja realizada uma chamada ao método Unlock(). No caso de TThreadList, uma chamada a TThreadList::LockList() devolve o objeto do tipo Tlist associado e impede que outras threads acedam à lista até que se realize uma chamada a UnlockList(). As chamadas a outros métodos TCanvas::Lock ou TThreadList::LockList podem ser aninhados com segurança. O desbloqueio não se produz até que se tenha realizada uma chamada de desbloqueio para cada chamada de bloqueio. Ver o exemplo seguinte. Em UBola.cpp, eliminar a chamada ao método Synchronize() realizado em TThreadBola::Execute(). Em ObjGraf.cpp tanto em TBola::Mostrar() como em TBola::Apagar(): PaintBox->Canvas->Lock(); ... PaintBox->Canvas->Unlock();

7.7.3. Seções Críticas Se um objeto não conta com mecanismos de bloqueio incorporados, sempre que se pode deve se usar a seção crítica. As seções críticas funcionam com portas que permitem o passo de uma só thread de cada vez. Para utilizar uma seção crítica há que criar uma instância global de TCriticalSection. TCriticalSection conta com dois métodos: Acquire() – que impede a outras threads acessar à seção crítica – e Release() – que elimina o bloqueio. Cada seção crítica se associa a um recurso que se deseja compartilhar por várias threads. Todas as threads que acessem a um mesmo recurso deverão utilizar o método Acquire() da correspondente seção crítica para assegurar de queo recurso não está sendo utilizado por nenhuma outra thread. Ao finalizar suas operações as threads devem realizar uma chamada ao métod Release() para que as demais threads possam acessar ao recurso invocando o método Acquire(). Se omitir a chamada a Release() o recurso ficará bloqueado para sempre. Por exemplo, em ObjGraf.cpp: #include <syncobjs.hpp> ... TCriticalSection *SC = new TCriticalSection();

Em TBola::Mostrar() e em TBola::Apagar(), eliminar o bloqueio do objeto do tipo TCanvas e usar no seu lugar a seção crítica: { SC->Acquire();

168


E

N

G

E

N

H

A

R

I

A

D

E

P

R

O

C

E

S

S

A

M

E

N

T

O

D

I

G

I

T

A

L

I I

try { ... } __finally { SC->Release(); } }

7.8. Sincronização entre Threads Pode acontecer que distintas threads de uma aplicação realizem tarefas que não são completamente independentes, pelo qual será necessário que em certas ocasiões uma thread espere a que outra termine a execução de uma ação determinada. Por exemplo, no caso de duas threads que calculam o total de ingressos e de gastos de uma companhia. Uma terceira thread deve obter o balanço final, e por isso deverá esperar que as duas threads anteriores terminem o seu trabalho.

7.8.1. Espera a finalização de uma T hre a d Para esperar que finalize a execução de outra thread, pode ser usado o método WaitFor() da outra thread. WaitFor() bloqueia a execução da thread atual até que a outra thread tenha sido encerrada, seja por haver finalizado a execução do seu método Execute() ou por ter acontecido uma exceção. WaiFor() devolve um valorinteiro que é o valor da propriedade ReturnValue da thread. A propriedade ReturnValue pode ser alterada no método Execute() e o seu significado será dado pelo programador. Por exemplo, em UPrincipal.cpp: void __fastcall TFrmPrincipal::FormDestroy(TObject *Sender) { int i; for (i=0; i<4; i++) Objs[i]->Terminate(); for (i=0; i<4; i++) try { Objs[i]->WaitFor(); } catch (Exception &error) { } delete[] Objs; }

169


L

U

I

F

S

E

R

N

A

N

D

O

E

S

P

I

N

O

S

A

C

O

C

I

A

N

7.8.2. Esperar completar uma tarefa: Sucessos Em algumas ocasiões é necessário esperar que outra thread tenha realizado uma tarefa determinada sem que isto implique em que haja finalizado a sua execução. Nestes casos há que utilizar um objeto do tipo sucesso (TEvent). Quando uma thread completa uma operaçào da que dependem outras threads, realiza a chamada a TEvent::SetEvent(). SetEvent() ativa um sinal que qualquer thread pode detectar invocando ao método TEvent::WaitFor(). Para desativar este sinal existe o método TEvent::ResetEvent(). O método WaitFor() espera um tempo determinado (estabelecido em milisegundos ao chamar o método) até que se ative o sinal. WaitFor() pode deveolver um dos seguintes valores: Valor

Significado

wrSignaled

O sinal associado ao sucesso foi ativado.

wrTimeout

Tem transcorrido o tiempo especificado sem a ativação do sinal.

wrAbandoned

O objeto de tipo TEvent foi destruido antes do timeout.

wrError

Foi produzido um erro durante a espera. O código do erro concreto pode ser obtido da propiedade TEvent::LastError.

Se desejar seguir a espera de um sucesso indefinidamente pode se passar o valor INFINITE como parâmetro do método WaitFor(). Mas deve se ter cuidado, pois a thread ficará bloqueada por tempo indefinido se nunca se chegar a produzir o evento que espera.

170


07-Threads-C++BuilderV10