No meu artigo anterior sobre NIO e continuations em java
o Willian Mitsuda e o Felipe ficaram em dúvida da utilidade de utilizar continuations. A principal vantagem dessa técnica é poder programar utilizando multiplexação e programação seqüêncial juntas.
Vou dar dois exemplos de como ler linhas usando i/o bloqueante e não bloqueante, deles vou discutir quais os pontos fracos e fortes, por fim vou mostrar como é possivel ter um modelo superior a ambos utilizando Continuations.
//1 I/O bloqueante: void readLines(Socket sock) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream())); String str; while ((str = br.readLine()) != null) { System.out.println(str); } } //2 I/O não bloqueante: //faz o setup do channel, registra ele para leitura //quando o selector retornar o channel em questão, invocamos o método read de ChannelState. void readLinesSetup(SocketChannel channel, Selector sel) throws IOException { channel.configureBlocking(false); channel.register(sel, SelectionKey.OP_READ, new ChannelState()); } class ChannelState { static final byte CR = 13; static final byte LF = 10; boolean skipLF; StringBuffer builder = new StringBuffer(); CharsetDecoder decoder = Charset.defaultCharset().newDecoder(); ByteBuffer buffer = ByteBuffer.allocate(1000); CharBuffer cbuff = CharBuffer.allocate(1000); //retorna true se deve continuar lendo boolean read(SocketChannel channel) throws IOException { CoderResult cr = null; int res; do { res = channel.read(buffer); buffer.flip(); do { cbuff.clear(); cr = decoder.decode(buffer, cbuff, res == -1); cbuff.flip(); while (cbuff.remaining() > 0) { char current = cbuff.get(); switch (current) { case CR: dumpLine(); skipLF = true; break; case LF: if (!skipLF) dumpLine(); skipLF = false; break; default: builder.append(current); } } } while (cr != CoderResult.UNDERFLOW); //prepara o buffer para mais leituras buffer.compact().position(buffer.limit()).limit(buffer.capacity()); } while (res > 0); if(res == -1 && builder.length() > 0) System.out.println(builder.toString()); return res != -1; } private void dumpLine() { System.out.println(builder.toString()); builder = new StringBuffer(); } }
Bom, como vocês podem notar, usando I/O não bloqueante da muito mais trabalho, já que precisamos lidar com a situação de apenas uma parte dos dados estar disponível. Vejam ainda que eu coloquei um exemplo super simples, usei StringBuffer em vez que acumular as instâncias de CharBuffer e não coloquei a lógica de reciclar buffer.
Vamos analisar o primeiro exemplo, ele exige que uma thread seja alocada para cada conexão, então acaba por limitar a escalabilidade do sistema, pois dificilmente vai aguentar mais que 500 sockets simultâneos antes do sistema operacional começar a entrar em parafuso. A vantagem fica por conta do código ser super simples e facil de testar.
Quanto ao segundo, podemos ver a lógica convoluta para lidar com a conversão para texto e como ter de suportar leituras parciais é dificil. No geral, todo sistema que utiliza NIO para ler dados acaba implementando uma máquina de estados para isso, esforço enorme, que é muito mais dificil de testar. Eu precisei de uma máquina de pilha e 19 estados para implementar o parsing apenas do header de requests HTTP. A complexidade se paga quando vemos a escalabilidade possivel quando usamos multiplexação, é perfeitamente possivel suportar dezenas de milhares de sockets desta forma.
Comparando as duas soluções vemos que I/O bloqueante é facil de desenvolver, mas não escala; enquanto I/O não bloqueante com multiplexação é extremamente escalavel, porém muito dificil de programar. O ideal seria conseguir combinar as duas de forma a ter um pouco das vantagens de cada modelo. Para nossa sorte, é possivel, basta usar continuations.
Usando continuations não deixamos de utilizar I/O não bloqueante, porêm nos é permitido esconder de uma maneira clara os problemas relacionados a leituras parciais: suspendemos a continuation até que os dados estejam disponíveis, de forma que o usuário possa programar assim como no exemplo 1, mas garantido uma maior escalabilidade.
Alguém pode me questionar se o custo de dar suspend/resume de uma continuation não é semelhante ao de um thread-switch e se o consumo de memória não é alto. O custo do resume é pequeno, pois não precisamos fazer toda parafernália que um SO deve, depende apenas do suporte da linguagem; quanto ao consumo de memória, uma continuation não exige que sejam criadas várias estruturas com memória do kernel (recurso super limitado) e nem alocada uma pilha enorme - por padrão a JVM aloca 256k de memória para a pilha de cada thread.
Espero conseguir com esse artigo expor uma das muitas possibilidades que continuations nos permitem. Servidores muito mais escalaveis são um exemplo, outro muito interessante é o SeaSide, um framework web escrito em SmallTalk que as usa para gerenciar o estado do usuário.
Update: O Felipe apontou um erro no texto, está corrigido. Valeu Felipe!
2 responses so far ↓
1 Rafael // Nov 8, 2006 at 12:37 am
“uma continuation não existe que sejam criadas várias estruturas com memória do kernel ” não deveria ser “uma continuation não exiGe que sejam criadas várias estruturas com memória do kernel “?
(ótimo artigo, BTW)
2 felipe // Nov 8, 2006 at 10:13 am
Rodrigo,
essa continuação do post esclareceu bem minhas duvidas, obrigado!
Acho que seria legal também incentivar o pessoal estudar o i/o nao bloqueante “puro” pra depois pensar em como usar continuations, se não o pessoal acaba pulando uma etapa(e no geral é o que muita gente gosta de fazer)
Em http://developers.sun.com/learning/javaoneonline/2006/coreplatform/
tem uma apresentação “How to build Scalable Multiplexed Server with NIO” do javaone de 2006 com audio e legendas.
Leave a Comment