Reguły planowania

Za pomocą reguł planowania zadania można określać, kiedy zadanie jest wykonywane w odniesieniu do innych zadań. Reguły planowania pozwalają w szczególności zapobiec współbieżnemu wykonywaniu wielu zadań w sytuacji, gdy współbieżność mogłaby spowodować niespójne wyniki. Reguły umożliwiają również zapewnienie określonej kolejności wykonywania serii zadań. Skuteczność reguł planowania najlepiej jest zilustrować przykładem. Na początek zdefiniowane zostaną dwa zadania, które umożliwiają współbieżne włączanie i wyłączanie przełącznika:

      public class LightSwitch {
      private boolean isOn = false;
      public boolean isOn() {
         return isOn;
      }
      public void on() {
         new LightOn().schedule();
      }
      public void off() {
         new LightOff().schedule();
      }
      class LightOn extends Job {
         public LightOn() {
            super("Włączanie kontrolki");
         }
         public IStatus run(IProgressMonitor monitor) {
            System.out.println("Włączanie kontrolki");
            isOn = true;
            return Status.OK_STATUS;
         }
      }
      class LightOff extends Job {
         public LightOff() {
            super("Wyłączanie kontrolki");
         }
         public IStatus run(IProgressMonitor monitor) {
            System.out.println("Wyłączanie kontrolki");
            isOn = false;
            return Status.OK_STATUS;
         }
      }
   }

Teraz utworzony zostanie prosty program, który definiuje przełącznik kontrolki, a następnie włącza ją i wyłącza:

      LightSwitch light = new LightSwitch();
   light.on();
   light.off();
   System.out.println("Czy kontrolka jest włączona? " + switch.isOn());

Po uruchomieniu tego małego programu wystarczająco wiele razy otrzymamy w końcu następujący wynik:

      Wyłączanie kontrolki
   Włączanie kontrolki
   Czy kontrolka jest włączona? true

Jak to możliwe? Kontrolka miała zostać włączona i wyłączona, więc na koniec powinna być wyłączona! Problem polega na tym, że nic nie stało na przeszkodzie, żeby zadanie LightOff było wykonywane w tym samym czasie co zadanie LightOn. A zatem mimo że zadanie "włączenia" zostało zaplanowane w pierwszej kolejności, współbieżne wykonanie tych zadań powoduje, że nie można przewidzieć dokładnej kolejności wykonania. Jeśli zadanie LightOff zostanie uruchomione przed zadaniem LightOn, otrzymamy niepoprawny wynik. Trzeba znaleźć sposób, aby uniemożliwić współbieżne wykonanie tych dwóch zadań, i właśnie tutaj pomocne okazują się reguły planowania.

Przykładowy kod zostanie poprawiony przez utworzenie prostej reguły planowania, która zadziała jak blokada mutex (określana również mianem semafora binarnego):

      class Mutex implements ISchedulingRule {
      public boolean isConflicting(ISchedulingRule rule) {
         return rule == this;
      }
      public boolean contains(ISchedulingRule rule) {
         return rule == this;
      }
   }

Reguła ta jest następnie dodawana do dwóch zadań obsługujących przełącznik z poprzedniego przykładu:

      public class LightSwitch {
      final MutextRule rule = new MutexRule();
      ...
      class LightOn extends Job {
         public LightOn() {
            super("Włączanie kontrolki");
            setRule(rule);
         }
         ...
      }
      class LightOff extends Job {
         public LightOff() {
            super("Wyłączanie kontrolki");
            setRule(rule);
         }
         ...
      }
   }

Podczas planowania zadań obsługujących przełącznik infrastruktura zadań wywoła metodę isConflicting, aby porównać reguły planowania tych dwóch zadań. Zostanie odnotowane, że zadania te mają reguły, które powodują konflikt. Zostaną podjęte kroki mające na celu upewnienie się, że zadania zostaną wykonane w odpowiedniej kolejności. Zadania te nigdy nie zostaną uruchomione jednocześnie. Teraz nawet jeśli wykonamy przykładowy program milion razy, zawsze otrzymamy ten sam wynik:

      Włączanie kontrolki
   Wyłączanie kontrolki
   Czy kontrolka jest włączona? false

Z reguł można również korzystać niezależnie od zadań, stosując je w roli ogólnego mechanizmu blokującego. W kolejnym przykładzie wprowadzenie reguły w obrębie bloku try/finally zapobiega wykonaniu innych wątków i zadań z użyciem tej reguły w przedziale czasu między wywołaniami metod beginRule i endRule.

      IJobManager manager = Platform.getJobManager();
  try {
      manager.beginRule(rule, monitor);
      ... wykonywanie określonej pracy ...
   } finally {
      manager.endRule(rule);
   }

Podczas przejmowania i zwalniania reguł planowania przy użyciu tego rodzaju wzorca kodu należy zachować wyjątkowa ostrożność. Nieuwzględnienie zakończenia reguły, dla której wywołano metodę beginRule, spowoduje bezterminowe jej zablokowanie.

Tworzenie własnych reguł

Choć interfejs API zadania definiuje układ reguł planowania, nie udostępnia żadnych implementacji reguł. Ogólna infrastruktura zasadniczo nie jest w stanie określić, które zestawy zadań można bezproblemowo wykonywać współbieżnie. Domyślnie zadania nie mają reguł planowania, a zaplanowane zadanie wykonuje się tak szybko, jak szybko da się utworzyć wątek niezbędny do jego uruchomienia.

Gdy zadanie ma regułę planowania, za pomocą metody isConflicting określa się, czy reguła powoduje konflikt z regułami zadań, które są aktualnie wykonywane. Implementując metodę isConflicting, można określić, kiedy wykonanie zadania jest bezpieczne. W przykładzie z przełącznikiem implementacja metody isConflicting opiera się po prostu na porównaniu tożsamości z udostępnioną regułą. Jeśli inne zadanie ma identyczną regułę, nie zostanie uruchomione współbieżnie. Tworząc własne reguły planowania, należy się dokładnie zapoznać z układem interfejsu API dla metody isConflicting i do niego zastosować.

Jeśli zadanie ma kilka niepowiązanych warunków, można przygotować zestaw reguł planowania przy użyciu klasy MultiRule. Przykładowo, jeśli zadanie ma służyć do włączania przełącznika, a także zapisywania informacji w gnieździe sieciowym, można przypisać do niego regułę dla przełącznika i regułę dotyczącą zapisu w gnieździe, połączone w jedną regułę za pomocą metody fabrycznej MultiRule.combine.

Hierarchie reguł

Omówiona została już metoda isConflicting w interfejsie ISchedulingRule, ale nie wspomniano jeszcze metody contains. Metoda ta jest używana w ramach specjalnego zastosowania reguł planowania, które nie będzie wymagane przez wielu klientów. Reguły planowania można układać w logiczne hierarchie, które umożliwiają sterowanie dostępem do zasobów o naturze hierarchicznej. Najprostszy przykład ilustrujący ten mechanizm to system plików bazujący na drzewie. Jeśli aplikacja chce uzyskać blokadę na wyłączność w odniesieniu do danego katalogu, zwykle sygnalizuje, że wymaga również wyłącznego dostępu do plików i podkatalogów w tym katalogu. Do określania hierarchicznych związków między blokadami wykorzystuje się metodę contains. Jeśli nie ma potrzeby tworzenia hierarchii blokad, można zaimplementować metodę contains w celu wywołania metody isConflicting.

Poniżej przedstawiono przykład blokady hierarchicznej umożliwiającej sterowanie uprawnieniami do zapisu względem uchwytów java.io.File.

      public class FileLock implements ISchedulingRule {
      private String path;
      public FileLock(java.io.File file) {
         this.path = file.getAbsolutePath();
      }
      public boolean contains(ISchedulingRule rule) {
         if (this == rule)
               return true;
         if (rule instanceof FileLock)
            return path.startsWith(((FileLock) rule).path);
         if (rule instanceof MultiRule) {
            MultiRule multi = (MultiRule) rule;
            ISchedulingRule[] children = multi.getChildren();
            for (int i = 0; i < children.length; i++)
               if (!contains(children[i]))
            return false;
               return true;
            }
            return false;
      }
      public boolean isConflicting(ISchedulingRule rule) {
         if (!(rule instanceof FileLock))
            return false;
         String otherPath = ((FileLock)rule).path;
         return path.startsWith(otherPath) || otherPath.startsWith(path);
      }
   }

Metoda contains włącza się do akcji, jeśli wątek próbuje uzyskać drugą regułę, a ma już inną. Aby uniknąć możliwości zakleszczenia, każdy wątek może mieć w danym momencie tylko jedną regułę planowania. Jeśli wątek wywołuje metodę beginRule w sytuacji, gdy ma już jakąś regułę (przez wcześniejsze wywołanie metody beginRule lub wykonanie zadania z przypisaną regułą planowania), następuje sprawdzenie metody contains w celu określenia, czy te reguły są równoważne. Jeśli metoda contains zwróci dla istniejącej już reguły wartość true, wywołanie metody beginRule powiedzie się. Jeśli metoda contains zwróci wartość false, wystąpi błąd.

Aby przedstawić konkretny przykład, załóżmy, że do wątku należy przykładowa reguła FileLock założona w odniesieniu do katalogu "c:\temp". Dopóki wątek posiada tę regułę, może jedynie modyfikować pliki w obrębie poddrzewa tego katalogu. Jeśli spróbuje modyfikować pliki w innych katalogach, które nie znajdują się w katalogu "c:\temp", działanie wątku zakończy się niepowodzeniem. Reguła planowania jest zatem konkretnym wskazaniem, co wolno, a czego nie wolno robić w ramach zadania lub wątku. Złamanie tych wytycznych spowoduje wyjątek wykonania. W literaturze poświęconej współbieżności mechanizm ten określa się mianem blokowania dwufazowego. W takim układzie proces musi określić z góry wszystkie blokady, których będzie potrzebował do określonej czynności, a następnie nie może uzyskiwać żadnych innych blokad w trakcie wykonywania operacji. Blokowanie dwufazowe eliminuje warunek wstrzymania i oczekiwania, który stanowi wymaganie wstępne dla zakleszczenia związanego z oczekiwaniem cyklicznym. Dzięki temu nie ma możliwości wystąpienia zakleszczenia w systemie korzystającym wyłącznie z reguł planowania w roli podstawowego mechanizmu blokowania.