Threads são partes de um programa que executam paralelamente. Com seu uso é possível executar simultâneamente duas ou mais rotinas. Atualmente, com o barateamento dos processadores de vários núcleos (multi core), seu uso fica mais justificado. Embora eu não recomende trabalhar com threads, convém entender o que elas representam na prática.
Antes de tudo vou ressaltar: não recomendo trabalhar com threads a menos que seja o desenvolvimento de um jogo AAA, o qual não é foco deste site (digamos um Batman: Asilo Arkhan). Jogos assim, exigem intenso processamento de I.A., física e gráficos e possuem, em geral, muito dinheiro para serem feito. Isso justifica a utilização de uma forma de programação mais complexa. Ainda mais que estes jogos em geral são criados em C++, uma das piores linguagens para trabalhar com threads. Mesmo no Java, cujo uso é relativamente simples (como veremos abaixo), depurar um programa com thread é um inferno.
Além disso, projetar o código para tirar proveito de threads é tarefa para arquitetos competentes – caso contrário você vai criar um código complexo, difícil de depurar e manter e, provavelmente, vai cair em dead locks e outros problemas de concorrência . E ainda corre o risco de acabar com um código pouco eficiente, apesar do esforço adicional.
Fica então o aviso: o código abaixo serve para efeito de exemplo e não deve ser base para um jogo do mundo real.
Bom, se depois do que foi dito acima você ainda não desistiu de ler sobre threads, vamos em frente – o fato é que o conceito por trás delas, e sua correta utilização, são importantes para tirar todo proveito de uma CPU com múltiplos núcleos. Vamos ver como fazer isso com o gameloop desenvolvidos nos meus artigos anteriores. Se você não leu, filtre os posts pela tag Java e leia-os antes, ok?
Modularizando o jogo
Começo criando duas classes especializadas em atualizar a lógica do jogo (lembra do update?) e em atualizar a tela (lembra do render?). Chamarei elas de Controlador e Renderizador, respectivamente. Seu código está abaixo. Basicamente, eu tirei as rotinas update() e render() da minha classe do jogo e coloquei em classes separadas.
Arquivo Controlador.java
001package abrindoojogo.exemplos.thread; 002 003import javax.swing.JFrame; 004 005public class Controlador 006{ 007 Contador contador; 008 JFrame frame; 009 Nave nave; 010 011 public Controlador(JFrame frame, Contador contador, Nave nave) 012 { 013 this.frame = frame; 014 this.contador = contador; 015 this.nave = nave; 016 } 017 018 public void update() 019 { 020 contador.contaPulso(); 021 nave.x += 1; 022 if (nave.x > frame.getWidth() + 30) 023 { 024 nave.x = 0; 025 } 026 } 027}
Arquivo Renderizador.java
001package abrindoojogo.exemplos.thread; 002 003import java.awt.Color; 004import java.awt.Graphics2D; 005import java.awt.image.BufferStrategy; 006import javax.swing.JFrame; 007 008public class Renderizador 009{ 010 BufferStrategy bs; 011 Contador contador; 012 JFrame frame; 013 Nave nave; 014 public boolean terminado = false; 015 016 public Renderizador(JFrame frame, BufferStrategy bs, 017 Contador contador, Nave nave) 018 { 019 this.frame = frame; 020 this.bs = bs; 021 this.contador = contador; 022 this.nave = nave; 023 } 024 025 public void render() 026 { 027 contador.contaFrame(); 028 Graphics2D g = (Graphics2D) bs.getDrawGraphics(); 029 g.setColor(Color.black); 030 g.fillRect(0, 0, frame.getWidth(), frame.getHeight()); 031 int x = nave.x; 032 int y = nave.y; 033 for (int i = 0; i < nave.qtd; i++) 034 { 035 g.setColor(Color.yellow); 036 g.drawLine(x, y, x - 20, y - 5); 037 g.drawLine(x, y, x - 20, y + 5); 038 g.drawLine(x - 20, y - 5, x - 20, y + 5); 039 y += 15; 040 if (y > 550) 041 { 042 y = nave.y; 043 x += 15; 044 } 045 } 046 g.setColor(Color.white); 047 g.drawString("Pulsos: " + contador.getPulsosPorSegundo() + 048 " Frames: " + contador.getFramesPorSegundo() + 049 " naves: " + nave.qtd, 10, 20); 050 g.dispose(); 051 bs.show(); 052 } 053}
Veja que essas classes recebem “de fora”, no momento de sua construção, os objetos necessários para seu funcionamento. Ou seja, elas não possuem dados próprios, encapsulando apenas o algoritmo. Para que elas compartilhem os mesmos dados, criei também uma classe separada para armazenar os dados da nave:
Arquivo Nave.java
001package abrindoojogo.exemplos.thread; 002 003public class Nave 004{ 005 int x; 006 int y; 007 int qtd; 008}
A classe do jogo em sí é exatamente igual à classe JogoLoopSimples vista em um artigo anterior, só que sem update() e sem render() e com o gameloop modificado conforme abaixo:
001public void gameloop() 002 { 003 initialize(); 004 Controlador c = new Controlador(this, contador, nave); 005 Renderizador r = new Renderizador(this, bs, contador, nave); 006 while (true) 007 { 008 c.update(); 009 r.render(); 010 } 011 }
Além disso, mais uma pequena modificação: ao sair do programa (pressionar ESC), envio para o console os dados de pulsos e frames, para registro. Isso é feito com um System.out.println() chamado na rotina keyPressed():
001public void keyPressed(KeyEvent e) 002 { 003 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) 004 { 005 System.out.println("Pulsos: " + contador.getPulsosPorSegundo() + 006 " Frames: " + contador.getFramesPorSegundo() + 007 " naves: " + nave.qtd); 008 System.exit(0); 009 } 010 ...
Executando o programa assim, temos o seguinte resultado:
Reconhece a tela? É o mesmo protótipo da nave dos outros posts. Mas perai… Uau! 460 fps! Não eram apenas 40! Como foi que ficou mais rápido?
Simples: troquei de máquina… Veja: essa é uma máquina bem mais rápida e tem um processador de dois núcleos como podemos ver no painel do gerenciador de tarefas. Note outra coisa também: apesar do jogo estar programado de forma a rodar o mais rápido possível (sem limitação de pulsos), ele utiliza apenas 50% do processamento da máquina. Nos gráficos pode-se ver claramente que o jogo está ocupando praticamente todo núcleo da esquerda, enquanto o da direita está ocioso (quase ocioso, na verdade, está atendendo o S.O. e processos de fundo, como o antivirus).
Abaixo da tela está a saída do console do NetBeans, mostrando o número de pulsos e frames registrados ao fechar o programa.
A soma das partes
Modifiquemos agora nosso gameloop comentando o update(). Assim vamos ver quantos frames conseguimos se eliminarmos o trabalho de atualizar a lógica do jogo.
A saída é a seguinte:
Veja que temos zero pulsos, já que update() nunca foi executada. Mas a quantidade de frames não mudou. Ou seja, o impacto de processamento da nossa lógica é muito pequeno. Vejamos o contrário. Vamos comentar o render().
Resultado abaixo:

Pois é, sem o render() não aparece a tela. Mas o programa está lá, rodando, como podemos ver pelo processamento. Ao pressionar ESC basta olhar para a saída para ver a quantidade de pulsos e… Caraca! Mais de três milhões e meio de pulsos por segundo! Realmente, a parte (bem) mais pesada é a atualização da tela. A atualização da lógica (nesse caso, pelo menos) é tão efêmera que esse computador consegue realizar mais de 3.000.000 em um segundos contra apenas 460 atualizações da tela no mesmo tempo.
Analisando os resultados, entram as Threads
A primeira coisa importante é ver como o render() segura o jogo quando usamos esse tipo de gameloop, que é o mais simples. A cada volta do laço temos um update() (muito rápido) e um render(), que demora e faz com que o próximo update() acabe demorando a ser executado.
Mas o que conta para este post é o seguinte: reveja as imagens acima e preste atenção no processamento. Ele nunca passa de 50%. Ou seja, meu jogo não está tirando tudo da minha máquina nova! Praticamente ele só usa o núcleo da esquerda, enquanto o outro fica com pouco processamento, oriundo das tarefas do sistema. Isso é bom, na verdade. Mas e se eu precisasse de mais processamento? Teria uma forma de usar o outro núcleo? Tem sim, com threads.
Você sabe que o S.O. é multitarefa, ou seja, executa vários programas ao mesmo tempo. Você pode ver um vídeo e ouvir música ao mesmo simultâneamente, porque o S.O dá um pouco de processador para o vídeo e um pouco para a música. É a mesma CPU, porém dividida entre dois programas.
Se tem mais de uma CPU (mais de um núcleo, como na máquina acima), o S.O. pode dar um núcleo inteiro para o vídeo e o outro núcleo para a música, ao invés de dividir a mesma CPU. Isso torna as coisas mais ágeis ainda.
As threads permitem ter esta multitarefa dentro do mesmo programa. Por exemplo, você cria uma thread para carregar os dados do jogo e outra para rodar uma animação. Assim, o S.O. dá um pouco de processamento para cada uma, e o resultado é que você pode mostrar uma animação enquanto os dados são carregados.
A modificação que faço a seguir no código vai permitir colocar o Controlador em uma thread e o Renderizador em outra, de forma a executarem paralelamente e, de quebra, rodar cada um em um núcleo, utilizando todo o poder da máquina. Se o render() sozinho dá 460 frames e update(), também sozinho, dá 3.000.000, agora deverei obter o jogo rodando simultâneamente com 3.000.000 pulsos e 460 frames.
Vamos ver se funciona:
001public class Controlador implements Runnable 002{ 003 Contador contador; 004 JFrame frame; 005 Nave nave; 006 007 public Controlador(JFrame frame, Contador contador, Nave nave) 008 { 009 this.frame = frame; 010 this.contador = contador; 011 this.nave = nave; 012 } 013 014 public void run() 015 { 016 while (true) 017 { 018 Thread.yield(); 019 update(); 020 } 021 } 022 023 public void update() 024 { 025 ... 026 027public class Renderizador implements Runnable 028{ 029 BufferStrategy bs; 030 Contador contador; 031 JFrame frame; 032 Nave nave; 033 public boolean terminado = false; 034 035 public Renderizador(JFrame frame, BufferStrategy bs, Contador contador, Nave nave) 036 { 037 this.frame = frame; 038 this.bs = bs; 039 this.contador = contador; 040 this.nave = nave; 041 } 042 043 public void run() 044 { 045 while (!terminado) 046 { 047 Thread.yield(); 048 render(); 049 } 050 } 051 052 public void render() 053 { 054 ...
Modifiquei o código das classes Controlador e Renderizador para que implementem a interface Runnable do Java. Para isso, também declarei o método run() e dentro dele coloquei um laço. O Renderizador tem um laço que fica chamando render(). O Controlador tem um laço que fica chamando o update().
Agora a modificação no gameloop:
001public void gameloop() 002 { 003 initialize(); 004 Controlador c = new Controlador(this, contador, nave); 005 Renderizador r = new Renderizador(this, bs, contador, nave); 006 Thread t; 007 t = new Thread(c); 008 t.start(); 009 t = new Thread(r); 010 t.start(); 011 }
Aqui está o uso das threads, finalmente. Basta criar um objeto do tipo Thread informando para ele um objeto do tipo Runnable (o Renderizador, por exemplo). Depois chamamos o método start() da thread e ela vai chamar o método run() do objeto que lhe foi passado. O método run() será executado em paralelo ao resto do programa. Então, estamos colocando o laço do render() e o laço do update() para rodarem em paralelo.
Curioso para ver o resutado? Está abaixo:
Voilá! Um milhão e oitocentos mil pulsos e 459 frames. E usado 100% da CPU. Veja que agora os dois núcleos estão absolutamente carregados – a linha do gráfico está “cravada” no topo.
Mas porque deu valores menores do que quando rodamos cada um separadamente? Bom, é que a CPU ainda precisa atender os tais processos de fundo, S.O., antivirus, etc. Eles devem estar rodando no mesmo núcleo do update(), o que fez ele executar mais lentamente. Mas foi uma boa escolha do S.O., já que esse é o processo mais leve. Inteligentes estas máquinas modernas, não?
De qualquer forma, é um desempenho muito acima da primeira versão que eu mostrei.
Conclusão
Não usem threads.
Pelo menos não até chegar a um ponto onde não tenha mais o quê otimizar no código de vocês e ainda assim o jogo esteja lento. Uma minoria de jogos atuais faz uso intensivo de threads (menos de 20, pelo que sei), e todos especialistas concordam que não é fácil.
O que fiz no código acima, compartilhar um objeto (a nave) entre duas threads rodando paralelamente é perigoso. Nesse caso uma delas atualiza os dados e a outra apenas lê. Mas se duas threads atualizarem os mesmos dados ao mesmo tempo, as consequencias serão imprevisíveis. O Java facilita porque possui formas de sincronizar dados, impedindo sua leitura simultaneamente por duas threads. Mas não adianta sincronizar tudo, senão uma thread vai acabar esperando pela outra para poder acessar os dados e vão acabar rodando em sequencia, ao invés de em paralelo.
Por outro lado, o Java, por sí, já tira proveito de processadores de múltiplos núcleos. Existem outros processos que a máquina virtual faz, como coleta de lixo e otimização interativa do código. A JVM (Máquina Virtual Java) faz uso de threads para separar e rodar em paralelo estas operações, melhorando o desempenhdo do seu programa sem você ter que fazer nada.
Bom, fica aí um exemplo de uso de threads para quem não conhecia.
Baixe o código fonte deste artigo.
Arquivado em: Java, Programação, Técnico




Ótimo post Luiz, é o primeiro post que leio em seu blog, agora fiquei com uma dúvida, como ficaria o processamento do seu exemplo se você limitasse a quantidade de frames por segundo, por exemplo, pra 30fps?
Olá Jean. Se utilizarmos a técnica mostrada no outro post, para limitar a taxa de frames ou pulsos, continuamos com 100% de processamento. Isso ocorre porque mesmo tendo uma limitação na quantidade de vezes que update() ou render() é chamado dentro do loop, o loop em sí fican dando voltas o mais rápido possível. Para obter liberação de CPU, é preciso uma técnica mais elaborada, que meça o tempo que o render() (por exemplo) demorou e faça a thread “dormir” (Thread.sleep()) pelos milisegundos que sobraram. Assim o loop pára durante este tempo e o processamento é liberado. Para ter uma idéia, sem utilizar calculos, apenas fazendo cada thread (update e render) dormir por 30 milisegundos a cada volta do loop, tenho pulsos e frames em 30 por segundo e fico com menos de 5% de processamento. E a carga fica em apenas um dos núcleos da CPU.