Šta je Thread i multithreading?
Thread (srp. nit) je niz uputstava u okviru programa koji se može izvršiti nezavisno od drugog koda. Ako kompjuter ima više procesora i operativni sistem koji podržava tzv. multithreading, to omogućava programerima da dizajniraju programe čiji se thread-ovi mogu izvršavati istovremeno.
Thread mehanizmi
“Looper” i “MessageQueue” su mehanizmi koje koristi Thred pri izvršavanju zadataka. “Message” je zadatak (Runnable objekt) koji treba da se izvrši na Thread-u. MessageQueue se sa jedne strane puni zadacima koji trebaju da se izvrše (message), dok sa druge strane “looper” uzima jedan po jedan zadatak sve dok ne isprazni MessageQueue.
Kod androida MainThread (glavna nit) je zauzeta bavljenjem stvarima kao što je crtanje korisničkog interfejsa (UI), odgovaranje na interakcije korisnika i generalno, po defaultu, izvršavanje (većine) koda koje pišemo. Stoga pošto andriod podržava multithreading, pametno je iskoristiti ovu mogućnost i pri programiranju radnje koje mogu privremeno da blokiraju rad UI-a izmestiti na drugi Thread. Kada izmestimo zahtevne radnje koje se sporo izvršavaju sa glavne niti, potrebno je da se vratimo sa tim podacima na nju jer samo glavna nit može da ažurira UI.
Primer pogrešnog koda koji blokira UI
Klikom na dugme počinje da se download-uje slika sa mreže (akcija može da potraje), interfejs je sve vreme zamrznut dok traje download i dok se slika ne prikaže u svom ImageView:
1 2 3 4 5 6 7 |
((Button)findViewById(R.id.Button1)).setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { final Bitmap b = loadImageFromNetwork(); mImageView.setImageBitmap(b); } }); |
OBJAŠNJENJE:
“Caller Thread”: je thread koja poziva “worker Thread” za obavljanje nekog zadatka. Često je “Caller Thread” ustvari nit koji crta UI tzv. “Main Thread”, pa se tada “Worker Thread” još i zove “background Thread”.
Kreiranje novog Thread-a
Kreiranje novog Thread-a se može izvršiti na dva načina.
a) Ekstendovanjem Thread klase
Prvi način je da naša klasa ekstenduje klasu Thread i tako nasledi sve metode ove klase. Metoda koja je bitna za izvršavanje zadataka na Thread-u je run().
1 2 3 4 5 6 |
class ThreadKlasa extends Thread{ public void run(){ System.out.println("thread is running..."); // neki kod koji treba da se izvrši na sporednom thread-u } } |
Nakon kreiranja klase je dovoljno u okviru aktivnosti kreirati njenu instancu i pokrenuti metodu start():
1 2 |
ThreadKlasa noviThread = new ThreadKlasa() noviThread.start() |
b) Implementiranjem Runnable interfejsa
Za ovaj pristup je potrebno kreirati klasu koja implementira Runnale interfejs. Runnable interfejs sadrži samo jednu metodu run(). U ovu metodu se ubacuje kod koji treba da se izvrši. Objekat koji nastaje instanciranjem ovakve klase se koristi kao parametar pri kreiranju Thread-a. Jedan objekat se može koristiti za više Thread-ova.
1 2 3 4 5 6 |
class RunnableKlasa implements Runnable { public void run(){ System.out.println("thread is running..."); // neki kod koji treba da se izvrši na sporednom thread-u } } |
Da bi kreirali Thread na ovaj način potrebno je u okviru aktivnosti kreirati instancu klase Thread koja prihvata Runnable objekat kao parametar i zatim pokrenuti metodu start():
1 2 |
Thread noviThread = new Thread(new RunnableKlasa()) noviThread.start() |
NAPOMENA:
U praksi se instanca klase koja implementira “Runnable” interfejs nejčešće kreira “on the fly” bez definisanja klase:
1 2 3 4 5 |
new Thread(new Runnable() { public void run() { // neki kod } }).start(); |
Koja je razlika izmedju ova dva pristupa?
Ako proširimo klasu sa Thread, naša klasa ne može više proširiti neku drugu klasu jer Java ne podržava višestruko nasledjivanje. Ali, ako implementiramo Runnable interfejs, naša klasa se može i dalje proširivati sa drugim klasama. Medjutim treba naglasiti da proširivanjem klase interfejsom Runnable ipak ne dobijamo sve metode koje se dobijaju sa proširivanjem klase sa Thread (npr. yield(), interrupt()…). Stoga ukoliko Vam trebaju te metode interfejs nije opcija, ali ako vam je dovoljna metoda run() Runnable interfejs je preporučen način stim što vam preostaje opcija da možete proširite vašu klasu sa nekom drugom klasom.
Ažuriranje UI sa sporednog Thread-a
Kod Androida radnje koje se odvijaju na drugoj niti (background thread) ne mogu da ažuriraju UI, jer UI može da ažurira samo Main Thread. Stoga potrebno je da sporedna nit na neki način komunicira sa glavnom niti i pošalje joj zadatke u MessageQueue, nakon čijeg izvršenja bi glavna nit ažurirala UI.
a) View.post() / View.postDelayed()
Ovaj pristup koristi metodu post() i na taj način obezbedjuje da Runnable bude dodat u message queue gde čeka na red da se pokrene na glavnom threadu.
Primer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void onClick(View v) { Thread noviThread = new Thread(new Runnable() { public void run() { final Bitmap b = loadImageFromNetwork(); mImageView.post(new Runnable() { public void run() { mImageView.setImageBitmap(b); } }); } }) noviThread.start(); } |
Ovo je najlakši način ali nije prigodan za složenije slučajeve, jer je teško održavanje i debugovanje koda. Tako da je preporuka da se za kompleksnije interakcije koristimo ili AsyncTask i Handler.
b) Handler
Handler se koristi da olakša komunikaciju izmedju “caller Thread-a” i “worker Thread-a”. Ovaj pristup je odličan upravo kada se poveća kompleksnost kao ili kada izvodimo više ponovljajućih zadataka (npr. preuzimanje više slika koje će biti prikazane u ImageView-u…).
Primer
U ovome primeru se u novom worker Thread-u u okviru run() metode izvršava izvršava dugačka radnja
final Bitmap b = loadImageFromNetwork();
a zatim po završetku Handlerova metoda post() koja ažurira UI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
final Handler myHandler = new Handler(); Thread noviThread = new Thread(new Runnable() { @Override public void run() { final Bitmap b = loadImageFromNetwork(); myHandler.post(new Runnable() { @Override public void run() { mImageView.setImageBitmap(b); } }); } } noviThread.start(); |
Activity.runOnUiThread()
Ovo je ustvari nadogradjena verzija Handler-a. Metoda runOnUiThread() proverava da li je trenutni Thread ustvari UiThread, ako jeste odmah izvršava odredjeni kod na UI Thread-u. Tek ako trenutni Thread nije Ui Thread, onda se ponaša kao Handler koji prosledjuje zadatak u MessageQueue. Radi lakšeg shvatanja u sledećem delu je prikazan kako bi izgledao sadržaj ove metode:
1 2 3 4 5 6 7 8 9 10 11 |
final Handler mHandler = new Handler(); private Thread mUiThread; // ... public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); } // ... } |
A koristi se tako što joj se prosledi Runnable objekat sa zadatkom za izvršenje.
Primer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Thread noviThread = new Thread(new Runnable() { @Override public void run() { final Bitmap b = loadImageFromNetwork(); runOnUiThread(new Runnable() { @Override public void run() { mTextView.setText(result); } }); } }); noviThread.start(); |
b) AsyncTask
AsyncTask klasa takodje omogućava da se zadaci izvršavaju na sporednom Threadu a da se njihovi rezultati publikuju na main Thread-u. Za izvršavanje asinhronih zadataka je dovoljno ekstendovati našu klasu sa AsyncTask klasom i primeniti njene metode:
- onPreExecute()
- doInBackground()
- onProgressUpdate()
- onProgressUpdate()
- onPostExecute()
Ovo je jednostavan način za koji nije potrebno znanje Thread modela, jer klasa enkapsulira kreiranje Thread-ova i korišćenje Handler-a. Problem sa AsyncTask-om je što klasa nije “svesna” životnog ciklusa (lifecycle) aktivnosti ili fragmenta, pa je na programeru da reši šta da radi sa zadacima definisam kroz AsyncTasks kada se aktivnost uništi. Stoga ovo nije baš najbolja opcija za dugačke operacije, jer ako aplikaciju uništi Android radi oslobadjanja memorije, uništiće i naš pozadinski proces.
AsyncTasks je idealan kada je CallerThread ustvari UI Thread, i akcije ne traju dugo, kao što je:
- Prikupljanje podataka sa web servisa i njihov prikaz
- Upit bazi podataka (database query)
- Čitanje i pisanje na hard disk I/O
Primer
1 2 3 4 5 6 7 8 9 10 11 12 |
private class DownloadImageTask extends AsyncTask { protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); } protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); } } public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png"); } |
Anonimna klasa je klasa koja nema ime, stoga kada bi hteli da instanciramo objekat od ovakve klase to bi izgledalo ovako:
1 |
new { } |
Nama je potrebna specifična anonimna klasa, ona koja implementira odredjeni interfejs, stoga iako ne moramo da definišemo ime klase moramo da definišemo koji interfejs da implementiramo. Ako je interfejs definisan u svome fajlu to se postiže ubacivanjem naziva interfejsa ispred zagrada:
1 |
new NazivInterfejsa () { }; |
Pored ovoga je potrebno da implementiramo sve abstraktne metode ovog interfejsa:
1 2 3 4 5 6 |
new NazivInterfejsa () { @Override public void interfejsMetoda() { // neki kod u callback metodi } }; |