2012-04-19 10 views
6

Kiedy po raz pierwszy odkryłem wątki, próbowałem sprawdzić, czy faktycznie działały zgodnie z oczekiwaniami, wywołując sen w wielu wątkach, w przeciwieństwie do normalnego wywoływania snu. Udało się i byłem bardzo szczęśliwy.Co mogę użyć w wątkach Ruby, jeśli nie są one naprawdę równoległe?

Ale wtedy mój przyjaciel powiedział mi, że nici te nie były tak naprawdę równoległe, a ten sen musi udawać.

Więc teraz pisałem ten test zrobić jakieś prawdziwe przetwarzania:

class Test 
    ITERATIONS = 1000 

    def run_threads 
    start = Time.now 

    t1 = Thread.new do 
     do_iterations 
    end 

    t2 = Thread.new do 
     do_iterations 
    end 

    t3 = Thread.new do 
     do_iterations 
    end 

    t4 = Thread.new do 
     do_iterations 
    end 

    t1.join 
    t2.join 
    t3.join 
    t4.join 

    puts Time.now - start 
    end 

    def run_normal 
    start = Time.now 

    do_iterations 
    do_iterations 
    do_iterations 
    do_iterations 

    puts Time.now - start 
    end 

    def do_iterations 
    1.upto ITERATIONS do |i| 
     999.downto(1).inject(:*) # 999! 
    end 
    end 
end 

A teraz jestem bardzo smutny, ponieważ run_threads() nie tylko nie działają lepiej niż run_normal było nawet wolniej!

Dlaczego więc powinienem komplikować moją aplikację za pomocą wątków, jeśli nie są one naprawdę równoległe?

** UPDATE **

@ fl00r powiedział, że mogę skorzystać z wątków gdybym używał ich do zadań IO, więc napisałem jeszcze dwa warianty do_iterations:

def do_iterations 
    # filesystem IO 
    1.upto ITERATIONS do |i| 
    5.times do 
     # create file 
     content = "some content #{i}" 
     file_name = "#{Rails.root}/tmp/do-iterations-#{UUIDTools::UUID.timestamp_create.hexdigest}" 
     file = ::File.new file_name, 'w' 
     file.write content 
     file.close 

     # read and delete file 
     file = ::File.new file_name, 'r' 
     content = file.read 
     file.close 
     ::File.delete file_name 
    end 
    end 
end 

def do_iterations 
    # MongoDB IO (through MongoID) 
    1.upto ITERATIONS do |i| 
    TestModel.create! :name => "some-name-#{i}" 
    end 
    TestModel.delete_all 
end 

The Wyniki wydajności są wciąż takie same: normalne> wątki.

Ale teraz nie jestem pewien, czy moja maszyna wirtualna jest w stanie używać wszystkich rdzeni. Wróci, kiedy to przetestuję.

+0

Nici są do bani, włókna Fibers! : D – fl00r

+0

Nawlekanie jest sfałszowane, jeśli używasz oficjalnego rubinu, ale dla jubilera i rubiniusa, uważam, że jest to prawdziwe gwintowanie. – texasbruce

+0

Wydajność nie jest jedynym powodem używania wątków. Zobacz moją odpowiedź. – Phrogz

Odpowiedz

7

Wątki mogą być szybsze tylko wtedy, gdy masz wolne IO.

W Ruby masz globalną blokadę interpretera, więc tylko jeden wątek może działać jednocześnie. Ruby poświęca wiele czasu na to, który wątek powinien zostać uruchomiony w danej chwili (planowanie wątków). W twoim przypadku, gdy nie będzie żadnego IO, będzie wolniej!

Możesz użyć Rubinius lub JRuby, aby użyć prawdziwych wątków.

Przykład z IO:

module Test 
    extend self 

    def run_threads(method) 
    start = Time.now 

    threads = [] 
    4.times do 
     threads << Thread.new{ send(method) } 
    end 

    threads.each(&:join) 

    puts Time.now - start 
    end 

    def run_forks(method) 
    start = Time.now 

    4.times do 
     fork do 
     send(method) 
     end 
    end 
    Process.waitall 

    puts Time.now - start 
    end 

    def run_normal(method) 
    start = Time.now 

    4.times{ send(method) } 

    puts Time.now - start 
    end 

    def do_io 
    system "sleep 1" 
    end 

    def do_non_io 
    1000.times do |i| 
     999.downto(1).inject(:*) # 999! 
    end 
    end 
end 

Test.run_threads(:do_io) 
#=> ~ 1 sec 
Test.run_forks(:do_io) 
#=> ~ 1 sec 
Test.run_normal(:do_io) 
#=> ~ 4 sec 

Test.run_threads(:do_non_io) 
#=> ~ 7.6 sec 
Test.run_forks(:do_non_io) 
#=> ~ 3.5 sec 
Test.run_normal(:do_non_io) 
#=> ~ 7.2 sec 

pracy IO się 4 razy szybciej Nici i procesów podczas pracy poza IO sposobów a następnie dwa razy szybciej Nici i metod synchronizacji.

Również w Ruby prezentuje Fibers lekkich „corutines” i niesamowite em-synchrony gem do obsługi procesów asynchronicznych

+0

Zaktualizowałem swój wpis, dodając dwa kolejne przykłady wykonania niektórych operacji wejścia/wyjścia. – HappyDeveloper

+0

To polecenie jest wciąż zbyt szybkie, aby wpłynąć na wydajność. – fl00r

+0

Sprawdź moją aktualizację w Processes – fl00r

4

fl00r ma rację, globalna blokada tłumacza zapobiega wielu wątków uruchomionych w tym samym czasie w Ruby, z wyjątkiem IO.

Biblioteka parallel jest bardzo prostą biblioteką, przydatną do prawdziwie równoległych operacji. Zainstaluj za pomocą gem install parallel. Oto Twój przykład przepisany z niego korzystać:

require 'parallel' 
class Test 
    ITERATIONS = 1000 

    def run_parallel() 
    start = Time.now 

    results = Parallel.map([1,2,3,4]) do |val| 
     do_iterations 
    end 

    # do what you want with the results ... 
    puts Time.now - start 
    end 

    def run_normal 
    start = Time.now 

    do_iterations 
    do_iterations 
    do_iterations 
    do_iterations 

    puts Time.now - start 
    end 

    def do_iterations 
    1.upto ITERATIONS do |i| 
     999.downto(1).inject(:*) # 999! 
    end 
    end 
end 

Na moim komputerze (4 CPU), Test.new.run_normal trwa 4,6 sekundy, podczas gdy Test.new.run_parallel trwa 1,65 sekundy.

+0

Wow Nie wiedziałem o tym klejnocie. Spróbuję. – HappyDeveloper

+3

@HappyDeveloper Po prostu bądź ostrożny, domyślnie spawnuje procesy z potokiem jako mechanizmem wymiany. To nie jest wątek i nie jest lekki. I wątpię, byś miał jakąkolwiek przewagę, jeśli użyjesz opcji ': in_threads' z normalną Ruby. –

3

Zachowanie wątków jest definiowane przez implementację. JRuby implementuje na przykład wątki z wątkami JVM, które z kolei używają rzeczywistych wątków.

Urządzenie Global Interpreter Lock jest dostępne tylko z przyczyn historycznych. Gdyby Ruby 1.9 po prostu wprowadziła prawdziwe wątki znikąd, kompatybilność wsteczna zostałaby zerwana, a to jeszcze bardziej spowolniłoby jej przyjęcie.

This answer przez Jörg W Mittag zapewnia doskonałe porównanie między modelami wątków różnych implementacji Ruby. Wybierz taki, który jest odpowiedni do twoich potrzeb.

Z powiedział, że wątki mogą być używane czekać na proces dziecko, aby zakończyć:

pid = Process.spawn 'program' 
thread = Process.detach pid 

# Later... 
status = thread.value.exitstatus 
2

Nawet jeśli wątki nie wykonują równolegle mogą być bardzo skuteczny, prosty sposób realizacji niektórych zadań , takie jak zadania typu cron w procesie. Na przykład:

Thread.new{ loop{ download_nightly_logfile_data; sleep TWENTY_FOUR_HOURS } } 
Thread.new{ loop{ send_email_from_queue; sleep ONE_MINUTE } } 
# web server app that queues mail on actions and shows current log file data 

Używam również wątków na serwerze DRb do obsługi długotrwałych obliczeń dla jednej z moich aplikacji internetowych. Serwer internetowy rozpoczyna obliczenia w wątku i natychmiast reaguje na żądania internetowe. Może okresowo przeglądać status zadania i sprawdzać, jak się rozwija. Aby uzyskać więcej informacji, przeczytaj DRb Server for Long-Running Web Processes.

1

Dla prosty sposób, aby zobaczyć różnicę, korzystanie snu zamiast IO który również opiera się na zbyt wiele zmiennych:

class Test 


ITERATIONS = 1000 

    def run_threads 
    start = Time.now 
    threads = [] 

    20.times do 
     threads << Thread.new do 
     do_iterations 
     end 
    end 

    threads.each {|t| t.join } # also can be written: threads.each &:join 

    puts Time.now - start 
    end 

    def run_normal 
    start = Time.now 

    20.times do 
     do_iterations 
    end 

    puts Time.now - start 
    end 

    def do_iterations 
    sleep(10) 
    end 
end 

będzie to miało różnicę między gwintowanym rozwiązania nawet na MRB, z GIL

Powiązane problemy