de incompatibilidade, que engloba defeitos relacionados ao tipo de chamada, argumentos e tama- nho dos parâmetros; e, por fim, de recursos, constituídos por defeitos de alocação, inicialização e desalocação de recursos.
Pedersen(2006) apresenta uma categorização de defeitos em programas de passagem de mensagem através da observação e análise de programas desenvolvidos por estudantes da graduação. Um dos resultados é a classificação de defeitos em sete categorias: 1) Decomposição de dados - relacionado à maneira com que é feita a decomposição dos dados ao se mapear a versão sequencial de um programa para a versão paralela; 2) Decomposição funcional - na qual a origem do defeito está no modo de decomposição da funcionalidade no momento da implementação da versão paralela; 3) Uso de API - relaciona-se ao uso incorreto das APIs como a passagem de tipos incorretos; 4) Defeito sequencial - ou seja, defeitos também vistos em programas sequenciais, por exemplo, uso do operador atribuição (=) ao invés do operador igual (==); 5) Problema de mensagem - abrange o envio e recebimento de dados incorretos; 6) Problema de protocolo - relacionado à perda ou ausência de mensagens e 7) Outros - defeitos que não se enquadram em nenhuma das categorias anteriormente citadas.
Além desses trabalhos que tratam de uma variada gama de defeitos presentes em várias linguagens de programação, há investigações exclusivamente na presença de defeitos em progra- mas concorrentes desenvolvidos em Java. Como a ferramenta ValiPar (objeto de estudos neste projeto) foi desenvolvida para o teste de programas concorrentes Java, a próxima seção apresenta defeitos em programas concorrentes nessa linguagem.
5.3 Defeitos em Programas Concorrentes Java
EmFarchi, Nir e Ur(2003) é apresentada e categorizada uma taxonomia de padrões de defeitos, os quais correspondem à forma literária de descrever defeitos recorrentes na imple- mentação de programas de computador. Os autores ilustraram alguns padrões de defeitos que ocorrem na prática, classificando-os como:
1)Código assumido estar protegido
Um segmento de código concorrente é dito protegido se qualquer interleaving e qualquer execução do segmento de código por uma thread não sofre interferência de nenhuma outra thread. Em outras palavras, nenhuma outra thread executa o mesmo trecho de código de modo simultâneo, considerando do primeiro ao último evento concorrente executado pela primeira thread. Este padrão de defeito ocorre quando um segmento de código é assumido como protegido, quando na verdade não está. Ele pode ocorrer de várias formas, tais como:
• operação não atômica assumida como atômica: este defeito pode estar relacionado com o nível de abstração da linguagem de programação. Em Java, por exemplo, a operação de pós-incremento x++ pode aparentar ser de uma única operação atômica, entretanto,
a tradução desta operação em bytecode consiste em pelo menos três eventos possíveis de sofrer troca de contexto (leitura de x, incremento e escrita de x), resultando em um código desprotegido. Como o programador está ciente apenas de uma operação x++, erroneamente assume-se uma atomicidade.
• acesso em dois estágios: há casos em que uma sequência de operações deve ser protegida e erroneamente assume-se que proteger cada operação separadamente é suficiente. • bloqueio incorreto ou ausência de bloqueio: um segmento de código é protegido por
um lock mas outras threads não obtêm a mesma instância do lock durante a execução. O resultado é a possível ocorrência de interferência entre threads que podem executar segmentos de código que não estão realmente protegidos.
• bloqueio de dupla checagem: relacionado à inicialização de objetos em programas con- correntes. Quando um objeto é inicializado, a thread copia para sua área local o objeto inicializado mas nem todos os campos do objeto são transcritos para a pilha e isto faz com que na verdade o objeto seja parcialmente inicializado. Assim como no primeiro caso, a inicialização de um objeto é enganosa no nível de abstração da linguagem e os possíveis interleavings das atribuições reais para a área de memória local da thread não são considerados.
2)Interleavings previstos para nunca ocorrerem
O programador erroneamente assume que um certo interleaving nunca irá ocorrer. Este tipo de erro pode ocorrer, por exemplo, das seguintes formas:
• uso de sleep: uma thread - pai, por exemplo - deve aguardar o resultado de outra - filha, neste caso - e o programador acredita (erroneamente) que inserir uma primitiva sleep no pai garantirá que a filha terá tempo para terminar antes. O defeito é evidenciado nos casos em que a thread pai é mais rápida que a thread filha mesmo com o sleep. A solução neste caso é o uso de um join().
• perda de um notify: ocorre quando uma primitiva notify() é executada antes de seu wait() correspondente. Esta situação provoca a perda da primitiva notify() que não terá efeito e portanto o código executando o wait() pode não ser acordado. Novamente o programador tem a ideia errônea de que, em todas as situações, a primitiva wait() ocorrerá antes de quaisquer operações notify() correspondentes.
3)Bloqueio ou morte de uma thread
Ocorre quando alguns dos possíveis interleavings bloqueiam indefinidamente a aplicação. Alguns exemplos deste defeito são:
5.3. Defeitos em Programas Concorrentes Java 59
• bloqueio de uma região crítica: ocorre quando uma thread entra em uma região crítica e, eventualmente, nunca sai dela. Esta situação pode ocorrer se uma thread entrar na região crítica e então executar uma operação bloqueante de entrada e saída, que pode não ser atendida. Outras threads podem ficar esperando para entrar na região crítica; mas não conseguirão.
• thread órfã: ocorre quando várias threads são gerenciadas por uma única thread mestre. Este gerenciamento é feito por envio de mensagens e caso a thread mestre finalize sua exe- cução de forma inesperada ou anormal, as outras threads podem continuar suas execuções e ficar esperando por novas mensagens fazendo com que o sistema trave.
Além dos defeitos listados acima,Bradbury et al.(2012) citam mais três tipos de defeitos que são:
• uso do notify ao invés do notifyAll : ocorre quando é executado um notify() ao invés de um notifyAll(), então várias threads que estão bloqueadas pelo wait() correspondente não serão notificadas. Na prática apenas uma delas será escolhida para ser acordada e não todas, como era o esperado.
• interferência: ocorre quando duas ou mais threads fazem o acesso a uma variável com- partilhada e pelo menos um desses acessos é para a escrita. Caso as threads não façam o uso de mecanismos de exclusão mútua uma interferência pode ser notada.
• deadlock (DIJKSTRA,1965; TAI,1994): ocorre quando dois ou mais processos ficam impossibilitados de prosseguir suas computações pois um está a espera do outro, fazendo com que o sistema entre em um deadlock. Isto pode ocorrer, por exemplo, quando uma thread obtém um lock que outra thread deseja obter e vice-versa.
Os trabalhos descritos emBradbury, Cordy e Dingel(2006) sobre o uso da análise de mutantes para avaliar, comparar e melhorar as técnicas de garantia de qualidade para programas concorrentes Java, citam outros tipos de defeitos como:
• outros sinais ausentes ou inexistentes: esta é uma generalização do defeito “perda de um notify” visto anteriormente. Esta categoria engloba todas as perdas de outros sinais, por exemplo, quando em uma barreira a primitiva await() tem que ser chamada por um número definido de threads antes do programa prosseguir e por algum motivo alguma thread não o faz. Neste caso todas as outras threads estarão esperando na barreira. • starvation (BEN-ARI, 1982; TAI,1994): ocorre quando alguma thread nunca recebe
um tempo de CPU para ser executada. Este fato pode ocorrer pela própria política de escalonamento do sistema e pelas definições de prioridades dadas às threads.
• exaustão de recursos: ocorre quando um conjunto de threads se apossam de todos os recursos disponíveis e nenhuma delas os liberam enquanto alguma thread precisa de recursos adicionais para sua computação.
• inicialização incorreta de contadores: ocorre quando se inicializa um contador com um valor incorreto, por exemplo, em uma barreira quanto ao número de threads que devem estar esperando para que se prossiga a computação, ou a inicialização incorreta de um semáforo.
Em Long, Strooper e Wildman (2007) é citado também livelock (KWONG, 1979;
KWONG, 1981; TAI, 1994), além dos defeitos de interferência, deadlock, perda de um no- tify e outros sinais ausentes ou inexistentes. No livelock, diferentemente do deadlock em que nenhuma computação pode ser feita, há computações que podem ser feitas, sem levar a aplicação à frente.