Rodrigo Kumpera Weblog

Meus achados sobre tecnologia

A relação entre I/O não bloqueante e continuations

November 7th, 2006 · 2 Comments

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!

Tags: Programming · Scalability · java

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