Kumpera.net delírios sobre linguagens de programação

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