You are on page 1of 12

Threads em Java Miguel Jonathan- DCC/IM e NCE/UFRJ

atualizado em 16/11/2011

Conceitos: Diferena entre processo e thread: Um processo um programa em execuo que possui o seu prprio espao de endereamento. Um sistema operacional multitarefa capaz de controlar a execuo de mais de um processo ao mesmo tempo, de forma concorrente, fazendo a CPU alternar a execuo das instrues de cada processo. Uma thread um fluxo sequencial de controle, ou linha de execuo, dentro de um processo ou programa. Um processo pode assim ter diversas threads executando concorrentemente, mas todas partilhando o mesmo espao de endereamento. Como no h necessidade de trocar de contexto, as threads representam uma forma mais leve de processamento concorrente. A Mquina Virtual Java (JVM) permite que uma aplicao tenha diversas linhas de execuo rodando concorrentemente. H sempre pelo menos uma thread, que roda o mtodo main(). Essa thread criada automaticamente, e outras podem ser criadas pelo programador. O programa gc (garbage collector) roda em outra thread tambm iniciada pela JVM. Porque usar threads em aplicaes: Threads tm vrias utilidades, especialmente quando o uso de um recurso do programa pode demandar muito tempo, e travar outras tarefas que esto aguardando, ou quando se precisa ficar verificando a ocorrncia de algum evento para efetuar alguma ao. Nesse ltimo caso, uma thread pode ficar dedicada apenas para verificar periodicamente a ocorrncia do evento. A aplicao mais usual a interao com as interfaces grficas (menus, botes) quando se quer obter resposta quase que imediata a uma ao. A classe java.lang.Thread: Em Java, cada thread implementada como um objeto, que deve ser uma instncia de uma subclasse de java.lang.Thread criada para esse fim. A interface java.lang.Runnable Essa interface obriga a ter o mtodo public void run(). Toda thread criada para executar os comandos de algum mtodo com essa assinatura. A prpria classe Thread possui esse mtodo, mas vazio, ou seja, no faz nada. Como criar uma thread: H duas formas bsicas para se criar uma thread: a) Criar uma subclasse explcita de java.lang.Thread, e redefinir o mtodo run()com os comandos que a thread dever executar. Ex: class MinhaThread extends Thread{ public void run(){ <instrues da thread> } } A partir da possvel criar quantas instncias se desejar dessa classe. Cada vez que enviar a mensagem start() para uma instncia, uma nova linha de execuo ser iniciada com os comandos do mtodo run(), que rodar em paralelo com as outras threads: MinhaThread t = new MinhaThread(); t.start(); ou, simplesmente: new MinhaThread().start();

Note que no se pode enviar a mensagem run() diretamente para um objeto Thread. Envia-se a mensagem start(), que criar a thread onde rodaro os comandos do mtodo run(). Obs: Threads podem ser criadas sem referncia explcita, como na forma mais simples acima ( ex: new MinhaThread().start();) , sem risco de serem removidas pelo Garbage Collector, pois possuem uma referncia interna que permanece at o mtodo run() associado terminar.

b) A segunda forma de criar thread recomendada em situaes onde queremos executar um


mtodo de uma classe qualquer em uma thread separada, sem que essa classe herde de Thread. Isso necessrio no caso da classe j ser uma subclasse de outra, pois no poderia herdar tambm de Thread. Nesse caso, fazemos a classe em questo implementar a interface Runnable e colocamos no mtodo public void run() os comandos que sero executados na thread. No exemplo abaixo, um mtodo run() da classe X ser colocado em uma thread: class X implements Runnable{ public void run(){ <instrues da thread> } ..... outros mtodos de X, construtores, atributos, etc } A seguir, em outro mtodo, criamos uma instncia de X, e passamos a instncia como argumento do construtor da classe Thread: X x = new X(...); Thread t = new Thread(x); O efeito disso a criao de uma instncia de Thread que executar o mtodo run() definido na classe do argumento do construtor. Enviando a seguir a mensagem start() para a thread far iniciar uma nova linha de execuo com os comandos do mtodo run() da classe X, que rodar em paralelo com as outras threads. t.start(); Nome da thread: Toda thread tem um nome (uma string) para poder ser identificada. O nome pode ser passado como um parmetro do construtor, e pode ser recuperado enviando para a thread a mensagem getName(). Se um nome no for especificado no construtor, um novo nome ser gerado automaticamente para a thread. Como incluir o nome na chamada do construtor: //subclasse explicita de Thread: Thread t1 = new MinhaThread("Thread 1"); // usando instancia de classe X que implementa Runnable: Thread t2 = new Thread(new X(), "Thread 2"); Prioridade: Cada thread possui uma prioridade. Threads com prioridades mais elevadas so executadas preferencialmente antes de threads com menor prioridade. A prioridade default 5 (em uma escala de 1 a 10), mas pode ser alterada enviando a mensagem setPriority(int) para a thread. (Nota: na classe Thread existem as constantes static MAX_PRIORITY, MIN_PRIORITY e NORM_PRIORITY, com valores 10, 0 e 5, mas que podem variar dependendo da implementao).

Quando existem diversas threads de mesma prioridade sendo executadas, Java aloca um tempo mximo para processar os comandos de cada uma, aps o que interrompe a thread em execuo e passa a processar os comandos da seguinte. O mtodo de classe Thread.currentThread(): Esse mtodo permite acessar a thread que est executando no momento em que chamado. usado dentro do mtodo run() para permitir identificar e/ou alterar os parmetros da thread (lembre-se que o run() pode ser de outra classe). Por exemplo, para imprimir o nome da thread onde esse comando est executando: System.out.println(Thread.currentThread().getName()); Fazendo uma thread dormir por um certo tempo: O mtodo de classe Thread.sleep(long milisseg) permite interromper a execuo da thread pelo tempo dado no argumento. Deve ser usado dentro de um bloco try porque, caso a thread receba uma mensagem interrupt() enquanto estiver parada por um sleep(), ser lanada uma InterruptedException. Cedendo o tempo de processamento para outras threads: Uma thread pode ceder o restante do tempo de processamento alocado a ela, executando o comando yield(). O controle passa para a prxima thread que estiver aguardando. Evita desperdiar tempo de processamento. Exemplos: Os exemplos abaixo simulam uma corrida entre dois times, Flamengo e Botafogo. Nestes dois exemplos, duas threads escrevem em paralelo na console os nmeros de 1 a 99, seguido do nome do time. 1. No primeiro exemplo, uma subclasse de Thread definida, onde o seu mtodo run() simula a corrida. Duas instncias dessa classe so criadas para representar os dois jogadores. Cada thread recebe o nome de um time (Flamengo e Botafogo) passado pelo construtor. Para tornar o resultado aleatrio, e possibilitar uma melhor observao dos resultados, cada thread dorme uma quantidade aleatria de milisegundos entre 0 e 299 antes de imprimir a prxima linha. Para isso usado o mtodo esttico sleep(long miliseg) da classe Thread. Antes de terminar, cada thread imprime a palavra TERMINOU, seguido do seu nome. O mtodo getName() da classe Thread retorna a string com o nome.
public class ThreadSimples extends Thread { public ThreadSimples(String str) { super(str); } public void run() { for (int i = 0; i < 100; i++) { System.out.println(i + " " + getName()); try { sleep((long)(Math.random() * 300)); } catch (InterruptedException e) {} } System.out.println("TERMINOU " + getName()+"!"); } } public class TesteDuasThreads { public static void main (String[] args) { ThreadSimples bota = new ThreadSimples("BOTAFOGO"); bota.setPriority(4); ThreadSimples mengo =new ThreadSimples("FLAMENGO"); mengo.setPriority(6); mengo.start();

bota.start(); System.out.println("Main terminado!"); } }

2.

O segundo exemplo essencialmente o mesmo que o primeiro, s que nesse caso no se cria nenhuma subclasse de Thread. Em vez disso uma classe Time definida, que implementa a interface Runnable. O mtodo run() de Time contm os mesmos comandos que o mtodo run() da subclasse de Thread do primeiro exemplo. Nesse caso, a prioridade de cada thread passada pelo construtor do time.
public class Time implements Runnable { String nome; int priority; public Time(String nome, int priority) { this.nome=nome; this.priority=priority; } public void run() { Thread.currentThread().setPriority(priority); for (int i = 0; i < 100; i++) { System.out.println(i + "R " + nome); try { Thread.sleep((long)(Math.random() * 300)); } catch (InterruptedException e) {} } System.out.println("TERMINOU " + nome+"!"); }

}
public class TesteDuasThreads { public static void main (String[] args) { Time bota = new Time("BOTAFOGO",1); Time mengo =new Time("FLAMENGO",7); new Thread(mengo).start(); new Thread(bota).start(); System.out.println("Main terminado!"); } }

Interrupo de uma Thread Toda thread tem uma varivel interna, um flag binrio, que contm o seu "estado de interrupo" (interrupt status). Esse flag fica "setado", ou seja, valendo 1, se a thread recebeu uma solicitao para ser interrompida, por meio da mensagem interrupt(). Essa mensagem no para a thread automaticamente. A prpria thread deve avaliar o que fazer e como se terminar. O caso tpico a thread fazer alguma finalizao e executar return, para se terminar. Caso a thread esteja bloqueada temporariamente por ter executado um sleep(), join() ou wait(), ela receber tambm uma InterruptedException ao receber a mensagem interrupt(). Nesse caso, ser obrigatrio haver blocos try-catch para essa exceo verificada, e o bloco catch poder fazer o tratamento da interrupo. caso do primeiro exemplo mostrado a seguir. No caso da thread no incluir comandos que podem lanar InterruptedException, a thread pode checar se o seu status de interrupo foi "setado" por meio de uma chamada ao mtodo static boolean interrupted()da classe Thread. Esse mtodo de classe verifica o status da

thread que est executando (a current thread), retornando true se o status for 1, e false em caso contrrio. A execuo desse mtodo tambm resseta automaticamente o status de interrupo para zero. Exemplos: As classes abaixo mostram o uso de interrupo nos dois casos. Uma thread separada da thread main() fica imprimindo indefinidamente na console os mltiplos de um valor raiz. A impresso deve ser interrompida quando a tecla ENTER do teclado for acionada dentro do mtodo main. As classes devem ser usadas aos pares. O mtodo main() dispara a thread e fica aguardando a entrada do teclado, com o comando br.readLine(). Em seguida, envia para a thread a mensagem interrupt(). No primeiro caso, a thread usa o mtodo Thread.sleep(1) para "dormir" 1 milissegundo entre cada impresso. O bloco catch pega a interrupo e termina a thread com return. No segundo caso, no usado o sleep(), e por isso no existem blocos try-catch para detectar a interrupo. O mtodo Thread.interrupted() usado para checar, a cada volta do lao de impresso, se o status de interrupo mudou para 1. a) Interrupo de thread por meio de InterruptedException (thread tem sleep()):
public class ImprimeThreadComSleep extends Thread{ private int raiz; public ImprimeThreadComSleep(int raiz){ this.raiz = raiz; } public void run(){ int i=1; int n=raiz; while(true){ System.out.println(n= raiz*i); i++; try{ sleep(1); } catch(InterruptedException e){ System.out.println ("Thread interrompida enquanto dormia!"); return; // termina a thread } } } } import java.io.*; public class InterrompeThread1 { public static void main(String[]args){ ImprimeThreadComSleep t; if (args.length < 1){ System.out.println("Este programa necessita de um argumento inteiro, a raiz"); return; } else { try{ int raiz = Integer.parseInt(args[0]); t = new ImprimeThreadComSleep(raiz); t.start(); }catch(NumberFormatException nf){ System.out.println("Este programa necessita de um argumento inteiro"); return; }

} BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try{ br.readLine(); // aguarda digitar ENTER } catch (IOException e) {} t.interrupt(); } // main termina }

b) Interrupo de thread por meio de Thread.interrupted() (thread sem sleep()):


public class ImprimeThreadSemSleep extends Thread { private int raiz; public ImprimeThreadSemSleep(int raiz) { this.raiz = raiz; } public void run() { int i = 1, n = raiz; while (true) { System.out.println(n = raiz * i); i++; if (Thread.interrupted()) { System.out.println("Thread interrompida sem estar dormindo!"); return; } } } } import java.io.*; public class InterrompeThread2 { public static void main(String[]args){ ImprimeThreadSemSleep t; if (args.length < 1){ System.out.println("Este programa necessita de um argumento inteiro, a raiz"); return; } else { try{ int raiz = Integer.parseInt(args[0]); t = new ImprimeThreadSemSleep(raiz); t.start(); } catch(NumberFormatException nf){ System.out.println("Este programa necessita de um argumento inteiro"); return; } } BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try{ br.readLine(); // aguarda digitar ENTER } catch (IOException e) {} t.interrupt(); }// main termina }

Uso Concorrente de Recursos Consiste na tentativa de uso simultneo de um recurso por mais de um processo: ex: dois carros tentando estacionar na mesma vaga, duas pessoas tentando passar pela mesma porta, dois processos tentando imprimir na mesma impressora, duas threads tentando acessar uma mesma rea da memria. importante evitar essas "colises". preciso ento garantir que um recurso ficar disponvel para acesso por apenas uma thread de cada vez. Isso

conseguido em Java marcando, com uma sinalizao de "tranca" ou "lock", a parte de cdigo em que a thread faz uso de um recurso partilhado com outras threads. O atendimento s threads que disputam um mesmo recurso no feito exatamente na forma de uma fila de espera, pois o escalonador da JVM no muito determinstico, embora possa seguir sugestes dadas por comandos como yield() e setPriority(). Java usa o mecanismo de semforos. Semforo (ou monitor, lock/tranca) e a palavra-chave synchronized. Um semforo um sinalizador associado a um recurso que monitorado para ser usado por uma nica thread de cada vez. O recurso sempre associado a um objeto. Todo objeto em Java contm uma nica "tranca", que partilhada por todos os mtodos sincronizados desse objeto.. Se o valor do semforo for zero, o objeto est disponvel. Em Java, esse mecanismo implementado com o uso da palavra-chave synchronized para prefixar um mtodo que deve rodar em uma thread com uso exclusivo do objeto que ativou o mtodo. Uma thread, ao entrar em um mtodo sincronizado de um objeto, toma posse e incrementa o semforo do objeto, impedindo outras threads de entrarem em outros mtodos sincronizados da mesma classe, at a thread que detm a tranca terminar de executar o mtodo. Ao sair de um mtodo sincronizado, a thread decrementa o semforo do objeto, que ficar liberado para uso por outras threads sincronizadas, quando o valor do semforo voltar a zero (uma mesma thread, aps adquirir o semforo de um objeto, pode entrar em vrios mtodos sincronizados do mesmo objeto, e cada vez que isso ocorre o semforo ser incrementado mais uma vez). Quando uma thread executa um mtodo sincronizado m(), que foi ativado a partir de um determinado objeto x, como em x.m(), o acesso ao objeto x fica bloqueado a todas as demais threads que tentarem executar esse ou outros mtodos sincronizados da classe de x, at que o mtodo m() termine e libere a tranca. Podemos tambm restringir o acesso a apenas um bloco de cdigo dentro de um mtodo, prefixando o bloco com a palavra synchronized, com um objeto entre parnteses: Ex: synchronized(x) { <bloco de cdigo> } Essa construo obriga a que a tranca do objeto x deva ser adquirida por uma thread para executar o bloco, e enquanto a tranca no for liberada por essa thread, nenhuma outra poder executar esse bloco para esse objeto. Trancas de Classes: Classes tambm so objetos, e possuem trancas. A tranca de uma classe se refere aos seus mtodos estticos sincronizados (synchronized static), e quando uma thread executa um mtodo esttico sincronizado, o acesso s variveis de classe fica bloqueado para outras threads. Liberando a tranca do objeto com o wait(): De dentro de um mtodo ou bloco sincronizado, o envio da mensagem wait()ao objeto que ativou o mtodo (this) suspende a execuo da thread, e libera a tranca desse objeto. A chamada para wait() pode no ter argumento (forma mais usada, espera indeterminada) ou ter um valor de tempo de espera determinado em milissegundos. A execuo do mtodo ficar suspensa (e a thread entra no estado bloqueado) at que o tempo se esgote, ou at que uma outra thread dispare um mtodo notify() ou notifyAll(). Nesse momento, a thread

em espera acorda e retoma ao estado "runnable", passando a poder receber tempo de processamento. Tipicamente, essa thread aguarda que uma condio externa mude (em geral por conta da ao de outras threads). Exemplo de uso de threads com sincronismo (Adaptado de um exemplo de Paulo Csar M. Jeveaux em www.portaljava.com.br) Nesse exemplo, um banco tem 10 contas. Cada conta criada com um valor inicial de 10.000 reais. So ento criadas 10 threads, uma para cada conta, e as 10 threads passam a executar seus mtodos run() em paralelo, com igual prioridade. Cada thread fica continuamente transferindo uma quantia aleatria (menor que 5000) para uma outra das 9 contas, escolhida tambm aleatoriamente. Como no haver depsitos ou saques externos, o total do dinheiro no banco (a soma dos saldos das 10 contas) deveria ficar constante em 100.000 reais. Para fazer uma transferncia, cada thread executa duas operaes em sequncia, em um mtodo transfere(): a) tenta debitar da sua conta (se houver saldo) uma quantia de dinheiro escolhida aleatoriamente, at 5000 reais; e b) aps conseguir debitar, credita a mesma quantia em outra conta escolhida tambm aleatoriamente. As duas aes conjuntas, dbito de uma conta, e crdito na outra conta, formam o que se chama uma transao atmica, no sentido de que deve ser indivisvel. Ou seja, ela no deve ser interrompida no meio, antes que outras aes manipulem (leiam ou alterem) os valores das contas, ou resultados estranhos podem ocorrer, como veremos a seguir. Cada thread entra num lao em que testa se a conta tem saldo suficiente para debitar a quantia a transferir e, no tendo, dorme por 5 milisegundos antes de testar novamente. A ideia que, em algum momento, depsitos tero sido feitos aleatoriamente que levem o saldo a ser suficiente para realizar a transferncia. Esse exemplo tem duas verses: 1) Na primeira verso, sem sincronizao, no h garantia que o mtodo transfere() completar as duas transferncias antes que outra thread possa acessar a rea de memria das contas do banco: Nesta verso, as 10 threads competem por ciclos de CPU sem qualquer restrio para acessar os valores das contas. Ocorre que a mquina virtual Java (JVM) pode a qualquer momento interromper a execuo de uma thread e passar o controle para outra, estando essa execuo em qualquer instruo (basta ter terminado a execuo de um bytecode). Nessa verso, pode ocorrer que uma ou mais transaes no tenham terminado (digamos, s foi feito o dbito, mas ainda no foi feito o crdito subsequente) antes de passar o controle para outra thread que calcula a soma dos valores das contas. Evidentemente que, nesse caso, a soma poder dar um resultado errado, pois deveria ser feita somente depois que todos os crditos forem realizados. 2) Na segunda verso, foramos que o mtodo transfere() da classe Banco tenha que ser realizado completamente (de forma atmica) por uma thread, antes de permitir que outra thread possa assumir o controle do recurso manipulado, no caso o vetor das contas. Isso feito prefixando o mtodo com a palavra-chave synchronized. Durante a execuo de um

mtodo sincronizado, a partir da chamada por um objeto, como em b.transfere(), a thread passa a possuir o monitor (a tranca, ou lock) do objeto que ativou o mtodo, no caso o banco b. Isso significa que somente essa thread pode acessar variveis de instncia ou de classe desse objeto, at que termine, ou libere voluntariamente o monitor com uma chamada de wait(). Dentro de um mtodo sincronizado, caso ocorra a necessidade de aguardar algum tempo para iniciar a transao atmica, e para no manter as demais threads esperando, possvel enviar o comando wait() para o objeto que ativou esse mtodo. O efeito bloquear essa thread e liberar o monitor ( i.e., a tranca) para que o escalonador de tarefas escolha outra thread para assumi-lo. A thread ficar bloqueada at que o objeto receba a mensagem notify() ou notifyAll(), que desbloqueia as threads em estado de bloqueio por wait. A diferena entre os dois casos que notify() s desbloqueia uma das threads que estava em wait, que escolhida arbitrariamente, enquanto que notifyAll() desbloqueia todas. Depois de desbloqueada, a thread passa a competir com as demais que esto ativas pela posse do monitor, e s depois de receb-la que voltar a executar.

Observaes: a) Os mtodos wait(), notify(), e notifyAll() s podem ser chamados de dentro de um bloco sincronizado. Se isso no for feito, o compilador no reclama, mas em tempo de execuo ocorrer uma exceo do tipo IllegalMonitorStateException. b) As chamadas de wait() devem ser feitas sempre dentro de um lao que testa a condio para permanecer em wait, e devem estar dentro de um bloco try-catch para capturar uma InterruptedException. Essa exceo pode ocorrer se a thread em questo receber uma mensagem interrupt() enquanto estiver em wait, aguardando um notify(). c) Os mtodos wait(), notify()e notifyAll() so definidos na classe Object. Ou seja, todo e qualquer objeto pode ser sincronizado e ter um monitor associado, e receber essas mensagens.

Implementao do exemplo: Primeiro caso: sem usar sincronizao: public class Banco { public static final int VALOR_TOTAL = 10000; // total de cada conta public static final int NUM_CONTAS = 10; // nmero de contas no banco private long conta[]; // array que armazena os valores de cada conta private int transferencias; // nmero de transferncias bancrias public Banco() { conta = new long[NUM_CONTAS]; for(int i = 0; i < NUM_CONTAS; i++) { conta[i] = VALOR_TOTAL; } transferencias = 0; teste(); } public void transfere(int de, int para, int quantia) { while(conta[de] < quantia) { try { Thread.sleep(5); }catch(InterruptedException e) {}

} conta[de] -= quantia; try { Thread.sleep(1); // para aumentar a chance da thread parar no meio }catch(InterruptedException e) {} conta[para] += quantia; transferencias++; if(transferencias % 5000 == 0) } public void teste() { long soma = 0; for(int i = 0; i < NUM_CONTAS; i++) soma = soma + conta[i]; System.out.println("No.de transaes: " + transferencias + " Soma: " + soma); } } // fim da classe Banco public class BancoSemSincronismo { public static void main(String[] args) { Banco b = new Banco(); for(int i=0; i < Banco.NUM_CONTAS; i++) { new Transferencia(b, i).start(); } } } public class Transferencia extends Thread { private Banco b; private int de; public Transferencia(Banco b, int de) { this.b = b; this.de = de; } public void run() { int para; while(true) { do{ para = (int)(Banco.NUM_CONTAS * Math.random()); } while(para==de); int quantia = 1+(int)(Banco.VALOR_TOTAL * Math.random()) /2; b.transfere(de, para, quantia); try { sleep(1); } catch(InterruptedException e) {} } }

teste();

Segundo caso: Usando sincronizao:

class Banco{ public static final int VALOR_TOTAL = 10000; //Total de cada conta public static final int NUM_CONTAS = 10; //Nmero de contas no banco private long conta[]; //Array que armazena o valor das contas private int transferencias; //indica o nmero de transferncias bancrias public Banco() { conta = new long[NUM_CONTAS]; for(int i = 0; i < NUM_CONTAS; i++) { conta[i] = VALOR_TOTAL; } transferencias = 0; teste(); } public synchronized void transfere(int de, int para, int quantia) { while(conta[de] < quantia) { try { wait(); }catch(InterruptedException e) {} } conta[de] -= quantia; try { Thread.sleep(1); // para aumentar a chance da thread parar no meio }catch(InterruptedException e) {} conta[para] += quantia; transferencias++; if(transferencias % 3000 == 0) notify(); } public void teste() { long soma = 0; for(int i = 0; i < NUM_CONTAS; i++) soma += conta[i]; System.out.println("Transaes: " + transferencias + " } } // fim da classe Banco public class BancoComSincronismo { public static void main(String[] args) { Banco b = new Banco(); for(int i=0; i < Banco.NUM_CONTAS; i++) { new Transferencia(b, i).start(); } } } public class Transferencia extends Thread { private Banco b; private int de; public Transferencia(Banco b, int de) { this.b = b;

teste();

Soma: " + soma);

this.de = de; } public void run() { int para; while(true) { do{ para = (int)(Banco.NUM_CONTAS * Math.random()); }while(para==de); int quantia = 1+(int)(Banco.VALOR_TOTAL * Math.random()) /2; b.transfere(de, para, quantia); try { sleep(1); } catch(InterruptedException e) {} } }

Referncias: Bruce Eckel, Thinking in Java, 3rd Edition, disponvel em: http://www.ibiblio.org/pub/docs/books/eckel/TIJ-3rd-edition4.0.zip Sun Java Tutorial parte dedicada a Threads e processos concorrentes: Concurrency: http://java.sun.com/docs/books/tutorial/essential/concurrency/index.html

You might also like