Ruby - wielowątkowość

Tradycyjne programy mają pojedynczy wątek wykonania, instrukcje lub instrukcje składające się na program są wykonywane sekwencyjnie aż do zakończenia programu.

Program wielowątkowy ma więcej niż jeden wątek wykonania. W każdym wątku instrukcje są wykonywane sekwencyjnie, ale same wątki mogą być wykonywane na przykład równolegle na wielordzeniowym procesorze. Często na jednej maszynie CPU wiele wątków nie jest w rzeczywistości wykonywanych równolegle, ale równoległość jest symulowana przez przeplatanie wykonywania wątków.

Ruby ułatwia pisanie programów wielowątkowych z klasą Thread . Wątki Ruby to lekki i wydajny sposób na osiągnięcie współbieżności w kodzie.

Tworzenie wątków Ruby

Aby rozpocząć nowy wątek, po prostu skojarz blok z wywołaniem Thread.new . Zostanie utworzony nowy wątek w celu wykonania kodu w bloku, a oryginalny wątek natychmiast wróci z Thread.new i wznowi wykonywanie kolejną instrukcją -

# Thread #1 is running here
Thread.new {
   # Thread #2 runs this code
}
# Thread #1 runs this code

Przykład

Oto przykład, który pokazuje, jak możemy używać wielowątkowego programu Ruby.

#!/usr/bin/ruby

def func1
   i = 0
   while i<=2
      puts "func1 at: #{Time.now}"
      sleep(2)
      i = i+1
   end
end

def func2
   j = 0
   while j<=2
      puts "func2 at: #{Time.now}"
      sleep(1)
      j = j+1
   end
end

puts "Started At #{Time.now}"
t1 = Thread.new{func1()}
t2 = Thread.new{func2()}
t1.join
t2.join
puts "End at #{Time.now}"

To da następujący wynik -

Started At Wed May 14 08:21:54 -0700 2008
func1 at: Wed May 14 08:21:54 -0700 2008
func2 at: Wed May 14 08:21:54 -0700 2008
func2 at: Wed May 14 08:21:55 -0700 2008
func1 at: Wed May 14 08:21:56 -0700 2008
func2 at: Wed May 14 08:21:56 -0700 2008
func1 at: Wed May 14 08:21:58 -0700 2008
End at Wed May 14 08:22:00 -0700 2008

Cykl życia wątku

Nowe wątki są tworzone za pomocą Thread.new . Możesz także użyć synonimów Thread.start i Thread.fork .

Nie ma potrzeby uruchamiania wątku po jego utworzeniu, uruchamia się on automatycznie, gdy dostępne są zasoby procesora.

Klasa Thread definiuje szereg metod służących do wykonywania zapytań i manipulowania wątkiem podczas jego działania. Wątek uruchamia kod w bloku skojarzonym z wywołaniem Thread.new, a następnie przestaje działać.

Wartość ostatniego wyrażenia w tym bloku jest wartością wątku i można ją uzyskać, wywołując metodę value obiektu Thread. Jeśli wątek został ukończony, wartość natychmiast zwraca wartość wątku. W przeciwnym razie metoda wartości blokuje się i nie zwraca, dopóki wątek nie zostanie zakończony.

Metoda klasy Thread.current zwraca obiekt Thread, który reprezentuje bieżący wątek. Pozwala to wątkom na manipulowanie sobą. Metoda klasy Thread.main zwraca obiekt Thread, który reprezentuje wątek główny. To jest początkowy wątek wykonania, który rozpoczął się wraz z uruchomieniem programu Ruby.

Możesz poczekać na zakończenie określonego wątku, wywołując metodę Thread.join tego wątku . Wątek wywołujący będzie blokowany do momentu zakończenia danego wątku.

Wątki i wyjątki

Jeśli wyjątek zostanie zgłoszony w głównym wątku i nie jest nigdzie obsługiwany, interpreter języka Ruby wypisuje komunikat i kończy pracę. W wątkach innych niż główny wątek nieobsłużone wyjątki powodują zatrzymanie działania wątku.

Jeśli wątek t kończy działanie z powodu nieobsługiwanego wyjątku i innego wątku swywołuje t.join lub t.value, a następnie wyjątek, który wystąpił wt jest podniesiony w wątku s.

Jeśli Thread.abort_on_exception ma wartość false , stan domyślny, nieobsługiwany wyjątek po prostu zabija bieżący wątek, a cała reszta nadal działa.

Jeśli chcesz, aby jakikolwiek nieobsługiwany wyjątek w dowolnym wątku powodował zakończenie działania interpretera, ustaw metodę klasy Thread.abort_on_exception na wartość true .

t = Thread.new { ... }
t.abort_on_exception = true

Zmienne wątku

Wątek może normalnie uzyskać dostęp do wszystkich zmiennych, które znajdują się w zakresie podczas tworzenia wątku. Zmienne lokalne dla bloku wątku są lokalne dla wątku i nie są udostępniane.

Klasa Thread ma specjalną funkcję, która umożliwia tworzenie zmiennych lokalnych wątku i uzyskiwanie do nich dostępu poprzez nazwę. Po prostu traktujesz obiekt wątku tak, jakby był hasłem, pisząc do elementów za pomocą [] = i odczytując je z powrotem za pomocą [].

W tym przykładzie każdy wątek rejestruje bieżącą wartość zmiennej count w zmiennej threadlocal z kluczem mycount .

#!/usr/bin/ruby

count = 0
arr = []

10.times do |i|
   arr[i] = Thread.new {
      sleep(rand(0)/10.0)
      Thread.current["mycount"] = count
      count += 1
   }
end

arr.each {|t| t.join; print t["mycount"], ", " }
puts "count = #{count}"

Daje to następujący wynik -

8, 0, 3, 7, 2, 1, 6, 5, 4, 9, count = 10

Główny wątek czeka na zakończenie wątków podrzędnych, a następnie wypisuje wartość count przechwyconą przez każdy.

Priorytety wątków

Pierwszym czynnikiem wpływającym na planowanie wątków jest priorytet wątków: wątki o wysokim priorytecie są planowane przed wątkami o niskim priorytecie. Dokładniej, wątek otrzyma czas procesora tylko wtedy, gdy nie ma wątków o wyższym priorytecie oczekujących na uruchomienie.

Możesz ustawić i zapytać o priorytet obiektu Ruby Thread z priorytetem = i priorytetem . Nowo utworzony wątek zaczyna się z tym samym priorytetem, co wątek, który go utworzył. Główny wątek zaczyna się z priorytetem 0.

Nie ma możliwości ustawienia priorytetu wątku przed jego uruchomieniem. Wątek może jednak podnieść lub obniżyć swój własny priorytet jako pierwsze działanie, które podejmuje.

Wykluczenie wątku

Jeśli dwa wątki współużytkują dostęp do tych samych danych i co najmniej jeden z nich modyfikuje te dane, należy szczególnie uważać, aby żaden wątek nigdy nie widział danych w niespójnym stanie. Nazywa się to wykluczeniem wątku .

Mutexto klasa, która implementuje prostą blokadę semafora w celu wzajemnie wykluczającego się dostępu do niektórych współdzielonych zasobów. Oznacza to, że w danym momencie tylko jeden wątek może utrzymywać blokadę. Inne wątki mogą zdecydować się na czekanie w kolejce, aż blokada stanie się dostępna, lub mogą po prostu wybrać natychmiastowy błąd wskazujący, że blokada nie jest dostępna.

Umieszczając dostęp do współdzielonych danych pod kontrolą muteksu , zapewniamy spójność i atomowe działanie. Spróbujmy na przykładach, pierwszy bez mutax, a drugi z mutaxem -

Przykład bez Mutax

#!/usr/bin/ruby
require 'thread'

count1 = count2 = 0
difference = 0
counter = Thread.new do
   loop do
      count1 += 1
      count2 += 1
   end
end
spy = Thread.new do
   loop do
      difference += (count1 - count2).abs
   end
end
sleep 1
puts "count1 :  #{count1}"
puts "count2 :  #{count2}"
puts "difference : #{difference}"

To da następujący wynik -

count1 :  1583766
count2 :  1583766
difference : 0
#!/usr/bin/ruby
require 'thread'
mutex = Mutex.new

count1 = count2 = 0
difference = 0
counter = Thread.new do
   loop do
      mutex.synchronize do
         count1 += 1
         count2 += 1
      end
   end
end
spy = Thread.new do
   loop do
      mutex.synchronize do
         difference += (count1 - count2).abs
      end
   end
end
sleep 1
mutex.lock
puts "count1 :  #{count1}"
puts "count2 :  #{count2}"
puts "difference : #{difference}"

To da następujący wynik -

count1 :  696591
count2 :  696591
difference : 0

Obsługa impasu

Kiedy zaczynamy używać obiektów Mutex do wykluczania wątków, musimy uważać, aby uniknąć impasu . Zakleszczenie to stan, który występuje, gdy wszystkie wątki oczekują na pozyskanie zasobu przechowywanego przez inny wątek. Ponieważ wszystkie wątki są zablokowane, nie mogą zwolnić blokad, które trzymają. A ponieważ nie mogą zwolnić blokad, żaden inny wątek nie może uzyskać tych blokad.

W tym miejscu pojawiają się zmienne warunkowe . Zmienna warunek jest po prostu semafor, który jest związany z zasobem i jest stosowany w ochronie określonej mutex . Gdy potrzebujesz zasobu, który jest niedostępny, czekasz na zmienną warunku. Ta czynność zwalnia blokadę odpowiedniego muteksu . Gdy jakiś inny wątek sygnalizuje, że zasób jest dostępny, oryginalny wątek kończy oczekiwanie i jednocześnie odzyskuje blokadę w regionie krytycznym.

Przykład

#!/usr/bin/ruby
require 'thread'
mutex = Mutex.new

cv = ConditionVariable.new
a = Thread.new {
   mutex.synchronize {
      puts "A: I have critical section, but will wait for cv"
      cv.wait(mutex)
      puts "A: I have critical section again! I rule!"
   }
}

puts "(Later, back at the ranch...)"

b = Thread.new {
   mutex.synchronize {
      puts "B: Now I am critical, but am done with cv"
      cv.signal
      puts "B: I am still critical, finishing up"
   }
}
a.join
b.join

To da następujący wynik -

A: I have critical section, but will wait for cv
(Later, back at the ranch...)
B: Now I am critical, but am done with cv
B: I am still critical, finishing up
A: I have critical section again! I rule!

Stany wątku

Istnieje pięć możliwych wartości zwracanych, odpowiadających pięciu możliwym stanom, jak pokazano w poniższej tabeli. Metoda status zwraca stan wątku.

Stan wątku Wartość zwracana
Runnable biegać
Spanie Spanie
Przerwanie przerywanie
Zakończono normalnie fałszywy
Zakończono z wyjątkiem zero

Metody klas wątków

Poniższe metody są dostarczane przez klasę Thread i mają zastosowanie do wszystkich wątków dostępnych w programie. Te metody zostaną wywołane jako używające nazwy klasy wątku w następujący sposób -

Thread.abort_on_exception = true
Sr.No. Metody i opis
1

Thread.abort_on_exception

Zwraca status globalnego przerwania w przypadku wyjątku . Wartość domyślna to false . Ustawienie wartości true spowoduje przerwanie wszystkich wątków (proces zakończy działanie (0)), jeśli w dowolnym wątku zostanie zgłoszony wyjątek

2

Thread.abort_on_exception=

Po ustawieniu na true wszystkie wątki zostaną przerwane, jeśli zostanie zgłoszony wyjątek. Zwraca nowy stan.

3

Thread.critical

Zwraca stan globalnego warunku krytycznego wątku .

4

Thread.critical=

Ustawia stan globalnego warunku krytycznego wątku i zwraca go. Po ustawieniu na true zabrania planowania jakiegokolwiek istniejącego wątku. Nie blokuje tworzenia i uruchamiania nowych wątków. Niektóre operacje wątku (takie jak zatrzymanie lub zabicie wątku, uśpienie w bieżącym wątku i zgłoszenie wyjątku) mogą spowodować zaplanowanie wątku nawet w krytycznej sekcji.

5

Thread.current

Zwraca aktualnie wykonywany wątek.

6

Thread.exit

Kończy aktualnie działający wątek i planuje uruchomienie innego wątku. Jeśli ten wątek jest już oznaczony do zabicia, exit zwraca Thread. Jeśli to jest wątek główny lub ostatni wątek, zakończ proces.

7

Thread.fork { block }

Synonim Thread.new.

8

Thread.kill( aThread )

Powoduje, że dany wątek na wyjściu

9

Thread.list

Zwraca tablicę obiektów Thread dla wszystkich wątków, które mogą być uruchomione lub zatrzymane. Wątek.

10

Thread.main

Zwraca główny wątek procesu.

11

Thread.new( [ arg ]* ) {| args | block }

Tworzy nowy wątek do wykonywania instrukcji podanych w bloku i zaczyna go uruchamiać. Wszystkie argumenty przekazane do Thread.new są przekazywane do bloku.

12

Thread.pass

Wywołuje harmonogram wątków, aby przekazać wykonanie do innego wątku.

13

Thread.start( [ args ]* ) {| args | block }

Zasadniczo to samo, co Thread.new . Jeśli jednak klasa Thread jest podklasą, wywołanie start w tej podklasie nie spowoduje wywołania metody inicjalizacji podklasy .

14

Thread.stop

Zatrzymuje wykonywanie bieżącego wątku, przełącza go w stan uśpienia i planuje wykonanie innego wątku. Resetuje stan krytyczny na fałsz.

Metody wystąpienia wątku

Te metody mają zastosowanie do wystąpienia wątku. Te metody będą wywoływane jako wykorzystujące wystąpienie Thread w następujący sposób -

#!/usr/bin/ruby

thr = Thread.new do   # Calling a class method new
   puts "In second thread"
   raise "Raise exception"
end
thr.join   # Calling an instance method join
Sr.No. Metody i opis
1

thr[ aSymbol ]

Odniesienie do atrybutu - zwraca wartość zmiennej lokalnej wątku, używając symbolu lub nazwy aSymbol . Jeśli określona zmienna nie istnieje, zwraca nil .

2

thr[ aSymbol ] =

Przypisanie atrybutu - ustawia lub tworzy wartość zmiennej lokalnej wątku przy użyciu symbolu lub ciągu.

3

thr.abort_on_exception

Zwraca status przerwania warunku wyjątku dla thr . Wartość domyślna to false .

4

thr.abort_on_exception=

Ustawienie wartości true powoduje, że wszystkie wątki (w tym program główny) są przerywane, jeśli w thr zostanie zgłoszony wyjątek . Proces skutecznie zakończy się (0) .

5

thr.alive?

Zwraca wartość true, jeśli thr działa lub śpi.

6

thr.exit

Kończy thr i planuje uruchomienie innego wątku. Jeśli ten wątek jest już oznaczony do zabicia, exit zwraca Thread . Jeśli jest to wątek główny lub ostatni wątek, kończy proces.

7

thr.join

Wątek wywołujący zawiesi wykonywanie i uruchomi thr . Nie wraca, dopóki thr nie wyjdzie. Wszelkie wątki, które nie zostaną przyłączone, zostaną zabite po zakończeniu działania programu głównego.

8

thr.key?

Zwraca wartość true, jeśli dany ciąg (lub symbol) istnieje jako zmienna lokalna wątku.

9

thr.kill

Synonim Thread.exit .

10

thr.priority

Zwraca priorytet thr . Wartość domyślna to zero; Wątki o wyższym priorytecie będą uruchamiane przed wątkami o niższym priorytecie.

11

thr.priority=

Ustawia priorytet thr na Integer. Wątki o wyższym priorytecie będą uruchamiane przed wątkami o niższym priorytecie.

12

thr.raise( anException )

Podnosi wyjątek z thr . Dzwoniący nie musi być thr .

13

thr.run

Budzi się thr , dzięki czemu kwalifikuje się do planowania. Jeśli nie jest to sekcja krytyczna, wywołuje harmonogram.

14

thr.safe_level

Zwraca bezpieczny poziom obowiązujący dla thr .

15

thr.status

Zwraca stan thr : sleep, jeśli thr śpi lub czeka na I / O, run, jeśli thr jest wykonywany, false, jeśli thr zostało zakończone normalnie, i nil, jeśli thr zostało zakończone z wyjątkiem.

16

thr.stop?

Zwraca wartość true, jeśli thr jest martwy lub śpi.

17

thr.value

Czeka, aż thr zakończy się za pośrednictwem Thread.join i zwraca jego wartość.

18

thr.wakeup

Zaznacza thr jako kwalifikujące się do planowania, może jednak nadal pozostać zablokowane we / wy.