Współbieżność

revcorey

Wstęp

W dzisiejszych czasach, przy zwiększającej się liczbie rdzeni w procesorach wykorzystywanych w domowych desktopach, coraz większy nacisk kładzie się na współbieżność. Jest to możliwość wykonywania przez program różnych zadań w tym samym czasie, np. nasz edytor tekstowy nasłuchuje czy nie przychodzą żadne pakiety z sieci a jednocześnie sprawdza naszą pisownię. Programowanie wielowątkowe pomaga uzyskać większą wydajność, jednak pojawiają się nowe wyzwania takie jak np. zakleszczanie czy sytuacje hazardowe.

Runnable, Callable, Thread i wykonawcy

Aby utworzyć nowe zadanie klasa musi dziedziczyć po interfejsie Runnable lub Callable. Dwiema Podstawowymi różnicą między dwoma interfejsami jest to, iż dla Runnable musimy napisać metodę run() a, dla Callable metodę call(). Metoda run() nie zwraca nic, z kolei call() pozwala nam zwrócić wynik.

Import java.util.concurrent.*;

	public class RunnableVersion implements Runnable{
	private int count=0;
public RunnableVersion(){}
public void run(){
	count++;
	System.out.println(count);
	Thread.yield();
}

}
public class CallableVersion implements Callable<String>{
	private int count;
public CallableVersion(int cn){count = cn;}
public String call(){
	System.out.println(„Wywołanie CallableVersion z count ” +count);
	Thread.yield();
}

}
public class ExecuteMyThread{
public void main(String[] args){
	ExecutorService exec=Executors.newCachedThreadPool();
	//ExecutorService exec=Executors.newFixedThreadPool(10);
	//ExecutorService exec=Executors.newSingleThreadExecutor();
	/*
	Thread t =new Thread(new  RunnableVersion());
	t.start();
*/
	   for(int i=0;i<5;i++)
	exec.execute(new new RunnableVersion());
	

	ArrayList<Future<String>> call_list=  new ArrayList<Future<String>>();
	    for(int i =0;i<2;i++)
	call_list.add(exec.submit(new CallableVersion(i)));
	    for(Future<String> str:call_list){
	 try{
		System.out.println(str.get());}
	catch(InterruptException e){
	System.out.prinln(e);
	return;}catch(ExectutionException e){System.out.println(e); return;}

	 finally{exec.shutdown();}
}

}
}

Opis zacznę od wykomentowanego kodu z obiektem Thread. Obiekt Thread wymaga przekazania klasy implementującej interfejs Runnable, metoda start() obiektu inicjalizuje wątek i wywołuje metodę run() obiektu RunnableVersion. Jest to najprostszy sposób stworzenia wątku.
Obiekty ExecutorService to tak zwani wykonawcy. Są pośrednikami między klientem a wykonawcą(zalecana metoda uruchamiania wątków w javie od SE5 w górę). Mamy różne rodzaje wykonawców. NewCachedThreadPool() jest to pula wątków dynamicznie rozszerzana w miarę potrzeb (co może zająć nieco czasu wykonywania) .newFixedThreadPool(10) tworzy pulę 10 wątków. NewSingleThreadExecutor(); zawiera pulę wątków równą jeden, jeśli przekażemy do niego więcej niż jedno zadanie zostaną one uszeregowane i wykonane w miarę możliwości. Przeskoczmy jeszcze na sam koniec do bloku finally exec.shutdown(); to blokada uruchamiania następnych wątków przez dany obiekt wykonawcy.
Czymże jest ArrayList<Future<String>>? Obiekt Future<T> reprezentuje ona wyniki operacji asynchronicznych. Wyniki w nim znajdujące się mogą być niekompletne, toteż umieściłem go w bloku try. Wywołanie yield(); to sugestia wywołania innych wątków o tym samym priorytecie. Oczywiście można także odziedziczyć klasę Thread i w ten sposób tworzyć wątki

Priorytety Wątków

Priorytety wątków oznaczają to iż dany wątek będzie wykonywany rzadziej lub częściej w zależności od priorytetu.

public class RunnableVersion implements Runnable{
	private int priorytet;
	private int count=0;
public RunnableVersion(int pr){priorytet = pr;}
public void run(){
	Thread.currentThread.setPriority(priorytet);
	count++;
	System.out.println(count);
	Thread.yield();
}
public class ExecuteMyThread{
public void main(String[] args){
	ExecutorService exec = Executors.newCachedThreadPool();
	exec.execute(new RunnableVersion(Thread.MIN_PRIORITY));
	exec.execute(new RunnableVersion(Thread.MAX_PRIORITY));
	exec.shutdown();
}}

Wywołanie Thread.currentThread.setPriority(priorytet); służy ustawieniu priorytetu. W javie jest 10 poziomów priorytetów, jednak ich odwzorowanie w różnych systemach nie zawsze jest dobre toteż najbezpieczniej korzystać z trzech: Thread.MIN_PRIORITY, Thread.NORMAL_PRIORITY, Thread.MAX_PRIORITY.

Wątki demony

Są to specjalnego rodzaju wątki. Działają one jakby w tle i zapewniaja one różne usługi dla innych wątków. Jeśli wszystkie wątki oprócz wątków demonów skończyły swoje działanie program zostaje zakończony. Aby stworzyć wątek demon trzeba to zadeklarować zaraz przed uruchomieniem wątku, np.

Thread dem = new Thread(new RunnableVersion());
dem.setDaemon(true);
dem.start();

Synchronizacja

Może się okazać że dwa wątki w tym samym czasie spróbują wykorzystać jakiś zasób(to może być drukarka bądź miejsce w pamięci). Aby uniknąć sytuacji takich jak zakleszczanie czy sytuacje hazardowe stosuje się różne sposoby synchronizacji wątków. Podczas synchronizacji dostęp do danego zasobu ma tylko jeden wątek.
Pierwszym sposobem synchronizacji jest poprzedzenie funkcji, do której mogą mieć dostęp różne wątki, słowem synchronized np.

class synchro{
public synchronized void fun(){/*jakiś kod który może potencjalnie doprowadzić do błędów w pracy programu poprzez brak synchronizacji w dostępie do zasobów*/}

}
Drugim sposobem jest użycie obiektów Lock, np.
	class synchroLock{
	private Lock lock = new ReentrantLock();//nasz obiekt lock który posłuży do synchronizacji
public void fun(){
	lock.lock();//założenie blokady
	 try{
	//kod}finally{
	lock.unlock();//zdjęcie blokady
}

}
}

Dlaczego użyłem bloku try i finally? Jeśli zostanie rzucony wyjątek mamy pewność, że mimo to zostanie zdjęta blokada.

Trzecim sposobem zabezpieczenia są sekcje krytyczne. Stosujemy je, jeśli chcemy aby wątki miały ograniczony dostęp tylko do pewnej partii kodu, np.
class synchro{
public void fun(){
	//jakiś kod
	synchronized(obiektSynchronizujący){
	//jakiś kod
}
	//jakiś kod
}
}

Co może być obiektem Synchronizującym? Może to być na przykład dany egzemplarz obiektu czyli this, ale może to być także inny obiekt. Przytoczę przykład z stron sun'a.

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

Nie ma potrzeby aby aktualizowanie c1 i c2 było prowadzone w ramach jednej metody. W ten sposób różne wątki mogą aktualizować c1 i c2.

Zawieszanie i wybudzanie

Wątki można zawieszać przy pomocy metody wait() a wybudzać przy pomocy notify() lub notifyAll(). Zawieszanie oznacza, iż wykonywanie danego wątku jest zawieszane, a blokada obiektu jest zdejmowana. Wątek możemy wybudzić za pomocą notify() lub notifyAll(). notify() wybudza pojedyncze zadanie, które zostało zawieszone na danej blokadzie, a notifyAll() wszystkie zadania zawieszone na danej blokadzie. Metody wait(), notify i notifyAll() muszą znajdować się w metodach synchronizowanych. Np. mamy klasę Blokuj, ma ona metodę, która umożliwia blokowanie (zawies()) i drugą metodę, która umożliwia odblokowanie (odwies()) za pomocą notifyAll()). Następnie tworzymy dwie klasy dziedziczące po Runnable(Zadanie1 i Zadanie2). Obiekt Blokuj występuje w obu klasach (jako obiekty blokuj1 i blokuj2). Następnie uruchamiamy zadania. W obu mteodach run() zadanie jest zawieszane za pomocą blokuj1.zawies() i blokuj2.zawies(). Następnie w main odwieszamy za pomocą odwies() tylko Zadanie1. W odwies() mamy notifyAll(), które odwiesi wszystkie zadania, ale tylko te należące do obiektu Zadanie1.
Od wersji SE5 można także używać obiektów Condition.np:

private Lock lock = new ReentrantLock();
	private Condition con = lock.newCondition();

a zawieszanie wygląda tak

con.await();

wybudzanie:

von.signal(); lub signalAll();

Kolejki

W javie istnieją kolejki synchronizowane. Pomagają one w synchronizacji zadań, np.
(przykład pochodzi z dokumentacji java se6)

class Producer implements Runnable {
   private final BlockingQueue queue;
   Producer(BlockingQueue q) { queue = q; }
   public void run() {
     try {
       while (true) { queue.put(produce()); }
     } catch (InterruptedException ex) { ... handle ...}
   }
   Object produce() { ... }
 }

 class Consumer implements Runnable {
   private final BlockingQueue queue;
   Consumer(BlockingQueue q) { queue = q; }
   public void run() {
     try {
       while (true) { consume(queue.take()); }
     } catch (InterruptedException ex) { ... handle ...}
   }
   void consume(Object x) { ... }
 }

 class Setup {
   void main() {
     BlockingQueue q = new SomeQueueImplementation();
     Producer p = new Producer(q);
     Consumer c1 = new Consumer(q);
     Consumer c2 = new Consumer(q);
     new Thread(p).start();
     new Thread(c1).start();
     new Thread(c2).start();
   }
 }

Kolejki tego typu są wewnętrznie synchronizowane. Zadania są odpowiednio wybudzane i wznawiane dzięki czemu unikamy potrzeby wywoływania notify() i notifyAll(). Metoda take() kolejki będzie czekać, dopóki nie pojawi się jakiś obiekt włożony tam w klasie Producer. Jak widać wszystko ładnie się synchronizuje.

Podsumowanie

Programowanie współbieżne wymaga niezwykle wielkiej uwagi. Każdy ruch dobrze jest staranie zaplanować i przeanalizować. Zachęcam do przejrzenia dokumentacji javy i stron firmy sun na której znajduje się wiele przykładów. Sam temat wielowątkowości jest dość obszerny, w javie istnieją jeszcze inne mechanizmy z tym związane.

3 komentarzy

Żeby się nikt nie zdziwił. Powyższy kod zawiera całą gromadkę błędów i się nie kompiluje (najbardziej podoba mi się brak static w metodzie main ;)).

wybaczcie że prawie po roku się odzywam ale mam mało czasu, o swingworker info pojawi się w przeciągu dwóch najbliższych tygodni
edit:
nie ma szans żeby nawet w sierpniu doszło, będzie gdzieś w połowie września o swing builder i może jeszcze coś innego też dojdzie
edit2:
plany mają to do siebie że się nie sprawdzają. Mam nawał pracy i rozszerzenie artykułu przesunąłem na bliżej nie określony czas.

Artykuł niezły, ale warto dodać by jeszcze klasę SwingWorker, której możliwości są naprawdę wielkie (java 1.6). A przecież dzisiaj niemal każdy program Javy korzysta ze Swinga lub SWT.