Jak ruby uruchamia mój kod?
Dzielenie kodu na tokeny
1.9.3-p392 :003 > pp Ripper.tokenize("a = 1 + 2")
["a", " ", "=", " ", "1", " ", "+", " ", "2"]
Przekształcenie tokenów na coś, co będzie zrozumiałe dla interpretera
> pp Ripper.lex("a = 1 + 2")
[[[1, 0], :on_ident, "a"],
[[1, 1], :on_sp, " "],
[[1, 2], :on_op, "="],
[[1, 3], :on_sp, " "],
[[1, 4], :on_int, "1"],
[[1, 5], :on_sp, " "],
[[1, 6], :on_op, "+"],
[[1, 7], :on_sp, " "],
[[1, 8], :on_int, "2"]]
Przekształcenie kodu na drzewko AST (abstract syntax tree)
1.9.3-p392 :009 > pp Ripper.sexp "a = 1 + 2"
[:program,
[[:assign,
[:var_field, [:@ident, "a", [1, 0]]],
[:binary, [:@int, "1", [1, 4]], :+, [:@int, "2", [1, 8]]]]]]
=> [:program, [[:assign, [:var_field, [:@ident, "a", [1, 0]]], [:binary, [:@int, "1", [1, 4]], :+, [:@int, "2", [1, 8]]]]]]
Kompilacja drzewka AST w kod bajtowy
Maszyna wirtualna
Przeprowadza dwie operacje: kompilację i interpretację. Podczas kompilacji, maszyna ewaluuje AST i przekształca go w kod bajtowy. Nastęþnie, kod bajtowy jest uruchamiany przez interpreter. Maszyna również obsługuje wielowątkowość i rozszerzenia.
1.9.3-p392 :014 > puts RubyVM::InstructionSequence.compile("a = 1 + 2").disasm
== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 2] a
0000 trace 1 ( 1)
0002 putobject 1
0004 putobject 2
0006 opt_plus <ic:1>
0008 dup
0009 setlocal a
0011 leave
1.9.3-p392 :016 > pp RubyVM::InstructionSequence.compile("a = 1 + 2").to_a
["YARVInstructionSequence/SimpleDataFormat",
1,
2,
1,
{:arg_size=>0, :local_size=>2, :stack_max=>2},
"<compiled>",
"<compiled>",
nil,
1,
:top,
[:a],
0,
[],
[1,
[:trace, 1],
[:putobject, 1],
[:putobject, 2],
[:opt_plus, 1],
[:dup],
[:setlocal, 2],
[:leave]]]
INFO
Wielowątkowość
Wielowątkowość to uruchamianie kodu w kilku procesach.
Sekwencyjne uruchamianie:
Szybkie odpowiedzi muszą czekać na te wolniejsze,
przepustowość jest zależna od kolejki,
nie skaluje się, gdy obciążenie wzrasta,
Wątek:
Wielowątkowe uruchamianie:
Szybkie odpowiedzi przychodzą pierwsze,
przepustowość zależy od średniego czasu odpowiedzi,
czas odpowiedzi niewiele się zmienia przy wzrastającym obciążeniu,
zbyt wiele wątków może spowolnić serwer.
Procesor może wykonywać tylko jedną instrukcję naraz,
wielowątkowość jest zaimplementowana za pomocą techniki zmieniania kontekstu (ang. context switching). Zmienianie kontekstu polega na tym, że procesor przeskakuje z wykonywania jednego kawałka kodu do drugiego. Takie zmienianie jest szybsze niż przeskakiwanie pomiędzy procesami,
w Rubym wykorzystywany jest ruby fair scheduler, który każdemu kawałkowi daje 10 milisekund,
gdy w wątku jest operacja blokująca, scheduler będzie czekał na odblokowanie,
od Rubiego 1.9, stosowane są wątki natywne:
mogą być uruchamiane na wieloprocesorowych komputerach,
wykorzystywany jest scheduler systemu operacyjnego,
blokujące operacje I/O nie blokują innych wątków,
wątki muszą komunikować się ze sobą za pomocą współdzielonej pamięci co wymusza stosowanie technik mutex
lub blokad, aby nie doprowadzić do błędów w pamięci,
czasem pojawiają się niedeterministyczne zachowania,
w Rubym 1.9 dodano również włókna (ang. fibers):
GIL (ang. Global Interpreter Lock):
tylko jeden wątek może komunikować się z wirtualną maszyną naraz. Jeśli posiadamy tylko jeden procesor to wszystko jest ok, problem pojawia się, gdy mamy ich więcej
ułatwia pracę programistom - trudniej o błędy danych,
zapobiegnięcie hazardowi (ang. Race Condition) w rozszerzeniach napisanych w C,
ułatwia pisanie rozszerzeń w C,
większość rozszerzeń nie obsługuje wielowątkowości,
części implementacji Rubego (Hash
) nie obsługują wielowątkowości,
wiele implementacji nie posiada GIL
,