Praticando concorrência em Java! – Semáforos

Como falado no post anterior sobre concorrência em java, o problema do Produtor/Consumidor pode ser resolvido utilizando um monitor que bloqueia o recurso, permitindo apenas um acesso de cada vez.

Desta vez vamos comentar um pouco sobre semáforos, que permitem um número maior de Thread acessando os recursos.

Semáforos

Um semáforo é uma estrutura de dados que controle o acesso de aplicações aos recursos, baseando-se em um número inteiro, que representa a quantidade de acessos que podem ser feitos. Assim utilizamos semáforos para controlar a quantidade de acesso a determinado recurso.

Na API do Java existe uma implementação de semáforos que fazem justamente esse controle. Vamos ver a seguir um exemplo que usa o Semáforo nativo do Java.

Utilizando semáforos

Primeiro vamos definir uma implementação de Thread que vai utilizar o semáforo.

public class ProcessadorThread extends Thread {
	private int idThread;
	private Semaphore semaforo;

	public ProcessadorThread(int id, Semaphore semaphore) {
		this.idThread = id;
		this.semaforo = semaphore;
	}
}

Definimos inicialmente um identificador para a nossa Thread e uma referência a um semáforo que irá controlar o acesso a essas variáveis.

Agora vamos definir os métodos da nossa Thread, dentro da classe ProcessadorThread:

	private void processar() {
		try {
			System.out.println("Thread #" + idThread + " processando");
			Thread.sleep((long) (Math.random() * 10000));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

Este método processar() apenas faz a thread dormir por algum tempo, simulando o efeito de um processamento longo.

	private void entrarRegiaoNaoCritica() {
		System.out.println("Thread #" + idThread + " em região não crítica");
		processar();
	}

Este método simula o acesso da Thread em uma região não crítica, ou seja, uma região ao qual não é necessário pedir uma trava. Exibimos o atual estado da Thread, para facilitar o entendimento do progama, e realizamos um processamento qualquer.

	private void entrarRegiaoCritica() {
		System.out.println("Thread #" + idThread
				+ " entrando em região crítica");
		processar();
		System.out.println("Thread #" + idThread + " saindo da região crítica");
	}

Este outro método será utilizado para simular o acesso da Thread em uma região crítica. Ele será chamado logo após conseguir a trava do semáforo.

	public void run() {
		entrarRegiaoNaoCritica();
		try {
			semaforo.acquire();
			entrarRegiaoCritica();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			semaforo.release();
		}
	}

E finalmente, como nossa classe extende o comportamento de uma Thread, nós sobrecarregamos o método run() que será chamado quando a Thread iniciar. Neste método nós realizamos um processamento não crítico, depois requisitamos o acesso ao semáforo (com o semaforo.acquire()) e em seguida realizamos o processamento de uma região crítica. Por fim, liberamos o recurso do semáforo (com o semaforo.release()).

Entendendo um pouco mais sobre semáforos

Vamos agora falar um pouco de teoria sobre semáforos. Todo semáforo deve possuir dois métodos: P e V, que têm sua origem das palavras parsen (passar) e e vrygeren (liberar). Esta definição de semáforo foi proposta por Dijkstra para evitar o tão temido DeadLock.

Quando se quer requisitar o recurso, faz-se uma chamada ao método P, que verifica se é possível liberar o recurso. Ao terminar, faz-se uma chamada ao método V, que notifica as outras Thread que o recurso foi liberado.

Na implementação do Java, o método “acquire()” faz o papel do método P e o método “release()” faz o papel do método V. Vamos analisar o método main e ver como são utilizados os semáforos:

	public static void main(String[] args) {
		int numeroDePermicoes = 2;
		int numeroDeProcessos = 6;
		Semaphore semaphore = new Semaphore(numeroDePermicoes);
		ProcessadorThread[] processos = new ProcessadorThread[numeroDeProcessos];
		for (int i = 0; i < numeroDeProcessos; i++) {
			processos[i] = new ProcessadorThread(i, semaphore);
			processos[i].start();
		}
	}

Para construir um semáforo precisamos informar o número máximo de Thread que podem acessar o recurso ao mesmo tempo. No nosso exemplo, apenas duas Thread poderão entrar na região crítica. Executando o programa a saída no console será algo do tipo:

Thread #1 em região não crítica
Thread #1 processando
Thread #0 em região não crítica
Thread #0 processando
Thread #5 em região não crítica
Thread #5 processando
Thread #3 em região não crítica
Thread #3 processando
Thread #2 em região não crítica
Thread #2 processando
Thread #4 em região não crítica
Thread #4 processando
Thread #0 entrando em região crítica
Thread #0 processando
Thread #4 entrando em região crítica
Thread #4 processando
Thread #4 saindo da região crítica
Thread #5 entrando em região crítica
Thread #5 processando
Thread #5 saindo da região crítica
Thread #3 entrando em região crítica
Thread #3 processando
Thread #0 saindo da região crítica
Thread #1 entrando em região crítica
Thread #1 processando
Thread #1 saindo da região crítica
Thread #2 entrando em região crítica
Thread #2 processando
Thread #3 saindo da região crítica
Thread #2 saindo da região crítica

Como já comentamos, no Java os Thread são acordados aleatoriamente, então a saída não será a mesma. O que é realmente importante notar é que nunca temos mais que duas Thread na região crítica.

A Thread 0 e 4 entram na área crítica e, somente quando a Thread 4 libera o recurso, a Thread 5 entra na região.

No próximo post vamos ver como implementar um semáforo próprio para resolver o problema do Produtor/Consumidor.

Obrigado pela visita, espero que este post tenha ajudado. Se gostou do post compartilhe com seus amigos e colegas, senão, comente o que pode ser melhorado. Encontrou algum erro no código? Comente também. Possui alguma outra opinião ou alguma informação adicional? Comenta ai! 🙂

Referências:

[1] JENKOV, Jakob. Semaphore. Disponível em: http://tutorials.jenkov.com/java-concurrency/semaphores.html. Acesso em: 25 set. 2011.
[2] LIMA J., José; FREITAS, Veronice de. Semáforos. Disponível em: http://www.jr.eti.br/mestrado/cmp041/semaforos.html. Acesso em: 25 set. 2011.

Anúncios

Praticando concorrência em Java!

Neste post vamos mostrar um exemplo prático de programação com concorrência em Java explorando as ferramentas da linguagem.

E no começo haviam os monitores

Para começar vamos falar sobre uma estrutura básica na programação concorrente: o monitor.

Segundo [1] monitor é uma técnica utilizada para sincronizar tarefas que compartilham um recurso. ou seja, o monitor oferece uma interface para permitir a manipulação de um recurso compartilhado. Para tanto utiliza uma trava de exclusão mútua, que consiste de variáveis de controle e regras para liberar ou travar o recurso.

De maneira prática, o monitor basicamente é uma classe que controla o recurso, assim quando for preciso requisitá-lo devemos enviar o pedido para ela.

Produtor/Consumidor

Para exemplificar vamos analisar o problema do Produtor e Consumidor. Segundo [2]:

Uma ou mais thread de produtores criam um produto e colocam em um buffer. Uma ou mais thread de consumidores consomem o produto colocado no buffer. O produtor precisa esperar o buffer ficar livre para produzir o produto e o cliente precisa esperar o buffer ficar preenchido para consumir o produto.

A tarefa do problema é sincronizar o acesso ao recurso, no caso a pilha, para que produtores saibam quando podem produzir e consumidores saibam quando podem consumir. Vamos então para a parte prática!

Primeiro vamos criar a classe do Consumidor:

public class Consumidor extends Thread {
	private int idConsumidor;
	private Buffer pilha;
	private int totalConsumir;

	public Consumidor(int id, Buffer p, int totalConsumir) {
		idConsumidor = id;
		pilha = p;
		this.totalConsumir = totalConsumir;
	}

	public void run() {
		for (int i = 0; i < totalConsumir; i++) {
			pilha.get(idConsumidor);
		}
		System.out.println("Consumidor #" + idConsumidor + " concluido!");
	}
}

Essa classe é derivada da clase Thread, ou seja, cada cliente vai funcionar em um thread diferente. Os dados que utilizamos são um identificador (idConsumidor), uma referência para um Buffer e um contador (totalConsumir) que vai indicar quanto deve ser consumido pelo consumidor.

O método run() é chamado quando a thread for iniciada, ou seja, é nele que devemos definir o nosso cliente de fato. Definimos então o laço para executar as chamadas que consomem o recurso do buffer.

Vamos então definir a classe Produtor:

public class Produtor extends Thread {
	private int idProdutor;
	private Buffer pilha;
	private int producaoTotal;

	public Produtor(int id, Buffer p, int producaoTotal) {
		idProdutor = id;
		pilha = p;
		this.producaoTotal = producaoTotal;
	}

	public void run() {
		for (int i = 0; i < producaoTotal; i++) {
			pilha.set(idProdutor, i);
		}
		System.out.println("Produtor #" + idProdutor + " concluido!");
	}
}

De maneira semelhante ao Consumidor, o produtor possui um identificador, uma referência para um buffer e um total de produtos a serem produzidos. Note que essa classe também é uma Thread, ou seja, cada produtor será executado independentemente. O método run() também é bem semelhante ao do Consumidor, um laço que faz as chamadas ao buffer.

Sincronizando o acesso ao recurso compartilhado

Agora vem a parte principal, o buffer. Ele possui um conteúdo, que é colocado pelo produtor, e um booleano que indica quando o conteúdo está disponível.

Para permitir que um produtor coloque um produto ela oferece um método set, que carrega o conteúdo e avisa para as outras thread que o produto está disponível. Para dar acesso ao Consumidor, o buffer oferece um método get, que devolve o conteúdo e avisa para outras thread que o produto não está mais disponível.

Esta classe merece uma atenção especial e será detalhada em partes menores. Primeiro veremos o método set():

	public synchronized void set(int idProdutor, int valor) {
		while (disponivel == true) {
			try {
				System.out.println("Produtor #" + idProdutor + " esperando...");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		conteudo = valor;
		System.out.println("Produtor #" + idProdutor + " colocou " + conteudo);
		disponivel = true;
		notifyAll();
	}

Enquanto houver um conteúdo disponível no buffer, o produtor deve esperar. Quando o buffer não estiver mais com conteúdo disponível, o conteúdo é carregado e as outras thread são notificadas que um novo conteúdo está disponível.

	public synchronized int get(int idConsumidor) {
		while (disponivel == false) {
			try {
				System.out.println("Consumidor #" + idConsumidor
						+ " esperado...");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		System.out.println("Consumidor #" + idConsumidor + " consumiu: "
				+ conteudo);
		disponivel = false;
		notifyAll();
		return conteudo;
	}

De forma semelhante ao Produtor, no método get o consumidor deve esperar enquanto não houver conteúdo disponível. Quando o conteúdo estiver disponível novamente o consumidor consome o recurso e notifca as outras thread que não há mais recurso disponível.

O código completo da classe Buffer fica assim:

package br.concorrencia.produtorConsumidor;

public class Buffer {

	private int conteudo;
	private boolean disponivel;

	public synchronized void set(int idProdutor, int valor) {
		while (disponivel == true) {
			try {
				System.out.println("Produtor #" + idProdutor + " esperando...");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		conteudo = valor;
		System.out.println("Produtor #" + idProdutor + " colocou " + conteudo);
		disponivel = true;
		notifyAll();
	}

	public synchronized int get(int idConsumidor) {
		while (disponivel == false) {
			try {
				System.out.println("Consumidor #" + idConsumidor
						+ " esperado...");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		System.out.println("Consumidor #" + idConsumidor + " consumiu: "
				+ conteudo);
		disponivel = false;
		notifyAll();
		return conteudo;
	}
}

O importante ai é verificar que os métodos get e set do buffer possuem um modificador especial: synchronized.

Segundo [4], métodos sincronizados são uma estratégia simples para prevenir erros de interferência e inconsistência de memória. Se um objeto é acessado por mais de uma thread (o que no nosso exemplo é o objeto Buffer), todas as operações de escrita e leitura neste objeto devem ser feitas através de métodos sincronizados.

Para testar o nosso código, vamos criar o seguinte método main:

	public static void main(String[] args) {
		Buffer bufferCompartilhado = new Buffer();
		Produtor produtor1 = new Produtor(1, bufferCompartilhado, 5);
		Produtor produtor2 = new Produtor(2, bufferCompartilhado, 5);
		Consumidor consumidor1 = new Consumidor(1, bufferCompartilhado, 2);
		Consumidor consumidor2 = new Consumidor(2, bufferCompartilhado, 8);

		produtor1.start();
		consumidor1.start();
		produtor2.start();
		consumidor2.start();
	}

Criamos dois produtores e dois consumidores. Em seguida executamos o método da classe Thread chamado start(). Este método vai então inicializar a Thread e logo em seguida vai executar o método run(), que nós definimos nas classes Consumidor e Produtor.

Observando a saída no console, temos algo do tipo:

Produtor #1 colocou 0
Produtor #1 esperando…
Consumidor #1 consumiu: 0
Consumidor #1 esperado…
Produtor #1 colocou 1
Produtor #1 concluido!
Consumidor #1 consumiu: 1
Consumidor #1 esperado…
Produtor #2 colocou 0
Produtor #2 esperando…
Consumidor #2 consumiu: 0
Consumidor #2 concluido!
Produtor #2 colocou 1
Produtor #2 concluido!
Consumidor #1 consumiu: 1
Consumidor #1 concluido!

A ordem de execução pode mudar pois a chamada aos Thread são feitas aleatoriamente em Java. Quando o método wait() na classe Buffer é executado, a Thread que está acessando ele fica em estado de espera. Quando o método notifyAll() é chamado, TODAS as Thread que estão em espera são acordadas e executadas, por isso definimos um laço while nos métodos get e set, ou seja, a Thread só será liberada quando a condição for verdadeira.

Uma discussão mais aprofundada sobre o assunto pode ser encontrada em [4]. No próximo post vamos ver alguns exemplos mais legais usando semáforos de diversos tipos para controlar o acesso ao recurso compartilhado.

Obrigado pela visita, espero que este post tenha ajudado. Se gostou do post compartilhe com seus amigos e colegas, senão, comente o que pode ser melhorado. Encontrou algum erro no código? Comente também. Possui alguma outra opinião ou alguma informação adicional? Comenta ai! 🙂

Referências:

[1] WIKIPEDIA. Monitor (concorrência). Disponível em: http://pt.wikipedia.org/wiki/Monitor_(concorrência). Acesso em: 23 set. 2011.
[2] MARTIN, Robert C. Clean code: a handbook of agile software craftsmanship.
[3] Sincronização de Threads. Disponível em: http://www.dsc.ufcg.edu.br/~jacques/cursos/map/html/threads/sincronizacao.html. Acesso em 23 set. 2011.
[4] ORACLE. Synchronized Methods. Disponível em: http://download.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html. Acesso em 23 set. 2011.