Skyframe StateMachines Kılavuzu

Sorun bildirme Kaynağı görüntüleme Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Genel Bakış

Skyframe StateMachine, yığınta bulunan yapısı bozulmuş bir işlev nesnesidir. Gerekli değerler hemen kullanılamadığında ancak eşzamansız olarak hesaplandığında esnek ve yedeklemesiz değerlendirmeyi1 destekler. StateMachine, beklerken bir mesaj dizisi kaynağını bağlayamaz. Bunun yerine askıya alınıp devam ettirilmelidir. Bu nedenle, yapı sökme işlemi, önceki hesaplamaların atlanması için açık yeniden giriş noktalarını gösterir.

StateMachine'ler; dizileri, dalları ve yapılandırılmış mantıksal eşzamanlılığı ifade etmek için kullanılabilir ve Skyframe etkileşimi için özel olarak özelleştirilir. StateMachine'ler daha büyük StateMachine'ler halinde oluşturulabilir ve alt StateMachine'leri paylaşabilir. Eşzamanlılık, yapısı gereği her zaman hiyerarşik ve tamamen mantıklıdır. Eşzamanlı her alt görev, paylaşılan tek bir üst SkyFunction iş parçacığında çalışır.

Giriş

Bu bölümde, java.com.google.devtools.build.skyframe.state paketinde bulunan StateMachine'ler kısaca açıklanmaktadır.

Skyframe'in yeniden başlatılmasına kısa bir giriş

Skyframe, bağımlılık grafiklerinin paralel değerlendirmesini yapan bir çerçevedir. Grafikteki her düğüm, parametrelerini belirten bir SkyKey ve sonucunu belirten bir SkyValue içeren bir SkyFunction'ın değerlendirmesine karşılık gelir. Hesaplama modeli, bir SkyFunction'ın SkyKey'ye göre SkyValue'ları arayabileceği şekildedir. Bu da ek SkyFunction'ların yinelemeli, paralel değerlendirmesini tetikler. İstekte bulunan SkyValue, hesaplamanın bir alt grafiği tamamlanmadığı için iş parçacığı bağlamak yerine bir null getValue yanıtı gözlemler ve eksik girişler nedeniyle tamamlanmadığını belirten bir SkyValue yerine null yanıtı döndürür. Skyframe, daha önce istenen tüm SkyValues kullanılabilir hale geldiğinde SkyFunctions'ı yeniden başlatır.

SkyKeyComputeState kullanıma sunulmadan önce, yeniden başlatma işlemini yönetmek için kullanılan geleneksel yöntem, hesaplamayı tamamen yeniden çalıştırmaktı. Bu yöntemin karmaşıklığı ikinci dereceden olsa da bu şekilde yazılan işlevler sonunda tamamlanır. Bunun nedeni, her yeniden çalıştırma işleminde daha az aramanın null döndürmesidir. SkyKeyComputeState ile manuel olarak belirtilen kontrol noktası verilerini bir SkyFunction ile ilişkilendirerek önemli ölçüde yeniden hesaplama tasarrufu elde edilebilir.

StateMachine, SkyKeyComputeState içinde bulunan ve SkyFunction yeniden başlatıldığında (SkyKeyComputeState'ın önbellekten çıkmadığı varsayılarak) askıya alma ve devam ettirme yürütme kancalarını göstererek neredeyse tüm yeniden hesaplamayı ortadan kaldıran nesnelerdir.

SkyKeyComputeState içindeki durum bilgisine sahip hesaplamalar

Nesne odaklı tasarım açısından, saf veri değerleri yerine işlem nesnelerini SkyKeyComputeState içinde depolamayı düşünebilirsiniz. Java'da, nesneyi taşıyan bir davranışın en basit tanımı işlevsel bir arayüzdür ve bu kişinin yeterli olduğu ortaya çıkar. StateMachine, aşağıdaki ilginç şekilde yinelenen tanımı2 içerir.

@FunctionalInterface
public interface StateMachine {
  StateMachine step(Tasks tasks) throws InterruptedException;
}

Tasks arayüzü SkyFunction.Environment ile benzerdir ancak eşzamansızlık için tasarlanmıştır ve mantıksal olarak eşzamanlı alt görevler için destek ekler3.

step işlevinin döndürdüğü değer başka bir StateMachine işlevidir ve bir adım dizisinin indüktif olarak belirtilmesine olanak tanır. step, StateMachine tamamlandığında DONE değerini döndürür. Örneğin:

class HelloWorld implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    System.out.println("hello");
    return this::step2;  // The next step is HelloWorld.step2.
  }

  private StateMachine step2(Tasks tasks) {
     System.out.println("world");
     // DONE is special value defined in the `StateMachine` interface signaling
     // that the computation is done.
     return DONE;
  }
}

aşağıdaki çıkışı içeren bir StateMachine'ü tanımlar.

hello
world

step2, StateMachine'un işlevsel arayüz tanımını karşıladığı için this::step2 yöntem referansının da StateMachine olduğunu unutmayın. Yöntem referansları, bir StateMachine içinde sonraki durumu belirtmenin en yaygın yoludur.

Askıya alma ve devam ettirme

Bir hesaplamayı monolitik bir işlev yerine StateMachine adıma bölmek, hesaplamayı askıya almak ve devam ettirmek için gereken bağlantı noktalarını sağlar. StateMachine.step döndürüldüğünde açık bir askıya alma noktası vardır. Döndürülen StateMachine değeri tarafından belirtilen devam, açık bir devam noktasıdır. Hesaplama tam olarak kaldığı yerden devam ettirilebildiği için yeniden hesaplama yapılması önlenebilir.

Geri çağırma, devam ettirme ve eşzamansız hesaplama

Teknik açıdan bakıldığında StateMachine, yürütülecek sonraki hesaplamayı belirleyen devamlılık görevi görür. StateMachine, engellemek yerine step işlevinden dönerek gönüllü olarak askıya alabilir. Bu durumda, kontrol bir Driver örneğine geri aktarılır. Driver, hazır bir StateMachine'e geçebilir veya kontrolü Skyframe'a geri verebilir.

Geleneksel olarak geri çağırma ve devam işlemleri tek bir kavram altında birleştirilir. Ancak StateMachine öğeleri arasında bir ayrım vardır.

  • Geri çağırma: Asenkron bir hesaplamanın sonucunun nereye depolanacağını tanımlar.
  • Devam: Sonraki yürütme durumunu belirtir.

Eşzamansız bir işlem çağrılırken geri çağırma işlevi gereklidir. Bu, SkyValue araması durumunda olduğu gibi, gerçek işlemin yöntem çağrıldıktan hemen sonra gerçekleşmediği anlamına gelir. Geri çağırma işlevleri mümkün olduğunca basit tutulmalıdır.

Devamlar, StateMachine işlevlerinin StateMachine döndürdüğü değerlerdir ve tüm asenkron hesaplamalar çözüldükten sonra gelen karmaşık yürütmeyi kapsar. Bu yapılandırılmış yaklaşım, geri çağırma işlemlerinin karmaşıklığını yönetilebilir düzeyde tutmaya yardımcı olur.

Görevler

Tasks arayüzü, StateMachine'lara SkyKey'e göre SkyValue'ları aramak ve eşzamanlı alt görevleri planlamak için bir API sağlar.

interface Tasks {
  void enqueue(StateMachine subtask);

  void lookUp(SkyKey key, Consumer<SkyValue> sink);

  <E extends Exception>
  void lookUp(SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);

  // lookUp overloads for 2 and 3 exception types exist, but are elided here.
}

SkyValue aramaları

StateMachines, SkyValues'ı aramak için Tasks.lookUp aşırı yüklemelerini kullanır. Bunlar SkyFunction.Environment.getValue ve SkyFunction.Environment.getValueOrThrow ile benzerdir ve istisna işleme anlamında benzer şekilde yer alır. Uygulama, aramayı hemen gerçekleştirmez. Bunun yerine, aramayı yapmadan önce mümkün olduğunca çok sayıda aramayı4 gruplandırır. Değer hemen kullanılamayabilir (ör. Skyframe'ın yeniden başlatılmasını gerektirebilir). Bu nedenle, arayan, geri çağırma işlevi kullanarak elde edilen değerle ne yapılacağını belirtir.

StateMachine işleyicisi (Driver'ler ve SkyFrame'a köprü oluşturma), değerin bir sonraki durum başlamadan önce kullanılabileceğini garanti eder. Aşağıda bir örnek verilmiştir.

class DoesLookup implements StateMachine, Consumer<SkyValue> {
  private Value value;

  @Override
  public StateMachine step(Tasks tasks) {
    tasks.lookUp(new Key(), (Consumer<SkyValue>) this);
    return this::processValue;
  }

  // The `lookUp` call in `step` causes this to be called before `processValue`.
  @Override  // Implementation of Consumer<SkyValue>.
  public void accept(SkyValue value) {
    this.value = (Value)value;
  }

  private StateMachine processValue(Tasks tasks) {
    System.out.println(value);  // Prints the string representation of `value`.
    return DONE;
  }
}

Yukarıdaki örnekte ilk adım, this'u tüketici olarak ileterek new Key() için bir arama yapar. Bunun nedeni, DoesLookup'ün Consumer<SkyValue>'ü uygulamasıdır.

Sözleşme uyarınca, bir sonraki DoesLookup.processValue eyaleti başlamadan önce DoesLookup.step ile ilgili tüm aramalar tamamlanmıştır. Bu nedenle, value, processValue'te erişildiğinde kullanılabilir.

Alt görevler

Tasks.enqueue, mantıksal olarak eşzamanlı alt görevlerin yürütülmesini ister. Alt görevler de StateMachine'dir ve normal StateMachine'lerin yapabileceği her şeyi yapabilir (ör. yinelemeli olarak daha fazla alt görev oluşturma veya SkyValues'ı arama). lookUp ile benzer şekilde durum makinesi sürücüsü, bir sonraki adıma geçmeden önce tüm alt görevlerin tamamlanmasını sağlar. Aşağıda bir örnek verilmiştir.

class Subtasks implements StateMachine {
  private int i = 0;

  @Override
  public StateMachine step(Tasks tasks) {
    tasks.enqueue(new Subtask1());
    tasks.enqueue(new Subtask2());
    // The next step is Subtasks.processResults. It won't be called until both
    // Subtask1 and Subtask 2 are complete.
    return this::processResults;
  }

  private StateMachine processResults(Tasks tasks) {
    System.out.println(i);  // Prints "3".
    return DONE;  // Subtasks is done.
  }

  private class Subtask1 implements StateMachine {
    @Override
    public StateMachine step(Tasks tasks) {
      i += 1;
      return DONE;  // Subtask1 is done.
    }
  }

  private class Subtask2 implements StateMachine {
    @Override
    public StateMachine step(Tasks tasks) {
      i += 2;
      return DONE;  // Subtask2 is done.
    }
  }
}

Subtask1 ve Subtask2 mantıksal olarak eşzamanlı olsa da her şey tek bir iş parçacığında çalıştığından i'nin "eşzamanlı" güncellemesi için senkronizasyon gerekmez.

Yapılandırılmış eşzamanlılık

Her lookUp ve enqueue bir sonraki eyalete geçmeden önce çözümlenmesi gerektiğinden bu, eşzamanlılığın doğal olarak ağaç yapılarıyla sınırlı olduğu anlamına gelir. Aşağıdaki örnekte gösterildiği gibi hiyerarşik5 eşzamanlılık oluşturmak mümkündür.

Yapılandırılmış Eşzamanlılık

UML'den eşzamanlılık yapısının bir ağaç oluşturduğunu anlamak zordur. Ağaç yapısını daha iyi gösteren bir alternatif görünüm vardır.

Yapılandırılmamış Eşzamanlılık

Yapılandırılmış eşzamanlılığın mantığını anlamak çok daha kolaydır.

Kompozisyon ve kontrol akışı kalıpları

Bu bölümde, birden fazla StateMachine'ün nasıl derlenebileceğine dair örnekler ve belirli kontrol akışı sorunlarına yönelik çözümler sunulmaktadır.

Sıralı durumlar

Bu, en yaygın ve basit kontrol akışı kalıbıdır. Buna örnek olarak SkyKeyComputeState içindeki duruma bağlı hesaplamalar bölümünü inceleyebilirsiniz.

Dallanma

StateMachine'lerde dallanma durumlarına, aşağıdaki örnekte gösterildiği gibi normal Java kontrol akışı kullanılarak farklı değerler döndürülerek ulaşılabilir.

class Branch implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    // Returns different state machines, depending on condition.
    if (shouldUseA()) {
      return this::performA;
    }
    return this::performB;
  }
  
}

Belirli şubelerin erken tamamlama için DONE döndürmesi çok yaygındır.

Gelişmiş sıralı bileşim

StateMachine kontrol yapısı bellek içermediğinden, StateMachine tanımlarını alt görev olarak paylaşmak bazen zor olabilir. M1 ve M2, S adlı bir StateMachine paylaşan StateMachine örnekleri olsun. M1 ve M2 sırasıyla <A, S, B> ve <X, S, Y> sıralı dizilerdir. Sorun, S'nin tamamlandıktan sonra B'ye mi yoksa Y'ye mi devam edeceğini bilmemesi ve StateMachine'lerin çağrı yığınını tam olarak tutmaması. Bu bölümde, bu amaca ulaşmak için kullanılabilecek bazı teknikler incelenmektedir.

Terminal dizisi öğesi olarak StateMachine

Bu işlem, ortaya atılan ilk sorunu çözmez. Yalnızca paylaşılan StateMachine sırayla terminal olduğunda sıralı kompozisyonu gösterir.

// S is the shared state machine.
class S implements StateMachine {  }

class M1 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performA();
    return new S();
  }
}

class M2 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performX();
    return new S();
  }
}

Bu, S'nin kendisi karmaşık bir durum makinesi olsa bile çalışır.

Sıralı kompozisyon için alt görev

Sıraya eklenen alt görevlerin sonraki durumdan önce tamamlanacağı garanti edildiğinden, bazen alt görev mekanizmasından biraz6 kötüye yararlanmak mümkündür.

class M1 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performA();
    // S starts after `step` returns and by contract must complete before `doB`
    // begins. It is effectively sequential, inducing the sequence < A, S, B >.
    tasks.enqueue(new S());
    return this::doB;
  }

  private StateMachine doB(Tasks tasks) {
    performB();
    return DONE;
  }
}

class M2 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performX();
    // Similarly, this induces the sequence < X, S, Y>.
    tasks.enqueue(new S());
    return this::doY;
  }

  private StateMachine doY(Tasks tasks) {
    performY();
    return DONE;
  }
}

runAfter yerleştirme

Bazen S yürütülmeden önce tamamlanması gereken başka paralel alt görevler veya Tasks.lookUp çağrıları olduğu için Tasks.enqueue'ten kötüye yararlanmak mümkün değildir. Bu durumda, S'ye bir runAfter parametresi ekleyerek S'yi ne yapacağını bildirebilirsiniz.

class S implements StateMachine {
  // Specifies what to run after S completes.
  private final StateMachine runAfter;

  @Override
  public StateMachine step(Tasks tasks) {
     // Performs some computations.
    return this::processResults;
  }

  @Nullable
  private StateMachine processResults(Tasks tasks) {
     // Does some additional processing.

    // Executes the state machine defined by `runAfter` after S completes.
    return runAfter;
  }
}

class M1 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performA();
    // Passes `this::doB` as the `runAfter` parameter of S, resulting in the
    // sequence < A, S, B >.
    return new S(/* runAfter= */ this::doB);
  }

  private StateMachine doB(Tasks tasks) {
    performB();
    return DONE;
  }
}

class M2 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performX();
    // Passes `this::doY` as the `runAfter` parameter of S, resulting in the
    // sequence < X, S, Y >.
    return new S(/* runAfter= */ this::doY);
  }

  private StateMachine doY(Tasks tasks) {
    performY();
    return DONE;
  }
}

Bu yaklaşım, alt görevleri kötüye kullanmaktan daha nettir. Bununla birlikte, örneğin birden fazla StateMachine öğesini runAfter ile iç içe yerleştirerek bu işlemi olabildiğince özgürce uygulamak, Callback Hell'e giden yolda ilerler. Bunun yerine, sıralı runAfter'leri sıradan sıralı durumlarla bölmek daha iyidir.

  return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))

şunlarla değiştirilebilir.

  private StateMachine step1(Tasks tasks) {
     doStep1();
     return new S(/* runAfter= */ this::intermediateStep);
  }

  private StateMachine intermediateStep(Tasks tasks) {
    return new T(/* runAfter= */ this::nextStep);
  }

Yasaklanmış alternatif: runAfterUnlessError

Önceki bir taslakta, hataları erkenden sonlandıracak bir runAfterUnlessError'yi değerlendirmiştik. Bunun nedeni, hataların genellikle iki kez kontrol edilmesidir. Hatalar bir kez runAfter referansı olan StateMachine tarafından, bir kez de runAfter makinesinin kendisi tarafından kontrol edilir.

Yaptığımız değerlendirmenin ardından, hata kontrolünü tekilleştirmekten daha önemli olanın kodun tekdüzeliği olduğuna karar verdik. runAfter mekanizmasının, her zaman hata kontrolü gerektiren tasks.enqueue mekanizmasıyla tutarlı bir şekilde çalışmaması kafa karıştırıcı olur.

Doğrudan yetki verme

Her resmi durum geçişinde ana Driver döngüsü ilerler. Sözleşme uyarınca, ilerleyen durumlar, daha önce sıraya eklenen tüm SkyValue aramalarının ve alt görevlerin bir sonraki durum yürütülmeden önce çözülmesi anlamına gelir. Bazen yetki verilen bir StateMachine mantığı, aşama ilerlemesini gereksiz veya olumsuz hale getirir. Örneğin, temsilcinin ilk step'ü, yetki veren eyaletin aramalarıyla paralelleştirilebilecek SkyKey aramaları gerçekleştiriyorsa aşama ilerletme, bu aramaları sıralı hale getirir. Aşağıdaki örnekte gösterildiği gibi doğrudan yetki verme işlemini gerçekleştirmek daha mantıklı olabilir.

class Parent implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks ) {
    tasks.lookUp(new Key1(), this);
    // Directly delegates to `Delegate`.
    //
    // The (valid) alternative:
    //   return new Delegate(this::afterDelegation);
    // would cause `Delegate.step` to execute after `step` completes which would
    // cause lookups of `Key1` and `Key2` to be sequential instead of parallel.
    return new Delegate(this::afterDelegation).step(tasks);
  }

  private StateMachine afterDelegation(Tasks tasks) {
    
  }
}

class Delegate implements StateMachine {
  private final StateMachine runAfter;

  Delegate(StateMachine runAfter) {
    this.runAfter = runAfter;
  }

  @Override
  public StateMachine step(Tasks tasks) {
    tasks.lookUp(new Key2(), this);
    return ;
  }

  // Rest of implementation.
  

  private StateMachine complete(Tasks tasks) {
    
    return runAfter;
  }
}

Veri akışı

Bir önceki tartışmada kontrol akışının yönetilmesine odaklandık. Bu bölümde, veri değerlerinin yayılması açıklanmaktadır.

Tasks.lookUp geri aramalarını uygulama

SkyValue aramalarında Tasks.lookUp geri çağırma işlevinin uygulanmasına dair bir örnek verilmiştir. Bu bölümde, birden fazla SkyValue'ı işlemeyle ilgili gerekçeler sunulmakta ve yaklaşımlar önerilmektedir.

Tasks.lookUp geri aramaları

Tasks.lookUp yöntemi, parametre olarak bir geri çağırma işlevi (sink) alır.

  void lookUp(SkyKey key, Consumer<SkyValue> sink);

Bu işlemi gerçekleştirmek için idiomatik yaklaşım, bir Java lambda kullanmaktır:

  tasks.lookUp(key, value -> myValue = (MyValueClass)value);

myValue, aramayı yapan StateMachine örneğinin bir üye değişkenidir. Ancak lambda, StateMachine uygulamasında Consumer<SkyValue> arayüzünü uygulamaya kıyasla ek bellek ayırma gerektirir. Birden fazla arama olduğunda lambda işlevi yine de yararlı olur.

Tasks.lookUp için SkyFunction.Environment.getValueOrThrow'a benzer hata işleme aşırı yüklemeleri de vardır.

  <E extends Exception> void lookUp(
      SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);

  interface ValueOrExceptionSink<E extends Exception> {
    void acceptValueOrException(@Nullable SkyValue value, @Nullable E exception);
  }

Aşağıda örnek bir uygulama gösterilmektedir.

class PerformLookupWithError extends StateMachine, ValueOrExceptionSink<MyException> {
  private MyValue value;
  private MyException error;

  @Override
  public StateMachine step(Tasks tasks) {
    tasks.lookUp(new MyKey(), MyException.class, ValueOrExceptionSink<MyException>) this);
    return this::processResult;
  }

  @Override
  public acceptValueOrException(@Nullable SkyValue value, @Nullable MyException exception) {
    if (value != null) {
      this.value = (MyValue)value;
      return;
    }
    if (exception != null) {
      this.error = exception;
      return;
    }
    throw new IllegalArgumentException("Both parameters were unexpectedly null.");
  }

  private StateMachine processResult(Tasks tasks) {
    if (exception != null) {
      // Handles the error.
      
      return DONE;
    }
    // Processes `value`, which is non-null.
    
  }
}

Hata işleme içermeyen aramalarda olduğu gibi, geri çağırmanın doğrudan StateMachine sınıfının uygulanması, lamba için bellek ayırmadan tasarruf sağlar.

Hata işleme biraz daha ayrıntılı bilgi sağlar ancak temelde hataların yayılması ile normal değerler arasında çok fazla fark yoktur.

Birden çok SkyValues kullanma

Genellikle birden fazla SkyValue araması gerekir. Çoğu zaman işe yarayan bir yaklaşım, SkyValue türünü etkinleştirmektir. Aşağıda, prototip üretim kodundan basitleştirilmiş bir örnek verilmiştir.

  @Nullable
  private StateMachine fetchConfigurationAndPackage(Tasks tasks) {
    var configurationKey = configuredTarget.getConfigurationKey();
    if (configurationKey != null) {
      tasks.lookUp(configurationKey, (Consumer<SkyValue>) this);
    }

    var packageId = configuredTarget.getLabel().getPackageIdentifier();
    tasks.lookUp(PackageValue.key(packageId), (Consumer<SkyValue>) this);

    return this::constructResult;
  }

  @Override  // Implementation of `Consumer<SkyValue>`.
  public void accept(SkyValue value) {
    if (value instanceof BuildConfigurationValue) {
      this.configurationValue = (BuildConfigurationValue) value;
      return;
    }
    if (value instanceof PackageValue) {
      this.pkg = ((PackageValue) value).getPackage();
      return;
    }
    throw new IllegalArgumentException("unexpected value: " + value);
  }

Değer türleri farklı olduğundan Consumer<SkyValue> geri çağırma işlevi uygulaması net bir şekilde paylaşılabilir. Bu durum söz konusu değilse lambda tabanlı uygulamalara veya uygun geri çağırma işlevlerini uygulayan tam iç sınıf örneklerine geri dönmek uygundur.

Değerleri StateMachine'ler arasında yayma

Şu ana kadar bu dokümanda yalnızca bir alt görevdeki işlerin nasıl düzenleneceği açıklanmıştı, ancak alt görevlerin çağrı yapana bir değer bildirmesi de gerekiyor. Alt görevler mantıksal olarak eşzamansız olduğundan sonuçları, bir geri çağırma yoluyla arayana geri iletilir. Bunun işe yaraması için alt görev, kurucusu aracılığıyla enjekte edilen bir akıtma arayüzü tanımlar.

class BarProducer implements StateMachine {
  // Callers of BarProducer implement the following interface to accept its
  // results. Exactly one of the two methods will be called by the time
  // BarProducer completes.
  interface ResultSink {
    void acceptBarValue(Bar value);
    void acceptBarError(BarException exception);
  }

  private final ResultSink sink;

  BarProducer(ResultSink sink) {
     this.sink = sink;
  }

   // StateMachine steps that end with this::complete.

  private StateMachine complete(Tasks tasks) {
    if (hasError()) {
      sink.acceptBarError(getError());
      return DONE;
    }
    sink.acceptBarValue(getValue());
    return DONE;
  }
}

StateMachine kimlikli bir arayanın görüntüsü aşağıdaki gibi olur.

class Caller implements StateMachine, BarProducer.ResultSink {
  interface ResultSink {
    void acceptCallerValue(Bar value);
    void acceptCallerError(BarException error);
  }

  private final ResultSink sink;

  private Bar value;

  Caller(ResultSink sink) {
    this.sink = sink;
  }

  @Override
  @Nullable
  public StateMachine step(Tasks tasks) {
    tasks.enqueue(new BarProducer((BarProducer.ResultSink) this));
    return this::processResult;
  }

  @Override
  public void acceptBarValue(Bar value) {
    this.value = value;
  }

  @Override
  public void acceptBarError(BarException error) {
    sink.acceptCallerError(error);
  }

  private StateMachine processResult(Tasks tasks) {
    // Since all enqueued subtasks resolve before `processResult` starts, one of
    // the `BarResultSink` callbacks must have been called by this point.
    if (value == null) {
      return DONE;  // There was a previously reported error.
    }
    var finalResult = computeResult(value);
    sink.acceptCallerValue(finalResult);
    return DONE;
  }
}

Yukarıdaki örnekte birkaç şey açıklanmaktadır. Caller, sonuçlarını geri yaymalı ve kendi Caller.ResultSink'unu tanımlamalıdır. Caller, BarProducer.ResultSink geri çağırma işlevlerini uygular. processResult, devam ettirildikten sonra bir hata oluşup oluşmadığını belirlemek için value öğesinin null olup olmadığını kontrol eder. Bu, bir alt görev veya SkyValue aramasının çıkışını kabul ettikten sonra yaygın olarak görülen bir davranış kalıbıdır.

acceptBarError'ün uygulanmasının, Hata kabartması gereği olarak sonucu Caller.ResultSink'e hemen ilettiğini unutmayın.

Üst düzey StateMachine'ler için alternatifler Driver'ler ve SkyFunctions'a köprü oluşturma bölümünde açıklanmıştır.

Hata işleme

Tasks.lookUp geri çağırmalarında ve StateMachines arasında değerleri aktarma bölümünde hata işlemeyle ilgili birkaç örnek bulunmaktadır. InterruptedException dışındaki istisnalar atılmaz, bunun yerine geri çağırma işlevleri aracılığıyla değer olarak iletilir. Bu tür geri çağırmalarda genellikle değer veya hatalardan yalnızca biri ile birlikte özel veya semantik kullanılır.

Sonraki bölümde, Skyframe hata işleme ile ilgili ince ama önemli bir etkileşim açıklanmaktadır.

Hata bulaştırma (--nokeep_going)

Hata kabarcıklaşması sırasında, istenen tüm SkyValues mevcut olmasa bile bir SkyFunction yeniden başlatılabilir. Böyle durumlarda, Tasks API sözleşmesi nedeniyle sonraki duruma hiçbir zaman ulaşılamaz. Ancak StateMachine yine de istisnayı yaymalıdır.

Yayma işlemi, bir sonraki duruma ulaşılıp ulaşılmadığına bakılmaksızın gerçekleşmesi gerektiğinden, hata işleme geri çağırma işlevi bu görevi gerçekleştirmelidir. İç StateMachine için bu, üst geri çağırma işlevi çağrılarak yapılır.

SkyFunction ile arayüz oluşturan üst düzey StateMachine'te bu, ValueOrExceptionProducer sınıfının setException yöntemi çağrılarak yapılabilir. ValueOrExceptionProducer.tryProduceValue, SkyValues eksik olsa bile istisnayı atar.

Doğrudan bir Driver kullanılıyorsa makinenin işlemeyi tamamlamamış olması bile SkyFunction'dan yayılan hataları kontrol etmeyi zorunlu kılar.

Etkinlik İşleme

Etkinlik yayınlaması gereken SkyFunctions için SkyKeyComputeState'e bir StoredEventHandler eklenir ve daha sonra bunları gerektiren StateMachine'lara eklenir. Geçmişte, SkyFrame'in tekrar oynatılmadığı bazı etkinlikleri bırakması nedeniyle StoredEventHandler gerekliydi. Ancak bu durum daha sonra düzeltildi. Hata işleme geri çağırmalarından yayınlanan etkinliklerin uygulanmasını basitleştirdiği için StoredEventHandler ekleme korunur.

Driver'ler ve SkyFunctions'a köprü oluşturma

Belirtilen bir kök StateMachine ile başlayan StateMachine'ların yürütülmesini yönetmekten Driver sorumludur. StateMachine öğeleri, alt görevleri (StateMachine) yinelemeli olarak sıraya koyabileceğinden, tek bir Driver çok sayıda alt görevi yönetebilir. Bu alt görevler, yapılandırılmış eşzamanlılığın bir sonucu olarak ağaç yapısı oluşturur. Driver, daha iyi verimlilik için alt görevler arasında SkyValue aramalarını gruplandırır.

Aşağıdaki API'yi kullanarak Driver etrafında oluşturulmuş çok sayıda sınıf bulunmaktadır.

public final class Driver {
  public Driver(StateMachine root);
  public boolean drive(SkyFunction.Environment env) throws InterruptedException;
}

Driver, parametre olarak tek bir kök StateMachine alır. Driver.drive çağrısı, Skyframe yeniden başlatılmadan StateMachine'ı mümkün olduğunca yürütür. StateMachine tamamlandığında "doğru", aksi takdirde, tüm değerlerin kullanılamadığını belirterek "yanlış" değerini döndürür.

Driver, StateMachine'un eşzamanlı durumunu korur ve SkyKeyComputeState'e yerleştirilmeye uygundur.

Driver sınıfını doğrudan örneklendirme

StateMachine uygulamaları, sonuçlarını genellikle geri çağırma işlevleri aracılığıyla iletir. Aşağıdaki örnekte gösterildiği gibi, bir Driver öğesini doğrudan örneklendirmek mümkündür.

Driver, biraz aşağıda tanımlanacak ilgili ResultSink uygulamasıyla birlikte SkyKeyComputeState uygulamasına yerleştirilir. Üst düzeyde, State nesnesi Driver'ten daha uzun ömürlü olacağından hesaplama sonucu için uygun bir alıcıdır.

class State implements SkyKeyComputeState, ResultProducer.ResultSink {
  // The `Driver` instance, containing the full tree of all `StateMachine`
  // states. Responsible for calling `StateMachine.step` implementations when
  // asynchronous values are available and performing batched SkyFrame lookups.
  //
  // Non-null while `result` is being computed.
  private Driver resultProducer;

  // Variable for storing the result of the `StateMachine`
  //
  // Will be non-null after the computation completes.
  //
  private ResultType result;

  // Implements `ResultProducer.ResultSink`.
  //
  // `ResultProducer` propagates its final value through a callback that is
  // implemented here.
  @Override
  public void acceptResult(ResultType result) {
    this.result = result;
  }
}

Aşağıdaki kod, ResultProducer değerini özetler.

class ResultProducer implements StateMachine {
  interface ResultSink {
    void acceptResult(ResultType value);
  }

  private final Parameters parameters;
  private final ResultSink sink;

   // Other internal state.

  ResultProducer(Parameters parameters, ResultSink sink) {
    this.parameters = parameters;
    this.sink = sink;
  }

  @Override
  public StateMachine step(Tasks tasks) {
      // Implementation.
    return this::complete;
  }

  private StateMachine complete(Tasks tasks) {
    sink.acceptResult(getResult());
    return DONE;
  }
}

Sonuçları tembel bir şekilde hesaplamak için kullanabileceğiniz kod aşağıdaki gibi olabilir.

@Nullable
private Result computeResult(State state, Skyfunction.Environment env)
    throws InterruptedException {
  if (state.result != null) {
    return state.result;
  }
  if (state.resultProducer == null) {
    state.resultProducer = new Driver(new ResultProducer(
      new Parameters(), (ResultProducer.ResultSink)state));
  }
  if (state.resultProducer.drive(env)) {
    // Clears the `Driver` instance as it is no longer needed.
    state.resultProducer = null;
  }
  return state.result;
}

Driver'ü yerleştirme

StateMachine bir değer oluşturur ve istisna oluşturmazsa aşağıdaki örnekte gösterildiği gibi Driver'i yerleştirmek de olası bir uygulamadır.

class ResultProducer implements StateMachine {
  private final Parameters parameters;
  private final Driver driver;

  private ResultType result;

  ResultProducer(Parameters parameters) {
    this.parameters = parameters;
    this.driver = new Driver(this);
  }

  @Nullable  // Null when a Skyframe restart is needed.
  public ResultType tryProduceValue( SkyFunction.Environment env)
      throws InterruptedException {
    if (!driver.drive(env)) {
      return null;
    }
    return result;
  }

  @Override
  public StateMachine step(Tasks tasks) {
      // Implementation.
}

SkyFunction'da aşağıdaki gibi görünen bir kod olabilir (State, SkyKeyComputeState işlevine özgü türdür).

@Nullable  // Null when a Skyframe restart is needed.
Result computeResult(SkyFunction.Environment env, State state)
    throws InterruptedException {
  if (state.result != null) {
    return state.result;
  }
  if (state.resultProducer == null) {
    state.resultProducer = new ResultProducer(new Parameters());
  }
  var result = state.resultProducer.tryProduceValue(env);
  if (result == null) {
    return null;
  }
  state.resultProducer = null;
  return state.result = result;
}

DriverStateMachine uygulamasına yerleştirmek, Skyframe'ın eşzamanlı kodlama stili için daha uygundur.

İstisna oluşturabilecek StateMachine'ler

Aksi takdirde, eşzamanlı SkyFunction koduyla eşleşecek eşzamanlı API'lere sahip SkyKeyComputeStateyerleştirilebilir ValueOrExceptionProducer ve ValueOrException2Producer sınıfları bulunur.

ValueOrExceptionProducer soyut sınıfı aşağıdaki yöntemleri içerir.

public abstract class ValueOrExceptionProducer<V, E extends Exception>
    implements StateMachine {
  @Nullable
  public final V tryProduceValue(Environment env)
      throws InterruptedException, E {
      // Implementation.
  }

  protected final void setValue(V value)  {   // Implementation. }
  protected final void setException(E exception) {   // Implementation. }
}

Yerleştirilmiş bir Driver örneği içerir ve Yerleştirme sürücüsündeki ResultProducer sınıfına çok benzer. SkyFunction ile benzer bir şekilde arayüz oluşturur. Bir ResultSink tanımlamak yerine, bu durumlardan biri gerçekleştiğinde uygulama setValue veya setException çağrısı yapar. Her ikisi de gerçekleştiğinde istisna öncelikli olur. tryProduceValue yöntemi, eşzamansız geri çağırma kodunu eşzamanlı koda bağlar ve bu kod oluşturulduğunda bir istisna uygular.

Daha önce de belirtildiği gibi, hata kabarcıklaşması sırasında, tüm girişler mevcut olmadığı için makine henüz çalışmayı tamamlamamış olsa bile hata oluşabilir. Bu durumu karşılamak için tryProduceValue, makine tamamlanmadan önce bile ayarlanmış istisnaları atar.

Son söz: Geri çağırmaları sonunda kaldırma

StateMachine'ler, asenkron hesaplama yapmak için son derece etkili ancak şablon yoğun bir yoldur. Bazel kodunun belirli bölümlerinde devamlılıklar (özellikle de ListenableFuture'a iletilen Runnable) içerikler yaygındır ancak SkyFunctions analizinde yaygın değildir. Analiz çoğunlukla CPU'ya bağlıdır ve disk I/O için verimli eşzamansız API'ler yoktur. Sonunda, öğrenme eğrisi olduğundan ve okunabilirliği engellediğinden geri aramaları optimize etmek iyi olur.

En umut verici alternatiflerden biri Java sanal iş parçacıklarıdır. Geri çağırma yazmak yerine her şey senkronize, engelleyen çağrılarla değiştirilir. Bunun nedeni, platform iş parçacığının aksine sanal iş parçacığı kaynağının bağlamanın ucuz olmasıdır. Ancak sanal mesaj dizileri kullanılsa bile basit senkronize işlemlerin mesaj dizisi oluşturma ve senkronizasyon primitifleriyle değiştirilmesi çok pahalıdır. StateMachine'lerden Java sanal iş parçacıklarına geçiş yaptık. Bu iş parçacıklar çok daha yavaştı ve uçtan uca analiz gecikmesinde neredeyse 3 kat artışa neden oldu. Sanal mesaj dizileri hâlâ önizleme aşamasında olduğundan bu taşıma işlemi, performansın arttığı daha sonraki bir tarihte gerçekleştirilebilir.

Kullanabileceğiniz başka bir yaklaşım da Loom coroutine'lerini (eğer kullanıma sunulursa) beklemektir. Buradaki avantaj, ortak çoklu görev özelliğini kullanarak senkronizasyon yükü azaltılabilmesidir.

Hiçbiri işe yaramazsa düşük düzeyli bayt kodunu yeniden yazma da uygun bir alternatif olabilir. Yeterli optimizasyonla, manuel olarak yazılmış geri çağırma koduna yaklaşan bir performans elde etmek mümkün olabilir.

Ek

Geri Arama Cehennemi

Geri çağırma cehennemi, geri çağırma kullanan eşzamansız kodlarda karşılaşılan kötü bir sorundur. Bu durum, sonraki bir adımın devamının önceki adıma yerleştirilmesinden kaynaklanır. Çok sayıda adım varsa bu iç içe yerleştirme işlemi son derece derin olabilir. Kontrol akışı ile birlikte kullanıldığında kod yönetilemez hale gelir.

class CallbackHell implements StateMachine {
  @Override
  public StateMachine step(Tasks task) {
    doA();
    return (t, l) -> {
      doB();
      return (t1, l2) -> {
        doC();
        return DONE;
      };
    };
  }
}

İç içe yerleştirilmiş uygulamaların avantajlarından biri, dış adımın yığın çerçevesinin korunabilmesidir. Java'da yakalanan lambda değişkenleri etkili bir şekilde nihai olmalıdır. Bu nedenle, bu tür değişkenleri kullanmak zahmetli olabilir. Aşağıda gösterildiği gibi yöntem referansları lambda yerine devam ettirme olarak döndürülerek derin iç içe yerleştirme önlenir.

class CallbackHellAvoided implements StateMachine {
  @Override
  public StateMachine step(Tasks task) {
    doA();
    return this::step2;
  }

  private StateMachine step2(Tasks tasks) {
    doB();
    return this::step3;
  }

  private StateMachine step3(Tasks tasks) {
    doC();
    return DONE;
  }
}

Geri çağırma cehennemi, runAfter enjeksiyonu kalıbı çok yoğun bir şekilde kullanıldığında da ortaya çıkabilir. Ancak enjeksiyonlar sıralı adımlarla dağıtılarak bu durum önlenebilir.

Örnek: Zincirli SkyValue aramaları

Uygulama mantığının, SkyValue aramalarının bağımlı zincirlerini gerektirmesi sık karşılaşılan bir durumdur. Örneğin, ikinci bir SkyKey ilk SkyValue'a bağlıysa. Bunu çok nazik bir şekilde düşünerek, karmaşık ve derinlemesine iç içe geçmiş bir geri çağırma yapısı oluşturabilirsiniz.

private ValueType1 value1;
private ValueType2 value2;

private StateMachine step1(...) {
  tasks.lookUp(key1, (Consumer<SkyValue>) this);  // key1 has type KeyType1.
  return this::step2;
}

@Override
public void accept(SkyValue value) {
  this.value1 = (ValueType1) value;
}

private StateMachine step2(...) {
  KeyType2 key2 = computeKey(value1);
  tasks.lookup(key2, this::acceptValueType2);
  return this::step3;
}

private void acceptValueType2(SkyValue value) {
  this.value2 = (ValueType2) value;
}

Ancak devamlar yöntem referansları olarak belirtildiğinden kod, durum geçişlerinde prosedürel görünür: step2, step1'dan sonra gelir. Burada value2 atamak için bir lambda kullanıldığını unutmayın. Bu, kodun sırasının hesaplamanın yukarıdan aşağıya doğru sıralamasıyla aynı olmasını sağlar.

Çeşitli İpuçları

Okunabilirlik: Yürütme Sırası

Okunabilirliği artırmak için StateMachine.step uygulamalarını yürütme sırasına ve geri çağırma uygulamalarını kodda iletildikleri yerin hemen sonrasına yerleştirmeye çalışın. Bu, kontrol akışının kollara yayıldığı durumlarda her zaman mümkün değildir. Bu gibi durumlarda ek yorumlar faydalı olabilir.

Örnek: Zincirli SkyValue aramaları bölümünde, bunu başarmak için bir ara yöntem referansı oluşturulur. Bu, okunabilirlik için küçük bir miktar performanstan ödün vermenizi gerektirir. Bu durumda bu durum muhtemelen faydalı olacaktır.

Kuşak Hipotezi

Orta ömürlü Java nesneleri, çok kısa süre yaşayan veya sonsuza kadar yaşayan nesneleri işlemek için tasarlanmış Java çöp toplayıcısının nesil hipotezini bozar. Tanımı gereği, SkyKeyComputeState içindeki nesneler bu hipotezi ihlal eder. Driver köklü, hâlâ çalışan tüm StateMachine'lerin oluşturulmuş ağacını içeren bu tür nesneler, asenkron hesaplamaların tamamlanmasını beklerken askıya alındığı için ara yaşam süresine sahiptir.

JDK19'da daha az kötü görünse de StateMachine kullandığınızda, oluşturulan gerçek çöplerde önemli düşüşler olsa bile GC süresinde artış gözlemlenebilir. StateMachine'lerin ömrü orta düzeyde olduğundan eski nesle yükseltilmeleri daha hızlı dolmasına neden olur ve bu nedenle, temizlemek için daha pahalı büyük veya tam GC'ler gerekir.

İlk önlem, StateMachine değişkenlerinin kullanımını en aza indirmektir. Ancak bu her zaman uygun değildir. Örneğin, birden fazla eyalette bir değer gerekiyorsa bu mümkün değildir. Mümkün olduğunda yerel yığın step değişkenleri genç nesil değişkenlerdir ve verimli bir şekilde GC'ye gönderilir.

StateMachine değişkenleri için işleri alt görevlere bölmek ve Değerleri StateMachine değerleri arasında yaymak için önerilen kalıbı uygulamak da faydalı olur. Kalıpta, yalnızca alt StateMachine öğelerinin üst StateMachine öğelerine referans verdiğini, bunun tersinin geçerli olmadığını unutmayın. Diğer bir deyişle, çocuklar sonuç geri çağırmalarını kullanarak ebeveynleri tamamlayıp güncelledikçe çocuklar doğal olarak kapsam dışına çıkar ve GC için uygun hale gelir.

Son olarak, bazı durumlarda, önceki durumlarda StateMachine değişkeni gerekir ancak sonraki durumlarda gerekmez. Büyük nesnelere artık ihtiyaç duyulmadığı bilindiğinde, referansları geçersiz kılmak faydalı olabilir.

Eyaletleri adlandırma

Bir yöntemi adlandırırken genellikle yöntemin içinde gerçekleşen davranışa göre adlandırmak mümkündür. Grup olmadığı için StateMachine'lerde bu işlemin nasıl yapılacağı daha net değildir. Örneğin, foo yönteminin bar alt yöntemini çağırdığını varsayalım. Bu, bir StateMachine dilinde, foo ve ardından bar durum sırasına çevrilebilir. foo artık bar davranışını içermiyor. Sonuç olarak, eyaletler için yöntem adları genellikle daha dar kapsamlı olur ve yerel davranışı yansıtabilir.

Eşzamanlılık ağaç şeması

Aşağıda, Yapılandırılmış eşzamanlılık bölümündeki diyagramın, ağaç yapısını daha iyi gösteren alternatif bir görünümü verilmiştir. Bloklar küçük bir ağaç oluşturuyor.

Yapılandırılmış Eşzamanlılık 3D


  1. Skyframe'ın, değerler mevcut olmadığında baştan başlatma kuralının aksine. 

  2. step işlevinin InterruptedException atmasına izin verildiğini ancak örneklerde bunun atlandığını unutmayın. Bazel kodunda bu istisnayı atan birkaç düşük düzey yöntem vardır ve bu istisna, daha sonra açıklanacak olan ve StateMachine'ı çalıştıran Driver'ye kadar yayılır. Gerekmediği durumlarda atılmayacağını belirtmemeniz sorun değildir. 

  3. Eş zamanlı alt görevler, her bağımlılık için bağımsız iş yürüten ConfiguredTargetFunction tarafından motive edilmiştir. Tüm bağımlılıkları aynı anda işleyen karmaşık veri yapılarını değiştirerek verimsizliklere yol açmak yerine, her bağımlılığın kendi bağımsız StateMachine'si vardır. 

  4. Tek bir adımdaki birden fazla tasks.lookUp çağrısı birlikte gruplandırılır. Eşzamanlı alt görevlerde gerçekleşen aramalar sayesinde ek toplu işlemler oluşturulabilir. 

  5. Bu, kavramsal olarak Java'nın yapılandırılmış eşzamanlılığına jeps/428 benzer. 

  6. Bu işlem, sıralı kompozisyon elde etmek için bir mesaj dizisi oluşturmaya ve bu mesaj dizisini birleştirmeye benzer.