Wątki - wprowadzenie i przykład
Spis treści
Wprowadzenie
Tworząc swoje aplikacje, szczególnie te wykorzystujące interfejs użytkownika z pewnością spotkałeś się z sytuacją, w której pewna czynność, jak na przykład obliczenie wyniku skomplikowanej funkcji, czy pobranie pewnych danych z bazy danych, zabierało dużo czasu, a przez to aplikacja sprawiała wrażenie jakby się zawiesiła. Nie jest to pożądana funkcjonalność i każdy programista chciałby jej uniknąć.
Istnieje jednak rozwiązanie, które w stosunkowo prosty sposób pozwala poradzić sobie z tym problemem -- są to wątki.
Wątki pozwalają na symultaniczne wykonywanie pewnych operacji dzięki czemu czas wykonania pewnych operacji można znacząco skrócić. W przypadku przykładu z „zawieszeniem" się interfejsu użytkownika można pewne skomplikowane obliczenia wykonać asynchronicznie w tle, dzięki czemu użytkownik aplikacji będzie miał lepsze odczucia w związku z jej użytkowaniem. Na obrazku wygląda to tak:
To co istotne to fakt, że zastosowanie wątków sprawdza się nawet na procesorze, który posiada tylko jeden rdzeń. Jest to spowodowane tym, że każdy z wątków może otrzymywać swój czas procesora na wykonanie pewnych operacji. W przypadku jednego wątku nie ma wyjścia -- jeśli wchodzimy do funkcji obliczającej skomplikowane równanie, cały interfejs użytkownika jest zamrażany aż do momentu skończenia obliczeń, natomiast jeśli obliczenia uruchomimy w wątku niezależnym od interfejsu będą one naprzemiennie otrzymywały krótki czas procesora i będą sprawiały wrażenie wykonywania się równoległego (a w przypadku procesora wielordzeniowego faktycznie tak będą działały) -- oczywiście pomijamy tu fakt tego, że obok nich wykonuje się wiele innych procesów, które również walczą o uzyskanie czasu procesora. Bardzo ważne jest również to, że jeśli uruchomimy jeden po drugim np. 10 wątków, to wcale nie będą one wykonywały się sekwencyjnie jeden po drugim, jeśli sami o to nie zadbamy. Jeśli jakieś zadanie nie zdąży się wykonać w czasie, który został dla niego przydzielony, to może ono zostać przerwane na pewien czas.
Kiedy wykorzystywać wątki? W wielu sytuacjach:
- Wszelkie obliczenia, które mogą zablokować interfejs użytkownika powinny być wykonywane asynchronicznie
- Animacje, które powinny być przetwarzane niezależnie od interfejsu użytkownika
- Pobieranie danych z internetu (zamiast przetwarzać strony internetowe jedna po drugiej można połączyć się np. z 10 jednocześnie)
- W ogólności wszystkie operacje wejścia/wyjścia, zapis i odczyt plików, czy baz danych
- Złożone obliczenia, które mogą być podzielone na mniejsze podzadania
- I wiele innych
Mając za sobą ten krótki wstęp teoretyczny przejdźmy więc do rzeczy bardziej praktycznych.
Wątki w Javie - podstawy i przykład
Wątki w Javie można tworzyć na kilka sposobów, poprzez:
- jawne rozszerzenie klasy Thread
- stworzenie klasy implementującej interfejs Runnable, który może być wykonany w osobnym wątku (Thread)
- stworzenie klasy implementującej interfejs Callable, który może być wykonany w osobnym wątku (Thread)
Preferowane jest stosowanie rozszerzeń interfejsów (czyli 2 i 3 punkt), ponieważ dają one dużo lepszą elastyczność, szczególnie jeśli dojdziemy do momentu szeregowania wątków, utrzymywania stałej puli wątków wykonujących się w tle. Interfejsy Runnable i Callable są do siebie bardzo podobne, jednak najważniejszą różnicą jest to, że Callable może zwrócić w wyniku pewną wartość, natomiast w przypadku Runnable nie ma takiej możliwości.
Napiszmy prostą aplikację, która utworzy 10 wątków, z których każdy będzie miał za zadanie jedynie wyświetlenie swojego przypisanego ID, a następnie wstrzymanie swojego działania na krótki okres czasu -- i tak w kółko.
MyRun.java - klasa pozwalająca obiekt, który będzie wykonywany w osobnym wątku
public class MyRun implements Runnable {
private int id;
public MyRun(int id) {
this.id = id;
}
@Override
public void run() {
while(true) {
System.out.println("Watek "+id);
try {
//usypiamy wątek na 100 milisekund
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Jak widać interfejs Runnable posiada jedną metodę, którą musimy zaimplementować- run() . Wszystko co się w niej znajduje zostanie wykonane po uruchomieniu wątku, do którego przekażemy obiekt klasy MyRun. My utworzyliśmy wewnątrz niej nieskończoną pętlę, której zadaniem jest wyświetlanie ID wątku, a następnie wstrzymanie działania na 100 milisekund za pomocą statycznej metody sleep().
Runner.java - klasa testowa
public class Runner {
public static void main(String[] args) {
Runnable[] runners = new Runnable[10];
Thread[] threads = new Thread[10];
for(int i=0; i<10; i++) {
runners[i] = new MyRun(i);
}
for(int i=0; i<10; i++) {
threads[i] = new Thread(runners[i]);
}
for(int i=0; i<10; i++) {
threads[i].start();
}
}
}
Wynik:
Watek 0
Watek 3
Watek 1
Watek 7
Watek 5
Watek 2
...
W klasie Runner utworzyliśmy tablicę typu Runnable (do której przypiszemy nasze obiekty MyRun) oraz drugą tablicę obiektów Thread - czyli tabilcę, która będzie przechowywała obiekty wątków, które będziemy mogli uruchomić.
W pierwszej pętli for tworzymy obiektu typu MyRun. Obiekty te to jednak nie są jeszcze obiekty wątków - te tworzymy w drugiej pętli, jak widać w celu utworzenia obiektu Thread przekazujemy w konstruktorze nasze wcześniej utworzone obiekty MyRun (rozszerzające Runnable).
W trzeciej pętli nie pozostaje nam nic innego jak uruchomienie wątki poprzez wywołanie metody start() - powoduje ona rozpoczęcie wykonania kodu, który stworzyliśmy w metodzie run().
Co ciekawe, jeśli spojrzymy na wydruk programu, to może on być po chwili nieco zaskakujący, ponieważ numery wątków nie będą wyświetlały się w kolejności tworzenia obiektów, ale w kolejności losowej - jest to związane z tym o czym pisałem we wprowadzeniu - możliwości losowego przypisywania czasu procesora różnym wątkom.
Dla porównania zobaczmy co się stanie, jeśli zupełnie pominiemy kwestię wątków w przypadku naszego kodu.
public class MyRun {
private int id;
public MyRun(int id) {
this.id = id;
}
public void run() {
while(true) {
System.out.println("NieWatek "+id);
try {
//usypiamy wątek na 100 milisekund
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Tym razem nie implementujemy interfejsu Runnable - tworzymy jedynie nieskończoną pętlę wewnątrz metody run().
public class Runner {
public static void main(String[] args) {
MyRun[] notRunners = new MyRun[10];
for(int i=0; i<10; i++) {
notRunners[i] = new MyRun(i);
}
for(int i=0; i<10; i++) {
notRunners[i].run();
}
}
}
Wynik:
NieWatek 0
NieWatek 0
NieWatek 0
NieWatek 0
...
Tworzymy tablicę obiektów MyRun, przypisujemy do niej obiekty, a następnie wywołujemy metodę run(). Jak się jednak okazuje w konsoli eclipse wyświetli nam się jedynie "Watek 0", co jest spowodowane tym, że kod wykonywany jest w tym przypadku sekwencyjnie - wchodząc do pętli pierwszego obiektu nasza aplikacja się w niej zawiesza i wykonuje bez końca, tak więc metoda run() obiektu o id=1 nie wykona się nigdy.
Na koniec zachęcam do zapoznania się z dokumentacją klas: Thread, Runnable. W kolejnych lekcjach mam nadzieję poruszyć tematy nieco bardziej zaawansowane takie jak synchronizacja, zarządzanie wątkami, utrzymywanie stałej puli wątków, czy ich wykorzystanie w połączeniu z klasą Robot do pisania aplikacji automatyzujących pewne czynności, czy tworzenia botów.
Temat wątków w Javie warto śledzić, ponieważ są to jedne z głównych usprawnień wprowadzonych zarówno w wersji 7 jak i planowanej wersji 8.
Dyskusja i komentarze
Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.
Poniżej znajdziesz archiwalne wpisy z czasów, gdy strona była jeszcze hobbystycznym blogiem.
Grzesiek
Dzieki za przejrzyste wprowadzenie do watkow. Czekam na temat synchronizacji... Pytanie: zaimplementowalem 1-szy przyklad klasy Runner stosujac 1 wspolna petle, zamiast trzech. Czy powinna pojawic sie roznica w outpucie?
Marcin Kunert
Wynik nie powinien się za bardzo różnić jeśli chodzi o jego istotę. Wszystko zależy od tego dla którego wątku wirtualna maszyna javy zdecyduje się przydzielić czas procesora. Ponieważ tworzenie wątku nie jest szczególnie czasochłonne, to czy utworzysz wszystkie w jednej pętli i uruchomisz w innej, czy zrobisz to jednocześnie nie ma większego znaczenia.
MrPhi
Co to znaczy asynchronicznie ? Mógłbyś rozwinąć ?
lolo
Chodzi o to, że będzie można dokonywać obliczeń niezależnie od wątku głównego. Nie będzie trzeba czekać aż się wykonają tylko wątku głównym może się dziać coś innego. A gdy się wykonają obliczenia w tle to wywołają odpowiednią funkcję.