Ruby Inline vs Block vs Proc

14 Setembro, 2009

Algo que estranhei bastante ao começar a programar em Ruby foi sem duvida os Blocos e os Procs, para alguém que tinha Java como linguagem preferida foi algo uma tanto “absurdo”.

Pensar mas para que raio é isto!?!? Se tenho um objecto porque não chamo simplesmente os seus métodos e pronto!?!?

Depois de algum tempo comecei a adaptar-me ao uso dos blocos a nível de código realmente consegue simplificar muita a vida do programar e tornar as muitos casos as coisas bem mais simples.

Então ai surgiu outra duvida! Será que o uso de blocos e procs trás melhores ou piores níveis de performance face ao simples código inline ?

Depois de alguma pesquisa encontrei o seguinte artigo http://www.pluralsight.com/community/blogs/dbox/archive/2006/05/09/23068.aspx  onde era discutido exactamente essa questão.

O autor questiona-se o porque da adoração dos programadores de Ruby pelos blocos em relação ao código inline, para tentar chegar a algumas conclusões ele cria o seguinte código.

require “benchmark”

IterationCount = 1000000
$x = 0

def UseBlock
yield
end

def UseProc(&p)
p.call
end

def InlineStatic()
i = 0
while (i < IterationCount)
$x = $x + 1
i = i + 1
end
end

def InlineLocal()
i = 0
y = 0
while (i < IterationCount)
y = y + 1
i = i + 1
end
end

def BlockStatic()
i = 0
while (i < IterationCount)
UseBlock do $x = $x + 1 end
i = i + 1
end
end

def BlockLocal()
i = 0
y = 0
while (i < IterationCount)
UseBlock do y = y + 1 end
i = i + 1
end
end

def ProcStatic()
i = 0
while (i < IterationCount)
UseProc do $x = $x + 1 end
i = i + 1
end
end

def ProcLocal()
i = 0
y = 0
while (i < IterationCount)
UseProc do y = y + 1 end
i = i + 1
end
end

Benchmark.bm { |r|
r.report { InlineStatic() }
r.report { InlineLocal() }
r.report { BlockStatic() }
r.report { BlockLocal() }
r.report { ProcStatic() }
r.report { ProcLocal() }
}

Ao executar este código no Ruby 1.8.7 obtive os seguintes resultados


user     system   total     real
0.570000 0.000000 0.570000 ( 0.584075)
0.500000 0.000000 0.500000 ( 0.510654)
1.320000 0.330000 1.650000 ( 1.662194)
1.280000 0.270000 1.550000 ( 1.555471)
3.240000 0.350000 3.590000 ( 3.623273)
3.100000 0.390000 3.490000 ( 3.507118)

Como podemos ver os resultados difererem como podemos ver o codigo inline é o com melhor performance, seguido pelos blocos com aproximadamente 3 vezes menos performance e por fim os procs com 7 vezes menos performance que o inline.

Claramente os Procs oferencem uma menor performance face ao inline e aos blocos. Em relação ao inline vs blocos é algo discutivél, cabe ao programador avaliar se a diferença é significativa para o objectivo em questão, tambem há que ter em consideração que se trata apenas de uma exemplo com contagem de 0 a 1000000, ao usar outras sequencias de codigo é possivel obter outros resultados embora os procs vejam sempre a estar em ultimo lugar dado que o uso do yield nos blocos cria um proc apenas parcial e não total como no caso do proc.

A titulo de curiosidade resolvi executar o mesmo codigo no Ruby 1.9.0 e os resultados embora tenham mantido a mesma sequencia no que toca a performance, tiveram valores um tanto diferentes…


user system total real
0.080000 0.000000 0.080000 ( 0.078453)
0.060000 0.000000 0.060000 ( 0.056817)
0.190000 0.000000 0.190000 ( 0.187863)
0.160000 0.000000 0.160000 ( 0.169452)
0.580000 0.000000 0.580000 ( 0.580614)
0.570000 0.000000 0.570000 ( 0.567870)

Os valores são claramente muito inferiores aos do Ruby 1.8 é sem duvida uma grande melhoria no que toca á performance do Ruby!

E tambem uma aproximação da performance entre o código inline e os blocos face aos blocos e proc que ficam ainda mais distanciados do que anteriormente.

Uma Thread é uma forma de dividir um processo em diversas tarefas que podem ser executadas simultaneamente. O suporte de Thread é dado pelo sistema operativo, conhecido por Kernel-Level Thread ou implementada por uma biblioteca de uma determinada linguagem, sendo neste caso User-Level Thread.
Uma Thread permite que o utilizador do programa utilize determinada funcionalidade do ambiente enquanto outras Threads processão outras operações.
Em sistemas com um único CPU, cada Thread é processada aparentemente em simultâneo, a mudança entre cada Thread é feita de forma muito rápida dando a ideia de que o processamento é paralelo. Em sistemas com múltiplos CPUs ou Multi-Core as Threads podem ser executadas realmente de forma paralela.

Os exemplos apresentados iram utilizar a API Thread da linguagem Ruby, esta API não utiliza Threads nativas ou seja Kernel-Level Thread, mas sim User-Level Thread, desta forma é sacrificada a eficiência oferecida pelas Threads nativas ao Sistema Operativo mas ganha em portabilidade, dados que as Threads escritas em um Sistema Operativo irá funcionar em qualquer outra que suporte a linguagem Ruby.
Versões futuras como Ruby 2.0 poderão vir a suportar Threads nativas dando ao programador mais possibilidades de desenvolvimento.

Criar e Executar Threads

Em Ruby as Threads podem ser criadas como qualquer outro objecto, utilizando o método new, ao chamar este método devemos passar à Thread um bloco com o código a ser executado por ela. Vejamos uma exemplo muito simples da utilização de Threads em Ruby.

palavras = ["Um","Dois","Tres","Quatro"]

 
numeros = [1,2,3,4]
 
puts "Sem Threads...."

 
palavras.each { |palavra| puts(palavra) }
 
numeros.each { |numero| puts(numero) }

 
 
puts "Com Threads...."
 
Thread.new {
  palavras.each { |palavra| puts(palavra + " - ") }

}
 
Thread.new{
  numeros.each { |numero| puts(numero) }

}
 
sleep(5)

output

Sem Threads....
Um
Dois
Tres
Quatro
1
2
3
4
Com Threads....
Um - 1
Dois - 2
Tres - 3
Quatro – 4

Como podemos ver a diferença é bem visível, com apenas a Thread principal primeiro são processadas as palavras e em seguida os números, por sua vez com varias Threads em simultâneo, podemos ver que tanto as palavras como os números são processados em simultâneo, embora não seja um simultâneo real como vimos anteriormente. O comando sleep(5) no final faz com que o nosso programa espere 5 segundos, este comando é utilizado para que seja possível ver os resultados do programa com Threads. Mais à frente veremos como resolver este pequeno problema sem ter de recorrer a delays.

A Main Thread

Mesmo que não criemos Threads explicitamente, existe sempre pelo menos uma Thread em execução, esta Thread é chamada de Main Thread é nesta Thread que o programa está a correr. Podemos verificar isso através do seguinte código.

p(Thread.main)

output

#<Thread:0xb7ddc1bc run>

É retornado o ID em hexadecimal e o seu estado “run”, ou seja a informação sobre a Thread principal que arranca quando um interpretador Ruby começa a execução.

Estados de uma Thread

Cada Thread pode encontrar-se em um dos seguintes estados

* run           - A Thread está a ser executada.
* sleep         - A Thread está em espera ou sleep.
* aborting      - A Thread está a ser abortada.
* false         - A Thread terminou normalmente.
* nil           - A Thread terminou com uma excepção.

Podemos obter o estado de uma Thread usando o método status, ao pedido do estado de uma Thread podemos ainda obter “dead” no caso dessa Thread já não existir.
Vejamos um simples exemplo que permite ilustrar todos os estados da Thread.

puts(Thread.main.inspect)

 
puts(Thread.new{ sleep }.kill.inspect)

 
puts(Thread.new{ sleep }.inspect)
 
t1 = Thread.new{ }

 
puts(t1.status)
 
t2 = Thread.new{ raise("Exception") }

 
puts(t2.status)

output

#<Thread:0xb7dc41ac run>
#<Thread:0xb7db66c4 dead>

#<Thread:0xb7db6660 sleep>
false
nil

Obter o estado de uma Thread é uma operação bastante simples como se pode ser no exemplo acima.

Garantir que a Thread é Executada

Voltamos agora ao problema mostrado no nosso primeiro exemplo em que o programa terminava antes que as Threads fossem executadas, nesse exemplo resolvemos o problema com a inserção de um sleep no nosso programa, mas inserir delays na aplicação não é de forma alguma a melhor solução.
Para isso usamos o método join, este método obriga a que a Thread invocadora espere que a nova Thread termine e só então esta continua a execução.
Modificando então o nosso exemplo obtemos o seguinte exemplo.

palavras = ["Um","Dois","Tres","Quatro"]
 
numeros = [1,2,3,4]

 
t1 = Thread.new {
  palavras.each { |palavra| puts(palavra + " - ") }

}
 
t2 = Thread.new{
  numeros.each { |numero| puts(numero) }

}
 
t1.join()
t2.join()

output

Um - 1
Dois - 2
Tres - 3
Quatro - 4

Como podemos ver o resultado é precisamente o pretendido e desta vez sem delays artificiais e sem o tempo de execução negligenciado. O método join pode ainda ter como argumento um inteiro que define o tempo em segundos pelo qual a Thread chamadora deve esperar no máximo pela Thread invocada, basicamente é um timeout para o join.

Prioridades das Threads

Até agora demos ao Ruby total liberdade de gerir o tempo passado em cada uma das Threads. Mas em determinadas situações certas Threads são mais importantes que outras. Por exemplo temos uma Thread que guarda uma determinada quantidade de dados num ficheiro e outras que mostra o progresso da gravação, faz sentido que a Thread responsável pela escrita no ficheiro tenha disponível mais tempo que a Thread que apenas mostra o progresso da gravação.
Para isso o Ruby permite a utilização de inteiros para indicar a prioridade de cada Thread, em teoria Threads com prioridade mais alta tem mais tempo de execução que Threads com prioridade mais baixa, na pratica não é tão linear devido a outros factores como por exemplo o numero de Threads que estão a correr.
Uma vez que as prioridades em pequenos programas são praticamente impossíveis de visualizar vamos utilizar a uma função factorial que será chamada 100 vezes por cada um das Threads.

def fac(n)
  n == 1 ? 1 : n * fac(n - 1)

end
 
t1 = Thread.new{
  0.upto(100) {

    fac(50)
    puts("t1\n")
  }

}
 
t2 = Thread.new{
  0.upto(100) {

    fac(50)
    puts("t2\n")
  }

}
 
t3 = Thread.new{
  0.upto(100) {

    fac(50)
    puts("t3\n")
  }

}
 
t1.priority=0
t2.priority=0
t3.priority=1
 
t1.join()

t2.join()
t3.join()

Como podemos ver são criadas três Threads ambas com as mesmo funcionalidade, apenas com identificadores diferentes. Se colocar-mos todas as Threads com a mesma prioridade o output será o seguinte.

output

t1
t2
t3
t1
t2
t3
t1
t2
t3

Ou seja Thread 1 seguida pela Thread 2 e por fim a Thread 3, a sequência repete-se até ao final. Por outro lado se por exemplo dermos prioridade 1 à Thread t2 e 0 à Thread 1 e 2 como mostrado no código acima o resultado será o seguinte.

output

t1
t2
t3
t1
t3
t3
…
t3
t2
t1
t2
t1
t2

Ou seja a Thread 3 passa a ter mais tempo de execução que a Thread 1 e 2 assim sendo é executada e termina primeiro que as restantes e em seguida as Thread 1 e 2 correr em paralelo dado terem a mesma prioridade. Também podem ser utilizados numero negativos para definir a prioridade das Threads, por vezes pode até ser preferível o uso deste numero devido à Thread Main que tem prioridade 0 como vamos ver em seguida.

Prioridade da Main Thread

Como qualquer Thread também a Main Thread tem um grau de prioridade que por defeito é 0. Sendo assim ao atribuir valores positivos para as restantes Threads estamos a dar maior prioridade a estas Threads em relação à Main. Ao usar valores negativos para as outras Threads estamos a garantir que a Main Thread estará sempre a um nível superior em relação a todas as outras Threads garantindo a coerência de execução. No caso de se preferir usar números positivos podemos definir a prioridade da Main Thread com uma numero elevado que não será superado pelas restantes, por exemplo 100.

Thread.main.priority=100

Criar mas não executar uma Thread

Todos os exemplos que vimos até agora ao criar uma Thread ela automaticamente começa a sua execução, alias no exemplo anterior com prioridades podemos ver que antes das prioridades serem definidas as Thread são executadas pela ordem que são criadas e só em seguida passam a ser executadas pelas prioridades que lhes são dadas, para resolver este pequeno problema basta usarmos o método stop no inicio da definição da nossa Thread.

t1 = Thread.new{
  Thread.stop

  0.upto(100) { |i|
    puts i
  }
}

 
t1.run
t1.join()

output

0
1
...
99
100

Desta forma a Thread t1 apenas é executada após ser chamado o método run da Thread, caso este método não seja chamado a Thread não entra em execução, embora esteja criada. Se verificarmos o seu estado antes de chamar o método run podemos ver que a Thread se encontra no estado sleep.

Mutexes

Em alguns casos é necessário que duas ou mais Threads utilizem o mesmo recurso global, por exemplo uma variável global, nesta situação podemos obter resultados imprevisíveis uma vez que pode acontecer uma Thread alterar esse recursos no mesmo instante que outra Thread o utiliza, fazendo com que esta use uma recurso obsoleto. Vejamos o exemplo seguinte do que pode acontecer.

$i = 0

 
a = Thread.new { 100000.times { $i += 1 } }

 
b = Thread.new{ 100000.times{ $i += 1 } }

 
 
a.join
b.join
 
puts($i)

O que o nosso programa faz é simplesmente usar duas Threads para incrementar uma unidade à variável global “i” 100000 em cada Thread. O esperado seria termos o resultado de 200000 mas como podemos ver não é bem isso que obtemos mas sim o valor de 109589.
A razão pela qual isto acontece é porque ambas as Threads estão a utilizar o mesmo recurso e uma vez que ambas são executadas praticamente em paralelo em determinados pontos estas Threads vão utilizar um recurso que na realidade já não existe, ou seja a Thread “a” incrementa a variável mas nesse mesmo instante a Thread “b” também o faz uma vez que não existe controlo de acesso à variável. Vejamos um exemplo simples, a variável i tem valor 1000 a Thread “a” vai buscar esse valor e incremente passando a 1001 ao mesmo tempo a Thread “b” também vai buscar o valor e também o incrementa para 1001, perdendo-se então 1 unidade.

Para resolver este problema temos de garantir que uma Thread só terá acesso ao recursos depois que este tenha sido libertado por outra Thread que o tenha utilizado. Neste sentido o Ruby fornece a classe Mutex e através do método synchronize podemos garantir isso mesmo. Vejamos então o exemplo.

require "thread"
 
$i = 0;
 
semaphore = Mutex.new

 
a = Thread.new{
  semaphore.synchronize {
    100000.times { $i += 1 }

  }
}
 
b = Thread.new{
  semaphore.synchronize {

     100000.times { $i += 1 }
  }

}
 
a.join
b.join
 
puts($i)

E como esperado o valor obtido é 200000 isto deve-se o método synchronize bloqueia os recursos globais usados dentro do seu bloco e apenas os liberta após o bloco ter terminado.

Passagem de Argumentos à Thread

O método new permite ainda a passagem de argumentos para a Thread, tornando assim possível enviar dados para serem processados na Thread sem ter de recorrer a recursos globais como por exemplo variáveis globais.

t1 = Thread.new("magician") { |arg|
  Thread.stop

  puts(arg)
}
 
puts("Hello")
 
t1.run

t1.join

Como podemos ver enviamos uma string que depois é processada pelo método puts que imprime a string. E por sua vez a Main Thread imprime a String “Hello” obtendo assim o output seguinte.

output

Hello
magician

Retornar Valores de uma Thread

É também possível retornar valores de uma Thread, para isso utilizamos o método value. Este método retorna o valor final após a execução do bloco da Thread, caso nenhum valor tenha sido obtido o resultado obtido por este método será nil. Vamos então ver alguns casos de como podemos obter estes valores.

t1 = Thread.new{
  puts "T1"

}
 
t2 = Thread.new{
  5 + 5
}

 
t3 = Thread.new{
  "X"
}
 
t4 = Thread.new{

  4 + 4
  "Y"
}
 
t5 = Thread.new{

  val = 1 + 1
  "T5"
  val
}
 
t6 = Thread.new{

  2 + 2
  "T"
  puts "T6"
}

 
puts("Valor = " + (t1.value == nil ? "nil" : t1.value.to_s))

puts("Valor = " + t2.value.to_s)
puts("Valor = " + t3.value)

puts("Valor = " + t4.value)
puts("Valor = " + t5.value.to_s)

puts("Valor = " + (t6.value == nil ? "nil" : t6.value.to_s))

output

T1
T6
Valor = nil
Valor = 10
Valor = X
Valor = Y
Valor = 2
Valor = nil

Como é possível ver a Thread t1 não retorna qualquer valor como tal o valor resultante é nil, por sua vez a Thread t2 retorna um inteiro com a soma de 5 + 5 da mesma forma que a Thread t3 retorna uma string com “X”. As Threads 4, 5 e 6 mostram que o valor retornado é sempre o último a ser processado no bloco, embora não seja colocada a palavra “return” o Ruby assume que a ultima instrução contem o valor a retornar como podemos ver na Thread t6.

Matar uma Thread

Por vezes pode ser necessário terminar o Thread antes que ela termine a execução do seu bloco de código. Isto pode acontecer por exemplo quando queremos cancelar uma operação que por algum motivo já não queremos que seja executadas mas em que a sua execução já foi iniciada.
Vejamos o exemplo seguinte em que uma Thread imprime a cada segundo a String “T1” indefinidamente e após esperar 10 segundos o nosso programa força a nossa Thread a terminar.

t1 = Thread.new{
  while true
    puts("T1")

    sleep(1)
  end
}
 
 
sleep(10)

 
t1.terminate
puts(t1.alive?)

output

t1
t1
...
t1
t1
false

O método kill obriga a que a Thread termine e retorna-a em seguida passa a execução para outra Thread. Caso não existam mais Threads a executar ou a Thread terminada seja a Main Thread então é processado o encerramento do programa. Para além do método kill, podemos ainda usar os métodos exit ou terminate, qualquer um deles realiza a mesma operação.

Passar a Execução para a proxima Thread

Em determinadas situações podemos querer que certa Thread que esteja em execução, passe a execução para outras Threads, por exemplo queremos que após a Thread X executar as três primeiras operações ela passe a execução para a Thread Y par que esta execute o seu bloco de execução. Para isso podemos utilizar o método pass da classe Thread, este método faz isso mesmo, invoca o “calendário” de Threads passando a execução à próxima Thread. Vejamos um exemplo que ilustra o uso deste método.

s = ""

 
t1 = Thread.new{
  s << "a"
  Thread.pass

  s << "b"
  Thread.pass
  s << "c"
}

 
t2 = Thread.new{
  s << "x"
  Thread.pass

  s << "y"
  Thread.pass
  s << "z"
}

 
t1.join
t2.join
 
puts(s)

Neste caso são iniciadas duas Threads a primeira t1, concatena a string “a” à string na variável “s” e passa a execução à Thread t2 que concatena também a String e passa a execução novamente para a Thread t1 e por ai fora até ambas terminarem os seus blocos de execução. Resultando o seguinte output.

output

axbycz

Caso não fosse usado o método pass o resultado seria diferente, a Thread t1 iria executar todo o seu bloco de código dado ser pequeno e em seguida seria a Thread t2 a fazê-lo. Nesta situação o output obtido seria o seguinte.

output

abcxyz

Variáveis Locais da Thread

É ainda possível criar e obter variáveis locais numa Thread, podemos cria-las dentro da Thread ou fora dela bem como ter acesso a elas. O processo é muito semelhante a usar uma Hash, passemos a uma exemplo que ilustra o processo.

t1 = Thread.new{

  Thread.current[:nome] = "magician"
  Thread.current[:lvl] = 3

}
 
puts("LVL ? " + t1.key?(:lvl).to_s)

puts("Nome = " + t1[:nome])
 
t1[:lang] = "ruby"

 
puts(t1.keys)
t1.join

output

LVL ? true
Nome = magician
lang
lvl
nome

Pelo exemplo podemos ver que é extremamente simples a utilização deste recurso, caso estejamos dentro da Thread basta usar o current e trabalhar de igual forma como se fosse uma Hash, caso estejamos fora da Thread basta usar a variável que detém o objecto Thread como se fosse uma Hash. Isto permite por exemplo a passagem de valores para dentro da Thread caso esta já tenha sido iniciada pelo método new, ou caso queiramos guardar vários valores que podem depois ser consultados no exterior da Thread.

Subclasses da Classe Thread

Para além da forma mais comum de criar Threads que é utilizando o método new, é possível, implementar Threads criando subclasses da mesma. Embora esta forma seja mais comum em linguagens por exemplo como Java e C# é também possível fazê-lo em Ruby. Vamos então ver um exemplo simples de uma subclasse de Thread.

class MinhaThread < Thread

 
  def initialize(*args)
    super{
      Thread.stop

      args.each { |arg|
        print(arg+"\n")
      }

    }
  end
 
  def run
    print("Thread a iniciar...\n")

    super
  end
 
end
 
 
m = MinhaThread.new("white", "magician", "P@P")

m.run
m.join

output

Thread a iniciar...
white
magician
P@P

Embora não seja tão trivial como os exemplos dados anteriormente este é uma exemplo simples de uma subclasse da classe Thread em que reescrevemos os métodos run e initialize de forma a que façam mais algumas acções, poderíamos também reescrever outros métodos como por exemplo o exit fazendo com que para além de terminar a Thread realiza-se outra operação que estaria implícita ao terminar da Thread.

ThreadGroups

Para finalizar vamos falar de ThreadGroups, no fundo um ThreadGroup é apenas um conjuntos de Threads, imaginemos o group como um array que contem N Threads. Uma Thread apenas pode pertencer a um grupo, ao ser adicionada ao grupo x caso esteja no grupo y ela é removida do grupo y no instante em que é adicionada ao grupo x.
No momento em que é criada uma Thread pertence ao mesmo grupo a que pertence a Thread que a criou, Threads terminadas ou “mortas” tem ThreadGroup nil ou seja não estão em nenhum grupo.
Para além de permitir adicionar Threads, um grupo permite criar uma lista de todas as Threads que pertencem a esse grupo, para isso basta usar o método list. Permite ainda bloquear o grupo ou seja o grupo não deixa serem adicionadas novas Threads nem que sejam removidas as Threads existentes. Temos agora um pequeno exemplo de como usar ThreadGroups.

tg = ThreadGroup.new
 

t1 = Thread.new {
  Thread.stop
}
 
t2 = Thread.new {

  Thread.stop
}
 
 
puts(t1.group.to_s + " == " + t2.group.to_s + " == " + Thread.main.group.to_s)

 
tg.add(t1)
tg.add(t2)
 
puts(t1.group.to_s + " == " + t2.group.to_s + " != " + Thread.main.group.to_s)

 
puts("Threads = " + tg.list.to_s)
 
t3 = Thread.new {

 
}
 
puts(t3.group)
 
tg.enclose
 
t4 = Thread.new{

  Thread.stop
}
 
tg.add(t4)

output

#<ThreadGroup:0xb7ca81c4> == #<ThreadGroup:0xb7ca81c4> == #<ThreadGroup:0xb7ca81c4>
#<ThreadGroup:0xb7c99fc0> == #<ThreadGroup:0xb7c99fc0> != #<ThreadGroup:0xb7ca81c4>

Threads = #<Thread:0xb7c99f48>#<Thread:0xb7c99fac>
nil
…`add': can't move to the enclosed thread group (ThreadError)...

Pelo exemplo podemos ver que as Threads ao serem criadas ficam com o mesmo grupo que a Thread que as criou, neste caso a Main Thread, depois de serem adicionadas a outro grupo, deixam de pertencer ao grupo onde se encontra a Main Thread e passa a pertencer ao novo grupo. Por sua vez as Threads terminadas tem grupo nil como é o caso da Thread t3, em seguida podemos ver que ao bloquear o grupo com o enclose e ao tentar-mos adicionar uma nova Thread a esse grupo é lançado um ThreadError dado que não é possível adicionar ou remover Threads deste grupo. É de sublinhar que um grupo que seja definido como enclose, irá permanecer neste estado, uma vez que não é actualmente possível inverter o processo de bloqueio.

Conclusão

Ao longo deste artigo foram exploradas diversas vertentes das Threads em Ruby, conteúdos como a sua criação, controlo, processamento, finalização e até organização foram explicados e exemplificados de forma simples. É possível agora criar Threads bastante complexas e funcionais a partir dos conhecimentos básicos aqui explicados.
Como tudo em programação também as Threads devem ser usadas com ponderação e controlo, e não usar-las por tudo e por nada ou então apenas não as usar.

Referencias

http://www.ruby-doc.org/core/classes/Thread.html

http://www.ruby-doc.org/core/classes/ThreadGroup.html

Autoria

Este artigo foi originalmente escrito por Fábio Correia

Download da Revista

Download da Revista

Estando já na recta final do ano lectivo, e com algum esforço devido ao calendário de exames, a Revista PROGRAMAR volta a trazer uma nova edição. Desta vez, no 20º número poderá encontrar um artigo que explora o tema da metaprogramação e templates em C++, as continuações dos artigos acerca da linguagem AWK e do Google Web Toolkit, um novo artigo sobre o Arduino e uma breve apresentação do projecto DEI@Academy.

Aproveitamos também para tornar público o apreço pelos autores e principalmente pela incansável equipa de revisão, cujo trabalho já produz resultados bem visíveis na qualidade da revista.

http://www.revista-programar.info/front/edition/20

Ruby include ou extend?

28 Abril, 2009

A resposta é os dois! Ou seja ambos tem um funcionamento semelhante mas o resultado final é diferente dai poder usar um ou outro dependendo do resultado final que queremos obter.

O include é chamado dentro de uma classe, basicamente o que ele faz é “pegar” nos métodos do módulo e adicionar esses métodos à classe como métodos de instância ou seja os métodos do módulo podem ser acedidos através dos objectos dessa classe. Por exemplo.

x = X.new
x.metodo_do_modulo(10)

Por sua vez o extend é também chamado dentro de uma classe, também ele “pega” nos métodos do módulos e adiciona esses métodos à classe mas neste caso como métodos de classe ou seja os métodos do módulos podem ser acedidos através da classe. Vejamos o exemplo

X.metodo_do_modulo("Hello")

É essa a diferença entre os dois, o include insere os métodos na instância da classe enquanto o extend insere na classe. Temos agora um pequeno exemplo na utilização dos dois.

module A
  def print_nome(nome)
    puts("Nome = " + nome)
  end
end

module B
  def print_hello()
    puts("Hello")
  end
end

class Classe
  include A
  extend B
end

a = Classe.new

a.print_nome("Magician")
Classe.print_hello()
a.print_hello()

output

Nome = Magician
Hello
22: undefined method `print_hello' for #<Classe:0xb7c3d428> (NoMethodError)

Como podemos ver o módulo A é adicionada com include como tá os seus métodos pode ser acedidos a partir do objecto, por outro lado o módulo B que é adicionado com extend os seus métodos apenas podem ser acedidos através da classe. Ao tentar aceder o métodos do módulo B como um método de instância é retornado um erro, dado que é um método de classe e não de instância.

PCAP (Packet Capture) consiste numa API para a captura de pacotes de rede. Em sistemas baseados em Unix o Pcap é implementado na biblioteca LibPcap, no caso dos sistemas Windows este encontra-se implementado na biblioteca WinPcap.

Estas bibliotecas permitem que software consiga capturar e filtrar pacotes que viagem pela rede, enviar pacotes e até listar todas as interfaces de rede existentes no sistema bem como obter informações sobre os mesmos, tais como o nome, IP ou MAC.

É ainda possível guardar os pacotes capturados num ficheiro e mais tarde voltar esses pacotes a partir do ficheiro onde foram guardados. Tais recursos são muito utilizados em ferramentas de monitorização e análise de rede como por exemplo packet sniffers, network monitors, network intrusion detection and traffic-generators.

Jpcap é no fundo uma biblioteca intermédia entre o Java e as bibliotecas LibPcap e WinPcap, permite ao programador criar aplicações Java utilizando as funcionalidades destas bibliotecas. Existem bibliotecas semelhantes para as mais diversas linguagens tais como Python, Ruby, .NET, Perl.

Ao longo deste artigo iremos abordar alguns dos principais recursos desta biblioteca tais como:

  • Interfaces de rede disponíveis no sistema.
  • Preparar a interface de rede para captura.
  • Capturar pacotes da interface de rede.
  • Aplicar filtros à captura de pacotes de rede.
  • Gravar pacotes de rede capturados em ficheiros.
  • Ler pacotes de rede gravados em ficheiros.

Para isso iremos criar pequenas aplicações, exemplos simples que demonstram como utilizar cada um dos recursos mencionados acima.

Mas antes de tudo vamos precisar de preparar o nosso ambiente, para isso precisar do Jpcap que podemos encontrar em

http://netresearch.ics.uci.edu/kfujii/jpcap/doc/download.html

Aqui encontramos versões para vários sistemas operativos, podemos utilizar uma versão de instalação como é o caso do Windows ou Linux ou podemos utilizar a versão código-fonte, esta parte deixo ao critério de cada um, não existem vantagens ao nível de desenvolvimento.

Para instalar basta seguir os passos descritos na página http://netresearch.ics.uci.edu/kfujii/jpcap/doc/install.html

Depois da instalação vamos por mão à obra!

Interfaces de rede disponíveis no sistema

Nesta sessão vamos ver como é simples obter a lista de interfaces de rede que se encontram disponíveis no nosso sistema, para além disso vamos ver ainda como é fácil retirar alguma informação destas interfaces.

Para isso vamos ver um pequeno programa que demonstra como, usando o Jpcap, podemos obter todos estes dados de uma forma bastante simples e rápida.

import jpcap.JpcapCaptor;
import jpcap.NetworkInterface;
import jpcap.NetworkInterfaceAddress;

/**
 *
 * @author magician
 */
public class JpcapInterfaces {

    /**
     * Dado um array de bytes retorna a String equivalente.
     * @param input - Array de bytes.
     * @return String com a representação textual do array de bytes.
     */
    public static String hex2String(byte [] input){
        String output = "";
        for (int i = 0; i < input.length-1; i++) {
            output += Integer.toHexString(input[i] & 0xff) + ":";
        }
        output += Integer.toHexString(input[input.length-1] & 0xff);
        return output;
    }

    public static void main(String args[]) {

        //Obtém a lista de interfaces de rede no sistema.
        NetworkInterface[] interfaces = JpcapCaptor.getDeviceList();

        for (NetworkInterface ni : interfaces) {
            System.out.println("-------------------------------------------------");
            //Nome da interface.
            System.out.println("Nome: " + ni.name);
            //Descrição da interface caso exista.
            System.out.println("Descrição: " + ni.description);
            //DataLink da interface.
            System.out.println("Nome da DataLink : " + ni.datalink_name);
            //Descrição do DataLink caso exista.
            System.out.println("Descrição da DataLink : " + ni.datalink_description);
            //MAC Address da interface.
            System.out.println("MAC Address: "+ hex2String(ni.mac_address));

            for(NetworkInterfaceAddress a : ni.addresses){
                //Endereço de IP da interface.
                System.out.println("IP: " + a.address.getHostAddress());
                //Endereço de Broadcast da interface.
                System.out.println("BroadCast: " + a.broadcast);
                //Mascara de SubRede.
                System.out.println("SubNet: " + a.subnet.getHostAddress());
                //Em caso de ligações P2P o endereço de destino.
                System.out.println("Destinho P2P: " + a.destination);
            }
            System.out.println("-------------------------------------------------\n");
        }
    }
}

Com o exemplo acima podemos ver como é simples obter as interfaces de rede disponíveis no sistema bem como todos os seus dados, alguns deles até bastante importantes como o Nome, IP, MAC e SubNet, dados esses que podem ser utilizados mais tarde das mais diversas formas. O resultado obtido após a execução desta pequena aplicação será algo como

Nome: eth0
Descrição: null
Nome da DataLink : EN10MB
Descrição da DataLink : Ethernet
MAC Address: 0:16:36:b0:5c:d7
IP: 192.168.2.183
BroadCast: /192.168.2.255
SubNet: 255.255.255.0
Destinho P2P: null
IP: fe80:0:0:0:216:36ff:feb0:5cd7
BroadCast: null
SubNet: ffff:ffff:ffff:ffff:0:0:0:0
Destinho P2P: null
-------------------------------------------------

-------------------------------------------------
Nome: any
Descrição: Pseudo-device that captures on all interfaces
Nome da DataLink : LINUX_SLL
Descrição da DataLink : Linux cooked
MAC Address: 0:0:0:0:0:0
-------------------------------------------------

-------------------------------------------------
Nome: lo
Descrição: null
Nome da DataLink : EN10MB
Descrição da DataLink : Ethernet
MAC Address: 0:0:0:0:0:0
IP: 127.0.0.1
BroadCast: null
SubNet: 255.0.0.0
Destinho P2P: null
IP: 0:0:0:0:0:0:0:1
BroadCast: null
SubNet: ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
Destinho P2P: null
-------------------------------------------------

Este output corresponde a um sistema Linux, noutros sistemas podem aparecer diferenças nomeadamente nos nomes dos dispositivos. Por exemplo, pode aparecer algo como \Device\NPF_{C3F5996D-FB82-4311-A205-25B7761897B9} ao invés do simples eth0.

Para que as nossas aplicações consigam utilizar o jpcap correctamente estas devem ser executadas com permissões de admin, caso contrário é possível que não sejam obtidos resultados, uma vez que o sistema pcap necessita dessas permissões para realizar as operações sobre as interfaces.

Preparar a interface de rede para captura

Para preparar a captura de pacotes de rede basta utilizar o método static openDevice da classe JpcapCaptor, este método abre a interface escolhida e prepara a captura de pacotes. Abaixo podemos ver como usar este método

    NetworkInterface[] interfaces = JpcapCaptor.getDeviceList();

    //openDevice(NetworkInterface interface, int snaplen, boolean promics, int to_ms)
    JpcapCaptor captor = JpcapCaptor.openDevice(interfaces[0], 65535, false, 20);

Como podemos ver são passados quatro argumentos ao método openDevice, o primeiro argumento é a interface a ser utilizada na captura, neste caso vamos utilizar o primeiro interface da lista ou seja o “eth0”. O segundo argumento corresponde ao numero de máximo bytes a serem capturados de cada vez, o terceiro argumento irá dizer se a captura será em modo promiscuo (true) ou não (false). O modo promiscuo permite capturar pacotes de rede mesmo que a sua origem ou destino não seja a da interface aberto para captura. O modo não promiscuo apenas captura pacotes cujo destino ou origem se a interface escolhida. O último argumento corresponde ao timeout em milissegundos dado para a captura de pacotes.

Capturar pacotes da interface de rede

Vamos agora passar à captura dos pacotes, o processo pode ser feito de duas formas diferentes mas que atingem o mesmo fim. Podemos utilizar a forma callback ou one-by-one.

A forma callback passa pela utilização dos métodos processPacket ou loopPacket da classe JpcapCaptor e pela implementação da interface PacketReceiver. De uma forma resumida o que vamos fazer é criar uma classe que implementa a interface PacketReceiver onde implementamos o método receivePacket que irá conter o que deverá ser feito a cada pacote capturado. Depois de implementada a interface, usamos o método processPacket ou loopPacket, ambos atingem o mesmo fim com a diferença que no processPacket é compativel com timeout e modo non blocking.

A forma one-by-one é a mais “primitiva” e a mais flexível dado que é possível controlar todas as acções feitas sobre o pacote. Para isso utilizamos o método getPacket da classe JpcapCaptor, este método como o nome indica retorna um pacote capturado, assim apenas temos que criar um mecanismo que irá repetir o processo quantas vezes quisermos.

Vamos para isso ver um pequeno exemplo que captura 20 pacotes e imprime na consola o IP de origem/destino bem como as portas correspondentes e o tamanho do pacote.

import java.io.IOException;
import jpcap.JpcapCaptor;
import jpcap.NetworkInterface;
import jpcap.packet.Packet;
import jpcap.packet.TCPPacket;
import jpcap.packet.UDPPacket;

/**
 *
 * @author magician
 */
public class OpenInterface {

   public static void main(String args []){

        //Lista de interfaces de rede no sistema.
        NetworkInterface[] interfaces = JpcapCaptor.getDeviceList();

        try{
            //Abre a interface 0 da lista.
            JpcapCaptor captor = JpcapCaptor.openDevice(interfaces[0], 65535, false, 20);

            //Simples contador.
            int i = 0;
            Packet p = null;

            //Cliclo para capturar 20 pacotes.
            while(i < 20){
                //Captura um pacote.
                p = captor.getPacket();

                //Verifica se o pacote é do tipo TCPPacket
                if(p instanceof TCPPacket){
                    TCPPacket tcp = (TCPPacket) p;
                    System.out.println("SRC: " + tcp.src_ip.getHostAddress() + ":" + tcp.src_port +
                            "   \tDST: " + tcp.dst_ip.getHostAddress() +":" + tcp.dst_port +
                            "   \tSize = " + tcp.length + " bytes");

                }
                //Verifica se o pacote é do tipo UDPPacket
                else if(p instanceof UDPPacket){
                    UDPPacket udp = (UDPPacket) p;
                    System.out.println("SRC: " + udp.src_ip.getHostAddress() + ":" + udp.src_port +
                            "   \tDST: " + udp.dst_ip.getHostAddress() +":" + udp.dst_port +
                            "   \tSize = " + udp.length + " bytes");
                }
                i++;
            }

            //Fecha a captura de pacotes.
            captor.close();
        }
        catch(IOException io){
            System.out.println(io.getMessage());
        }
        catch(Exception e){
            System.out.println(e.getMessage());
        }
    }
}

Como podemos ver apenas foram processados os pacotes UDP e TCP, mas existem outros como por exemplo ARPPacket, DatalinkPacket, ICMPPacket entre outros. Este pequeno exemplo irá gerar um ouput semelhante ao que se segue a baixo sem os * que foram colocados por motivos óbvios de privacidade e segurança.

SRC: 7*.177.*05.30:1670         DST: 19*.16*.*.1*3:49153        Size = 40 bytes
SRC: 19*.16*.*.1*3:49153        DST: 7*.177.*05.30:1670         Size = 1119 bytes
SRC: 19*.16*.*.1*3:1900         DST: 19*.16*.*.176:1536         Size = 247 bytes
SRC: *3.*5*.40.166:51457        DST: 19*.16*.*.1*3:49153        Size = 88 bytes
SRC: *3.30.6.103:*74*           DST: 19*.16*.*.1*3:49153        Size = 40 bytes
SRC: 19*.16*.*.1*3:49153        DST: *3.30.6.103:*74*           Size = 328 bytes
SRC: 19*.16*.*.1*3:579*5        DST: *1.111.49.15*:33655        Size = 445 bytes
SRC: *4.1*3.19*.**4:60000       DST: 19*.16*.*.1*3:49794        Size = 58 bytes
SRC: 19*.16*.*.1*3:49794        DST: *4.1*3.19*.**4:60000       Size = 160 bytes
SRC: *7.93.3.*55:16*4           DST: 19*.16*.*.1*3:49153        Size = 40 bytes
SRC: 19*.16*.*.1*3:49153        DST: *7.93.3.*55:16*4           Size = 550 bytes
SRC: 91.*1.*50.*11:*7075        DST: 19*.16*.*.1*3:49153        Size = 73 bytes
SRC: 19*.16*.*.1*3:49153        DST: 91.*1.*50.*11:*7075        Size = 93 bytes
SRC: *0*.99.194.194:49*90       DST: 19*.16*.*.1*3:50043        Size = 58 bytes
SRC: 19*.16*.*.1*3:50043        DST: *0*.99.194.194:49*90       Size = 260 bytes
SRC: **.**7.*00.1*:60053        DST: 19*.16*.*.1*3:469**        Size = 313 bytes
SRC: 19*.16*.*.1*3:469**        DST: **.**7.*00.1*:60053        Size = 58 bytes
SRC: *17.13*.7*.9*:600*0        DST: 19*.16*.*.1*3:504*7        Size = 52 bytes
SRC: 19*.16*.*.1*3:504*7        DST: *17.13*.7*.9*:600*0        Size = 1410 bytes
SRC: 19*.16*.*.1*3:504*7        DST: *17.13*.7*.9*:600*0        Size = 306 bytes

Embora neste exemplo apenas tenhamos extraído estes dados é possível extrair ainda mais dados dos pacotes, como por exemplo os dados (data) enviados no ficheiro, headers, version, etc.. Iremos ver mais à frente um exemplo em que retiramos os dados enviados.

Aplicar filtros à captura de pacotes de rede

O Jpcap permite ainda a utilização de filtros de forma a limitar os pacotes capturados a um círculo restrito de possibilidades e não tudo o que passa pela interface como vimos até agora.

Vamos começar por ver a sintaxe utilizada pelo tcpdump que é a mesma aplicada ao Jpcap.

Filtragem por Host
Sintaxe Descrição Exemplo
host <host> Captura todos os pacotes que entram e saem do host host 192.168.1.100
src host <host> Captura todos os pacotes com origem no host src host 192.168.1.100
dst host <host> Captura todos os pacotes com o host como destino dst host 192.168.1.100
Filtragem por Porta
Sintaxe Descrição Exemplo
port <port> Captura todos os pacotes que entram ou saem da porta port 80
src port <port> Captura todos os pacotes com origem na porta src port 80
dst port <port> Captura todos os pacotes com a porta como destino dst port 80
Filtragem por Network
Sintaxe Descrição Exemplo
net <net> Captura todos os pacotes da rede <net> net 192.168
src net <net> Captura todos os pacotes que saem da rede <net> src net 192.168
dst net <net> Captura todos os pacotes que entram na rede <net> dst net 192.168
Filtragem por Protocolo
Sintaxe Descrição
ip Captura todos os pacotes de IP
arp Captura todos os pacotes ARP
rarp Captura todos os pacotes ARP inversos
tcp Captura todos os pacotes TCP
udp Captura todos os pacotes UDP
icmp Captura todos os pacotes ICPM
Combinação de filtros
Sintaxe Descrição Exemplo
not Negação not src net 192.168
and Concatenação tcp and src host 192.168.1.100
or Alternância port 80 or port 8080

Acima temos a sintaxe principal que é possível utilizar para criar filtros, como podemos ver podem ser criados filtros por host, port, network, tipo de pacote e podemos ainda fazer combinações de filtros utilizando operações lógicas. Vamos agora ver um exemplo da utilização de filtros e iremos também ver neste exemplo como é possível ver os dados que são enviados nos pacotes.

import java.io.IOException;
import jpcap.JpcapCaptor;
import jpcap.NetworkInterface;
import jpcap.packet.TCPPacket;

/**
 *
 * @author magician
 */
public class JpcapFilter {

    public static void main(String args []){

        //Lista de interfaces de rede no sistema.
        NetworkInterface[] interfaces = JpcapCaptor.getDeviceList();

        try{
            //Abre a interface 0 da lista.
            JpcapCaptor captor = JpcapCaptor.openDevice(interfaces[0], 65535, false, 20);

            //Captura apenas pacotes TCP com origem no host 192.168.1.100 e que
            //tem como destino a porta 80 ou seja HTTP
            captor.setFilter("tcp and src host 192.168.1.100 and dst port 80", true);

            //Simples contador.
            int i = 0;

            //Cliclo para capturar 20 pacotes.
            while(i < 20){
                //Captura um pacote e converte para TCPPacket dado que apenas
                //a capturar pacotes TCP.
                TCPPacket p = (TCPPacket) captor.getPacket();

                //Gera o output com a informação sobre o pacote
                System.out.println("SRC: " + p.src_ip.getHostAddress() + ":" + p.src_port +
                        "   \tDST: " + p.dst_ip.getHostAddress() +":" + p.dst_port +
                        "   \tSize = " + p.length + " bytes");

                //Caso o pacote contenha dados este são impressos e o programa para.
                if(p.data.length > 0){
                    System.out.println(new String(p.data));
                    break;
                }
                i++;
            }

            //Fecha a captura de pacotes.
            captor.close();
        }
        catch(IOException io){
            System.out.println(io.getMessage());
        }
        catch(Exception e){
            System.out.println(e.getMessage());
        }
    }
}

O que o programa vai fazer assumindo que o nosso IP corresponde ao 192.168.1.100 é capturar todos os pacotes TCP que são enviados pelo nosso computador e que tem como destino a porta 80. De uma forma resumida vai capturar todos os pedidos HTTP feitos por nós. Na realidade não irá capturar todos mas apenas alguns, ou seja, irá capturar até encontrar um pacote que contenha dados e irá gerar um output semelhante ao que se segue.

SRC: 192.168.1.100:49323        DST: 66.102.9.147:80    Size = 60 bytes
SRC: 192.168.1.100:49323        DST: 66.102.9.147:80    Size = 52 bytes
SRC: 192.168.1.100:49323        DST: 66.102.9.147:80    Size = 715 bytes
GET / HTTP/1.1
Host: www.google.pt
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.11) Gecko/20071204 Ubuntu/7.10 (gutsy) Firefox/2.0.0.11
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: pt-pt,pt;q=0.8,en;q=0.5,en-us;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: NID=17=BNGeDayJAh13ZHF6XFJ2mwo5N3rNTRu6WaFDUYmbgAC4pyT0kzPnSKN0qKAz_ajH2WjpqDZ92DXTdjhNyiG_s-Xwd4Mwr;
PREF=ID=8ed9295:TM=197383:LM=213911:IG=8:S=T7z-r65IhJ

O exemplo acima mostra o pedido feito pelo Firefox ao servidor www.google.pt, como podemos ver trata-se de um pedido HTTP ao servidor.

Assim encerramos este artigo, neste momento estamos prontos a criar desde grandes aplicações a pequenos utilitários de análise de pacotes de rede.

Links

JPCAP Home

JPCAP API

JPCAP Samples

LibPcap/TCPDump

Winpcap

Autoria

Este artigo foi originalmente escrito por Fábio Correia (magician) para a 18ª Edição da Revista PROGRAMAR (Fevereiro de 2009).