Jednym z poważniejszych wyzwań związanych ze złożonymi systemami jest zapewnienie odpowiedniego poziomu reakcji podczas wykonywania zadań. Wyzwanie to jest jeszcze większe w przypadku systemów rozszerzalnych, gdy komponenty, które nie zostały zaprojektowane do wspólnego działania, współużytkują te same zasoby. Pakiet org.eclipse.core.runtime.jobs odpowiada na to wyzwanie, udostępniając infrastrukturę umożliwiającą planowanie i wykonywanie operacji współbieżnych oraz zarządzanie nimi. Infrastruktura ta bazuje na zastosowaniu zadań, które reprezentują jednostkę pracy możliwą do wykonania w trybie asynchronicznym.
class TrivialJob extends Job { public TrivialJob() { super("Trivial Job"); } public IStatus run(IProgressMonitor monitor) { System.out.println("To jest zadanie"); return Status.OK_STATUS; } }Zadanie jest tworzone i planowane w następującym fragmencie kodu:
TrivialJob job = new TrivialJob(); System.out.println("Zadanie zostanie zaraz zaplanowane"); job.schedule(); System.out.println("Planowanie zadania zakończone");Dane wyjściowe tego programu są uwarunkowane czasowo. Oznacza to, że nie ma pewności, kiedy metoda run zadania zostanie wykonana w odniesieniu do wątku, który posłużył do utworzenia i zaplanowania zadania. Dane wyjściowe mogą mieć postać:
Zadanie zostanie zaraz zaplanowane To jest zadanie Planowanie zadania zakończonelub:
Zadanie zostanie zaraz zaplanowane Planowanie zadania zakończone To jest zadanie
Aby upewnić się, że zadanie zostało zakończone, można skorzystać z metody join(). Metoda ta zablokuje program wywołujący do momentu zakończenia zadania lub przerwania wątku wywołującego. Powyższy fragment kodu zostanie teraz poprawiony, aby miał bardziej określony charakter:
TrivialJob job = new TrivialJob(); System.out.println("Zadanie zostanie zaraz zaplanowane"); job.schedule(); job.join(); if (job.getResult().isOk()) System.out.println("Zadanie zostało pomyślnie zakończone"); else System.out.println("Zadanie nie zostało pomyślnie zakończone");Zakładając, że wywołanie metody join() nie zostanie przerwane, ta metoda powinna zwrócić następujący wynik:
Zadanie zostanie zaraz zaplanowane To jest zadanie Zadanie zostało pomyślnie zakończone
Oczywiście zazwyczaj nie należy używać metody join zaraz po zaplanowaniu zadania, ponieważ w ten sposób nie osiąga się żadnej współbieżności. W takim przypadku można by wykonać pracę z poziomu metody run zadania bezpośrednio w wątku wywołującym. W dalszej części przeanalizowanych zostanie kilka przykładów, w których użycie metody join ma większy sens.
Ostatni fragment kodu korzysta również z wyniku zadania. Wynik to obiekt IStatus zwracany przez metodę run() zadania. Można użyć tego wyniku, aby przekazać niezbędne obiekty z metody run. Wyniku można również użyć do wskazania niepowodzenia (zwracając obiekt IStatus z poziomem istotności IStatus.ERROR) lub anulowania (IStatus.CANCEL).
Omówione zostały już sposoby planowania zadań i czekania na ich zakończenie, ale możliwych jest też wiele innych operacji. Jeśli zaplanowane zostanie zadanie, ale później okaże się ono zbędne, można je zatrzymać przy użyciu metody cancel(). Jeśli w momencie anulowania zadania nie jest ono jeszcze uruchomione, zostanie natychmiast usunięte i nie zostanie uruchomione. Jeśli jednak zostało już wcześniej uruchomione, reakcja na żądanie anulowania zależy od zadania. Przy próbie anulowania zadania może przydać się możliwość zaczekania na nie przy użyciu metody join(). Poniżej przedstawiono typowy przykład anulowania zadania oraz czekania na jego zakończenie przed kontynuowaniem:
if (!job.cancel()) job.join();
Jeśli operacja anulowania nie zostanie natychmiast wykonana, metoda cancel() zwróci wartość false, a program wywołujący użyje metody join(), aby zaczekać na zadanie w celu jego pomyślnego anulowania.
Nieco mniej radykalnym rozwiązaniem niż anulowanie jest metoda sleep(). Ponownie, jeśli zadanie się jeszcze nie rozpoczęło, metoda ta spowoduje jego bezterminowe wstrzymanie. Zadanie zostanie jednak zapamiętane przez platformę, a wywołanie metody wakeUp() spowoduje dodanie go do kolejki zadań oczekujących, w której zostanie w końcu wykonane.
Zadanie przechodzi w swoim cyklu życia przez kilka stanów. Oprócz możliwości manipulowania zadaniami przy użyciu funkcji API takich jak cancel() czy sleep(), stan zadania zmienia się w miarę jego wykonywania przez platformę. Zadania mogą przechodzić przez następujące stany:
Zadanie można uśpić tylko pod warunkiem, że jego aktualny stan to WAITING. Aktywowanie uśpionego zadania spowoduje przywrócenie jego stanu na WAITING. Anulowanie zadania powoduje zmianę stanu na NONE.
Jeśli moduł dodatkowy wymaga informacji o stanie określonego zadania, może zarejestrować funkcję nasłuchiwania zmian zadań, która jest powiadamiana o kolejnych fazach cyklu życia zadania. Funkcja ta przydaje się do wyświetlania postępu lub innych typów raportów na temat zadania.
Za pomocą metody addJobChangeListener klasy Job można zarejestrować funkcję nasłuchiwania dla określonego zadania. Interfejs IJobChangeListener definiuje protokół umożliwiający reagowanie na zmiany stanu zadania:
We wszystkich przypadkach funkcja nasłuchiwania otrzymuje interfejs IJobChangeEvent, który określa zadanie przechodzące zmianę stanu i stan po zakończeniu zmiany (jeśli zmiana dokona się pomyślnie).
Uwaga: Zadania definiują również metodę getState(), która umożliwia uzyskanie możliwie aktualnego stanu zadania. Wynik nie jest jednak w każdym przypadku wiarygodny, ponieważ zadania są wykonywane w różnych wątkach i mogą ponownie zmienić stan w momencie powrotu z wywołania. Zalecanym mechanizmem wykrywania zmian stanu zadania są odpowiednie funkcje nasłuchiwania zmian.
Interfejs IJobManager definiuje protokół umożliwiający pracę ze wszystkimi zadaniami w systemie. Moduły dodatkowe wskazujące postęp lub bazujące w inny sposób na infrastrukturze zadań mogą korzystać z interfejsu IJobManager do przeprowadzania czynności takich jak zawieszanie wszystkich zadań w systemie, identyfikowanie uruchomionych zadań lub odbieranie informacji zwrotnych dotyczących postępu w wykonywaniu określonego zadania. Aby uzyskać dostęp do menedżera zadań platformy, należy użyć interfejsu API Platform:
IJobManager jobMan = Platform.getJobManager();
Moduły dodatkowe wymagające informacji o stanie wszystkich zadań w systemie mogą zarejestrować funkcję nasłuchiwania zmian w odniesieniu do menedżera zadań, zamiast rejestrować wiele takich funkcji dla poszczególnych zadań.
Czasami wygodniej jest pracować z grupą powiązanych zadań, tak jakby były one pojedynczą jednostką. W tym celu można posłużyć się rodzinami zadań. Zadanie deklaruje, że należy do określonej rodziny, przesłaniając metodę belongsTo:
public static final String MY_FAMILY = "myJobFamily"; ... class FamilyJob extends Job { ... public boolean belongsTo(Object family) { return family == MY_FAMILY; } }Za pomocą protokołu IJobManager można anulować, uśpić, połączyć lub wyszukać wszystkie zadania w rodzinie:
IJobManager jobMan = Platform.getJobManager(); jobMan.cancel(MY_FAMILY); jobMan.join(MY_FAMILY, null);
Ponieważ niektóre rodziny są reprezentowane przy użyciu arbitralnych obiektów, istnieje możliwość przechowywania interesujących stanów na poziomie samej rodziny zadań, a zadania mogą dynamicznie budować obiekty rodzin odpowiadające konkretnym potrzebom. Ważne, aby korzystać z obiektów rodzin, które są w miarę możliwości unikalne, aby uniknąć przypadkowych interakcji z rodzinami utworzonymi przez inne moduły dodatkowe.
Rodziny to również wygodny sposób wyszukiwania grup zadań. Za pomocą metody IJobManager.find(Object family) można znaleźć instancje wszystkich uruchomionych, oczekujących i uśpionych zadań w dowolnym wybranym momencie.