1.
Treści programowe:
Równoległe wykonywanie operacji. Wątki. Kolejki komunikatów.
2.
Cel zajęć:
Celem
zajęć jest zrozumienie działania współpracy współbieżnie wykonywanych zadań w
ramach kilku wątków programu oraz zapoznanie się ze strukturą programu, gdzie
ważnym elementem jest przekazywanie komunikatów pomiędzy wątkami.
3. Materiały dydaktyczne
ü Thread
------------------------------------------------------------------------------------------------
·
runOnUiThread(()
Kiedy
używamy runOnUiThread()?
· gdy
jesteś w Activity
· gdy
chcesz natychmiast wykonać kod na UI Thread
· gdy
nie masz swojego Handlera
Wady:
Przykład
1
TextView textView = findViewById(R.id.textView);
new
Thread(new
Runnable()
{
@Override
public void run() {
String mmessage = "Dane
z wątku drugiego";
runOnUiThread(() -> textView.setText("Wynik:
" + mmessage));
}
}).start();
TextView textView = findViewById(R.id.textView);
new
Thread(new
Runnable()
{
@Override
public void run() {
String mmessage = "Dane
z wątku drugiego";
runOnUiThread(new
Runnable()
{
@Override
public
void run() {
textView.setText("Wynik:
" + mmessage);
}
});
}
}).start();
TextView textView =
findViewById(R.id.textView);
Handler mainHandler = new
Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message
msg) {
super.handleMessage(msg);
textView.setText("Wynik:
" + msg.obj);
}
};
new Thread(new Runnable() {
@Override
public void run()
{
Message msg = new
Message();
msg.obj
=
"Dane
z wątku drugiego";
//mainHandler.sendMessage(msg);
runOnUiThread(new
Runnable()
{
@Override
public
void run() {
textView.setText("Wynik:
" + msg.obj);
}
});
}
}).start();
}
TextView textView = findViewById(R.id.textView);
Handler mainHandler = new Handler(Looper.getMainLooper());
new
Thread(new
Runnable()
{
@Override
public void run() {
String message = "Dane
z wątku drugiego";
mainHandler.post(()
-> textView.setText("Wynik: " + message));
}
}).start();
TextView textView = findViewById(R.id.textView);
Handler mainHandler = new Handler(Looper.getMainLooper());
new
Thread(new
Runnable()
{
@Override
public void run() {
String message = "Dane
z wątku drugiego";
mainHandler.post(new
Runnable()
{
@Override
public
void run() {
textView.setText("Wynik:
" + message);
}
});
}
}).start();
Przykład, w którym przesyłane jest dwie wartości z
dwóch dodatkowych wątków (klasa MainActivity):
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
Handler handler; // Obiekt handler służy do przekazywania wiadomości
pomiędzy wątkami
TextView textView1;
private TextView textView2;
@Override
protected void onCreate(Bundle
savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView1
= (TextView) findViewById(R.id.textView1);
textView2
= (TextView) findViewById(R.id.textView2);
// W tym
miejscu handler zdefiniowany jest jako odbiorca z metodą handleMessage
wywoływaną wtedy, gdy nadchodzi wiadomość np. z innego wątku
handler
= new Handler(Looper.myLooper()){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Bundle bundle = msg.getData();
int licznik1 = bundle.getInt("LICZNIK_1");
int licznik2 = bundle.getInt("LICZNIK_2");
if(bundle.get("LICZNIK_1")!=null)
textView1.setText("Licznik 1: "+licznik1);
if(bundle.get("LICZNIK_2")!=null)
textView2.setText("Licznik 2: "+licznik2);
}
};
// Instancja
obiektu wątku stworzonego w oddzielnym pliku Watek2.java
new Watek2(handler);
//
instancja obiektu wątku stworzonego w tej samej klasie, w której obsługiwany
jest interfejs użytkownika
new Thread (new Runnable() {
int licznik=0;
@Override
public void run() {
while(true){
try {
licznik++;
Message message = new Message(); // obiekt wiadomości
message.obj=licznik; //
dodanie wartości do wiadomości
Bundle bundle = new Bundle(); //"Tobołek", do
którego wrzucane będą dane przesyłane handlerem. Bundle pozwala przesyłać dane
w formie klucz – wartość, różnego typu.
bundle.putInt("LICZNIK_1", licznik);// Wstawienie wartości licznika do obiektu bundle z
kluczem "LICZNIK_1"
message.setData(bundle); // Wstawienie obiektu bundle do wiadomości
handler.sendMessage(message); // przesłanie wiadomości do Handlera
Thread.sleep(1000); //
opóźnienie 1 sekunda
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
Klasa zawierający oddzielny wątek (Watek2.java):
import
android.os.Bundle;
import android.os.Handler;
import android.os.Message;
public class Watek2
implements
Runnable
{
Handler handler;
int licznik =0;
Thread thread;
Watek2(Handler handler){
this.handler=handler;
thread=new Thread(this);
thread.start();
}
@Override
public void run() {
while(true){
licznik++;
Message message = new Message();
message.obj=licznik;
Bundle bundle = new Bundle();
bundle.putInt("LICZNIK_2", licznik);
message.setData(bundle);
handler.sendMessage(message);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
plik
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Czas 1"
android:textSize="32dp"
android:textColor="#00f"/>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Czas 2"
android:textSize="32dp"
android:textColor="#f00"/>
</LinearLayout>
·
postInvalidate()
Do
odświeżenia wyglądu widoku z innego wątku niż UI służy metoda postInvalidate().
Ustawia flagę, że widok wymaga odświeżenia
(ponownego narysowania).
Wysyła to żądanie BEZPOŚREDNIO do głównego wątku,
nawet jeśli wywołasz ją z wątku roboczego.
Powoduje wywołanie onDraw() w najbliższym cyklu renderowania UI.
---------------------------------------------------------------------------------------------------
ü Executors
Executors to klasa dostarczająca gotowe
pule wątków, które:
- Executors
- zarządza wątkami za Ciebie.
- ExecutorService - wykonuje kod w tle.
Najpopularniejsze typy Executorów:
1.
Single Thread Executor
Jeden wątek, zadania wykonują się po kolei:
ExecutorService
executor =
Executors.newSingleThreadExecutor();
2.
Fixed Thread Pool
Określona liczba wątków pracujących równolegle:
ExecutorService
executor =
Executors.newFixedThreadPool(4);
3.
Cached Thread Pool
Tworzy nowe wątki dynamicznie, gdy ich potrzebuje:
ExecutorService
executor =
Executors.newCachedThreadPool();
-----------------------------------------------------------------------------------------------------------------
Przykład wykorzystania
klasy ExecutorService oraz Executors.newSingleThreadExecutor():
import android.os.Bundle;
import
android.os.Handler;
import
android.os.Looper;
import
android.widget.TextView;
import
androidx.appcompat.app.AppCompatActivity;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
public
class MainActivity extends AppCompatActivity {
ExecutorService executor;
protected void onCreate(Bundle
savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.textView);
// Executor – praca w tle
executor =
Executors.newSingleThreadExecutor();
// Handler – do aktualizacji
UI
Handler handler = new
Handler(Looper.getMainLooper());
executor.execute(new
Runnable()
{
@Override
public
void run() {
//
---- [MÓJ WĄTEK] ----
// Ciężka operacja (np.
obliczenia, pobieranie danych)
String
wynik = "Dane z wątku roboczego";
//
Przekazanie wyniku na wątek główny
handler.post(new Runnable() {
@Override
public
void run() {
//
---- [WĄTEK UI] ----
textView.setText(wynik);
}
});
}
});
}
@Override
protected void onDestroy()
{
super.onDestroy();
// Niezbędne do poprawnego
zamknięcia wątków
if (executor
!=
null)
{
executor.shutdown();
}
}
Dobrze jest wyłączyć executor, kiedy Activity się
niszczy executor.shutdown();
-----------------------------------------------------------------------------------------------------------------
Przykład wykorzystania
klasy ExecutorService oraz Executors.newFixedThreadPool(3):
import android.os.Bundle;
import
android.os.Handler;
import
android.os.Looper;
import
android.os.SystemClock;
import
android.widget.TextView;
import
androidx.appcompat.app.AppCompatActivity;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
public
class MainActivity extends AppCompatActivity {
ExecutorService executor;
protected void onCreate(Bundle
savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView =
findViewById(R.id.textView);
executor =
Executors.newFixedThreadPool(3);
//
Handler do aktualizacji UI
Handler uiHandler = new
Handler(Looper.getMainLooper());
//
Zadanie 1
executor.execute(new
Runnable()
{
@Override
public
void run() {
//
---- Wątek tła ----
int
wynik1
= 10
+
20;
//
symulacja
SystemClock.sleep(1000);
uiHandler.post(()
-> textView.append("Wynik 1: " +
wynik1
+
"\n"));
}
});
//
Zadanie 2
executor.execute(new
Runnable()
{
@Override
public
void run() {
//
---- Wątek tła ----
String
wynik2 = "Dane pobrane z serwera";
SystemClock.sleep(1500);
uiHandler.post(() -> textView.append("Wynik
2: " + wynik2 + "\n"));
}
});
//
Zadanie 3
executor.execute(new
Runnable()
{
@Override
public
void run() {
//
---- Wątek tła ----
double
wynik3
= Math.sqrt(12345);
SystemClock.sleep(500);
uiHandler.post(()
-> textView.append("Wynik 3: " +
wynik3
+
"\n"));
}
});
}
@Override
protected void onDestroy()
{
super.onDestroy();
// Niezbędne do poprawnego
zamknięcia wątków
if (executor
!=
null)
{
executor.shutdown();
}
}
Jeśli zadań będzie więcej niż wątków (czyli 3 w
naszym przykładzie) NOWE zadania NIE utworzą nowych wątków. Zostaną
umieszczone w kolejce i będą czekać, aż zwolni się któryś z 3 wątków.
Czyli zadania nie zginą, nic się nie wysypie
— po prostu będą wykonane później.
-----------------------------------------------------------------------------------------------------------------
Przykład użycia ExecutorService + Future + Callable
w Androidzie (Java).
To jeden ze sposobów na wykonanie zadania w tle i odebranie wartości zwrotnej.
Przykład: wykonywanie zadań w tle i pobieranie
wyników przez Future
- Callable - pozwala zwrócić wartość
- Future - odbieramy wynik, gdy zadanie
się wykona
- Handler(Looper.getMainLooper()) - bezpieczna aktualizacja UI
Future stosujemy zawsze wtedy, gdy chcemy uruchomić
zadanie w tle i ODEBRAĆ od niego wynik.
W przeciwieństwie do Runnable, który niczego nie zwraca — Future umożliwia:
To sprawia, że Future jest idealne do zadań, które
coś zwracają, np. dane z sieci, wynik obliczeń, treść pliku itd.
import android.os.Bundle;
import
android.os.Handler;
import
android.os.Looper;
import
android.widget.TextView;
import
androidx.appcompat.app.AppCompatActivity;
import
java.util.concurrent.Callable;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
import
java.util.concurrent.Future;
public
class MainActivity extends AppCompatActivity {
ExecutorService executor;
protected
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView =
findViewById(R.id.textView);
executor =
Executors.newSingleThreadExecutor();
Handler uiHandler = new
Handler(Looper.getMainLooper());
//
Zadanie zwracające wynik
Callable<String>
zadanie = new Callable<String>() {
@Override
public
String
call()
throws
Exception
{
Thread.sleep(1500);
//
symulacja ciężkiej pracy
return
"Wynik
z wątku: " + (10 + 20);
}
};
//
Wysyłamy zadanie i dostajemy Future
Future<String> future =
executor.submit(zadanie);
//
Osobny wątek sprawdza wynik Future (żeby nie blokować UI)
executor.execute(new
Runnable()
{
@Override
public
void run() {
try
{
//
Blokuje tylko WĄTEK TŁA, nie UI
String
wynik = future.get();
//
Przekazanie wyniku na wątek główny
uiHandler.post(()
-> {
textView.setText(wynik);
});
} catch
(Exception
e) {
e.printStackTrace();
}
}
});
}
@Override
protected void onDestroy()
{
super.onDestroy();
// Niezbędne do poprawnego
zamknięcia wątków
if (executor
!=
null)
{
executor.shutdown();
}
}
Wersja z lambda:
import android.os.Bundle;
import
android.os.Handler;
import
android.os.Looper;
import
android.widget.TextView;
import
androidx.appcompat.app.AppCompatActivity;
import
java.util.concurrent.Callable;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
import
java.util.concurrent.Future;
public
class MainActivity extends AppCompatActivity {
ExecutorService executor;
protected void onCreate(Bundle
savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView =
findViewById(R.id.textView);
executor =
Executors.newSingleThreadExecutor();
Handler uiHandler = new
Handler(Looper.getMainLooper());
// Lambda dla zadania
(Callable)
Callable<String>
zadanie = () -> {
Thread.sleep(1500);
//
symulacja ciężkiej pracy
return
"Wynik
z wątku (Lambda): " + (10 + 20);
};
Future<String> future = executor.submit(zadanie);
// Lambda dla wątku
sprawdzającego wynik (Runnable)
executor.execute(()
-> {
try
{
String wynik = future.get();
//
Lambda dla Handler.post (aktualizacja UI)
uiHandler.post(()
-> textView.setText(wynik));
} catch
(Exception
e) {
e.printStackTrace();
}
});
}
@Override
protected void onDestroy()
{
super.onDestroy();
// Niezbędne do poprawnego
zamknięcia wątków
if (executor
!=
null)
{
executor.shutdown();
}
}
Future
używany gdy:
1. Zadanie zwraca wartość
Np. pobieranie danych z API, z pliku, z bazy:
Future<String> wynik = executor.submit(() -> pobierzDane());
2. Chcemy poczekać na wynik — ale nie w UI Thread
String dane = future.get(); // oczekiwanie na zakończenie
3. Chcemy anulować zadanie
future.cancel(true);
4. Chcemy sprawdzić, czy zadanie się skończyło
if (future.isDone()) { ... }
5. Potrzebujemy timeout-u
future.get(2, TimeUnit.SECONDS);
Jeśli zadanie się nie wykona w 2 sekundy pojawi się
wyjątek TimeoutException.
Najważniejsza różnica między Future, a Runnable
polega na tym że Future gwarantuje wynik a Runntime nie.
----------------------------------------------------------------------------------------
Inny sposób na zwracanie
wartości i przekazywanie wyniku do wątku głównego. Wykorzystanie klasy CompletableFuture
oraz wykorzystanie puli wątków systemowych przez
mechanizm ForkJoinPool
CompletableFuture
.supplyAsync(() -> {
try
{
Thread.sleep(1500);
} catch
(InterruptedException
e) {
throw
new RuntimeException(e);
}
return
"Wynik:
" + (10 + 20);
})
.thenAccept(result ->
runOnUiThread(() -> textView.setText(result))
);
To samo bez lambdy:
CompletableFuture.supplyAsync(new
Supplier<String>()
{
@Override
public String get() {
try {
Thread.sleep(1500);
} catch (InterruptedException
e) {
e.printStackTrace();
}
return "Wynik:
" + (10 + 20);
}
}).thenAccept(new Consumer<String>() {
@Override
public void accept(final String result) {
runOnUiThread(new
Runnable()
{
@Override
public
void run() {
textView.setText(result);
}
});
}
});
----------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------
Przykład
wykorzystania klasy CompletableFuture, ExecutorService oraz Executors.newFixedThreadPool(3):
Poniższy przykład wykorzystuje klasę CompletableFuture oraz własną pulę wątków
zarządzanych przez ExecutorService.
Jest zalecana metoda programowania
wielowątkowego.
package com.example.localdatabaseroom;
import
android.os.Bundle;
import
android.os.SystemClock;
import
android.widget.TextView;
import
androidx.appcompat.app.AppCompatActivity;
import
java.util.concurrent.CompletableFuture;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
public
class MainActivity extends AppCompatActivity {
private TextView textView;
// Pula wątków dla 3 równoległych zadań
private ExecutorService executor =
Executors.newFixedThreadPool(3);
@Override
protected void onCreate(Bundle
savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView =
findViewById(R.id.textView);
textView.setText("---
ROZPOCZĘTO PRACĘ ASYNCHRONICZNĄ (UI PŁYNNE) ---\n");
// ZADANIE 1 (1000 ms)
CompletableFuture
.supplyAsync(()
-> {
SystemClock.sleep(1000);
return
10
+
20;
}, executor)
.thenAccept(wynik ->
runOnUiThread(()
-> textView.append("Wynik 1 (1000ms): Liczba: " +
wynik
+
"\n"))
);
// ZADANIE 2 (1500 ms)
CompletableFuture
.supplyAsync(()
-> {
SystemClock.sleep(1500);
return
"Dane
pobrane z serwera";
}, executor)
.thenAccept(wynik ->
runOnUiThread(()
-> textView.append("Wynik 2 (1500ms): Tekst: " +
wynik
+
"\n"))
);
// ZADANIE 3 (500 ms)
CompletableFuture
.supplyAsync(()
-> {
SystemClock.sleep(500);
return
Math.sqrt(12345);
}, executor)
.thenAccept(wynik ->
runOnUiThread(()
-> textView.append("Wynik 3 (500ms): Pierwiastek: " +
wynik
+
"\n"))
);
}
@Override
protected void onDestroy()
{
super.onDestroy();
// ZAWSZE zamykaj pulę
wątków, aby uniknąć wycieków pamięci
if (executor
!=
null)
{
executor.shutdown();
}
}
}
--------------------------------------------------------------------------------------------------------------------------
Poniższy
przykład nie jest zalecany dla androida ponieważ wykorzystuje pulę wątków
systemowych ForkJoinPool.commonPool.
- Nadal używana jest pula wątków, ale jest to pula systemowa,
wspólna dla całej maszyny wirtualnej Javy.
- Liczba wątków jest zdeterminowana przez liczbę
rdzeni procesora w urządzeniu.
- Tracisz kontrolę nad pulą (nie możesz jej
zamknąć, nie wiesz, jakie inne zadania z niej korzystają).
Thread (klasa java.lang.Thread) sam w sobie nie
jest wątkiem z puli ForkJoinPool.commonPool().
1.
Wątek Systemowy (OS Thread / Wątek Natywny)
Wątek systemowy to natywny zasób zarządzany bezpośrednio
przez jądro systemu operacyjnego (np. Linux, Windows, Android).
2.
ExecutorService (Menedżer)
ExecutorService to interfejs. Jest menedżerem,
który odrywa Cię od ręcznego tworzenia Thread. Przyjmuje zadania do kolejki i
recyklingu już istniejące wątki, zamiast tworzyć je od nowa, co znacznie
zwiększa wydajność.
3.
ForkJoinPool (Specjalista)
To jest wyspecjalizowana implementacja interfejsu ExecutorService. Jest zaprojektowany do:
· Łamania dużych problemów na małe.
· Maksymalizacji użycia CPU dzięki algorytmowi
"Work Stealing" (bezczynny wątek kradnie pracę od zajętego wątku).
Jest to idealny wybór dla operacji obliczeniowych
(np. sortowanie dużych tablic), ale zły dla zadań blokujących (np. operacji
sieciowych).
Thread
tworzy wątek
ForkJoinPool
zarządza grupą wątków, które już
istnieją.
Dlaczego Podajemy Własny ExecutorService
Gdy nie podasz własnego ExecutorService
(czyli użyjesz tylko supplyAsync(zadanie)), CompletableFuture domyślnie użyje ForkJoinPool.commonPool().
Podanie własnego wykonawcy daje Ci kilka kluczowych
korzyści i kontroli:
1. Kontrola nad Zasobami (Izolacja)
2. Zarządzanie Cyklem Życia
3. Wpływ na Nowoczesne Wątki
package com.example.localdatabaseroom;
import
android.os.Bundle;
import
android.os.SystemClock;
import
android.widget.TextView;
import
androidx.appcompat.app.AppCompatActivity;
import
java.util.concurrent.CompletableFuture;
//
UWAGA: Usunięto importy dla ExecutorService i Executors
public
class MainActivity extends AppCompatActivity {
private TextView textView;
// USUNIĘTO: Definicję ExecutorService
@Override
protected void onCreate(Bundle
savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView =
findViewById(R.id.textView);
textView.setText("---
ROZPOCZĘTO PRACĘ ASYNCHRONICZNĄ (UŻYTO commonPool) ---\n");
// ZADANIE 1 (1000 ms)
CompletableFuture
//
supplyAsync BEZ argumentu ExecutorService
.supplyAsync(()
-> {
SystemClock.sleep(1000);
return
10
+
20;
}) //
Usuń ', executor'
.thenAccept(wynik
->
runOnUiThread(()
-> textView.append("Wynik 1 (1000ms): Liczba: " +
wynik
+
"\n"))
);
// ZADANIE 2 (1500 ms)
CompletableFuture
//
supplyAsync BEZ argumentu ExecutorService
.supplyAsync(()
-> {
SystemClock.sleep(1500);
return
"Dane
pobrane z serwera";
}) //
Usuń ', executor'
.thenAccept(wynik
->
runOnUiThread(()
-> textView.append("Wynik 2 (1500ms): Tekst: " +
wynik
+
"\n"))
);
// ZADANIE 3 (500 ms)
CompletableFuture
//
supplyAsync BEZ argumentu ExecutorService
.supplyAsync(()
-> {
SystemClock.sleep(500);
return
Math.sqrt(12345);
}) //
Usuń ', executor'
.thenAccept(wynik
->
runOnUiThread(()
-> textView.append("Wynik 3 (500ms): Pierwiastek: " +
wynik
+
"\n"))
);
}
// USUNIĘTO: Metodę onDestroy, ponieważ nie ma
własnego ExecutorService do zamknięcia.
}
----------------------------------------------------------------------------------------------------------------------
4. Zadania
Zadanie 1.
Napisz aplikację realizującą funkcje stopera.
Wymagane funkcje to:
- start stopera,
- stop stopera,
- kasowanie wyniku.
Zadanie 2.
Zaprojektuj i napisz aplikację realizującą funkcje
stopera z możliwością pomiaru kilku wyników jednocześnie. Użyj mechanizmu ExecutorService.