Skyframe StateMachines Kılavuzu

Sorun bildir Kaynağı görüntüle Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Genel Bakış

Skyframe StateMachine, yığında bulunan yapısı bozulmuş bir işlev nesnesidir. Gerekli değerler hemen kullanılamadığında ancak eşzamansız olarak hesaplandığında esnek ve gereksiz olmayan değerlendirmeyi destekler1. StateMachine, beklerken bir iş parçacığı kaynağını bağlayamaz. Bunun yerine askıya alınması ve devam ettirilmesi gerekir. Bu nedenle, ayrıştırma işlemi, önceki hesaplamaların atlanabilmesi için açıkça yeniden giriş noktalarını ortaya çıkarır.

StateMachine'lar, sıralamaları, dallanmayı, yapılandırılmış mantıksal eşzamanlılığı ifade etmek için kullanılabilir ve özellikle Skyframe etkileşimi için tasarlanmıştır. StateMachine'lar daha büyük StateMachine'lar halinde oluşturulabilir ve alt StateMachine'ları paylaşabilir. Eşzamanlılık her zaman yapı olarak hiyerarşiktir ve tamamen mantıksaldır. Eşzamanlı her alt görev, tek bir paylaşılan üst SkyFunction iş parçacığında çalışır.

Giriş

Bu bölümde, java.com.google.devtools.build.skyframe.state paketinde bulunan StateMachine'lar kısaca tanıtılmaktadır.

Skyframe yeniden başlatma işlemine 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 ile bir SkyFunction'ın değerlendirilmesine karşılık gelir. Hesaplama modeli, bir SkyFunction'ın SkyKey ile SkyValue'ları arayabileceği ve ek SkyFunction'ların yinelemeli, paralel değerlendirmesini tetikleyebileceği şekildedir. Bir işleme alt grafiği tamamlanmadığı için istenen SkyValue henüz hazır olmadığında, bir iş parçacığını bağlayacak olan engelleme yerine, istekte bulunan SkyFunction bir null getValue yanıtı gözlemler ve eksik girişler nedeniyle tamamlanmadığını belirten bir SkyValue yerine null döndürmelidir. Skyframe, daha önce istenen tüm SkyValue'lar kullanılabilir hale geldiğinde SkyFunctions'ı yeniden başlatır.

SkyKeyComputeState kullanıma sunulmadan önce, yeniden başlatma işlemini yönetmenin geleneksel yolu hesaplamayı tamamen yeniden çalıştırmaktı. Bu durum ikinci dereceden karmaşıklığa sahip olsa da bu şekilde yazılan işlevler sonunda tamamlanır. Bunun nedeni, her yeniden çalıştırmada daha az arama null döndürmesidir. SkyKeyComputeState ile, elle belirtilen kontrol noktası verilerini bir SkyFunction ile ilişkilendirmek ve önemli ölçüde yeniden hesaplama yapmaktan kurtulmak mümkündür.

StateMachine, SkyKeyComputeState içinde bulunan ve askıya alma ile devam ettirme yürütme kancalarını kullanıma sunarak bir SkyFunction yeniden başlatıldığında (SkyKeyComputeState'nin önbellekten düşmediği varsayılarak) neredeyse tüm yeniden hesaplamaları ortadan kaldıran nesnelerdir.

SkyKeyComputeState içindeki durum bilgisi içeren hesaplamalar

Nesne yönelimli tasarım açısından, hesaplama nesnelerini saf veri değerleri yerine SkyKeyComputeState içinde depolamak mantıklıdır. Java'da, davranış taşıyan bir nesnenin en kısa açıklaması bir işlevsel arayüzdür ve bu açıklamanın yeterli olduğu anlaşılmıştır. StateMachine, merak uyandıran ve yinelemeli olan şu tanıma sahiptir2.

@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önüş değeri başka bir StateMachine işlevidir. Bu işlev, bir adım dizisinin tümevarımlı 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ışla birlikte bir StateMachine açıklıyor.

hello
world

step2, StateMachine'nin 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

Sezgisel olarak, bir hesaplamayı tek bir işlev yerine StateMachine adıma bölmek, hesaplamayı askıya almak ve devam ettirmek için gereken bağlantıları sağlar. StateMachine.step döndüğünde açık bir askıya alma noktası vardır. Döndürülen StateMachine değeriyle belirtilen devam ettirme, açık bir devam ettirme noktasıdır. Bu nedenle, hesaplama tam olarak kaldığı yerden devam edebileceğinden yeniden hesaplama yapılmaz.

Geri çağırmalar, devam ettirmeler ve eşzamansız hesaplama

Teknik olarak, StateMachine bir devamlılık görevi görür ve yürütülecek sonraki hesaplamayı belirler. Engelleme yerine, StateMachine, step işlevinden dönerek gönüllü olarak askıya alınabilir. Bu işlev, kontrolü tekrar bir Driver örneğine aktarır. Driver daha sonra hazır bir StateMachine'ye geçebilir veya kontrolü Skyframe'e geri verebilir.

Geleneksel olarak geri çağırmalar ve devam ettirmeler tek bir kavram olarak ele alınır. Ancak StateMachines, ikisi arasında ayrım yapar.

  • Geri Arama: Asenkron bir hesaplamanın sonucunun nerede saklanacağını açıklar.
  • Devam: Bir sonraki yürütme durumunu belirtir.

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

Devamlar, StateMachine değerlerinin StateMachine dönüş değerleridir ve tüm eşzamansız hesaplamalar çözümlendikten sonra gerçekleşen karmaşık yürütmeyi kapsar. Bu yapılandırılmış yaklaşım, geri çağırmaların karmaşıklığını yönetilebilir düzeyde tutmaya yardımcı olur.

Görevler

Tasks arayüzü, StateMachine'lere SkyKey ile SkyValue'ları aramak ve eşzamanlı alt görevler 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ı

StateMachine, SkyValue'ları aramak için Tasks.lookUp aşırı yüklemelerini kullanır. SkyFunction.Environment.getValue ve SkyFunction.Environment.getValueOrThrow ile benzerdir ve benzer istisna işleme semantiğine sahiptir. Uygulama, aramayı hemen gerçekleştirmez. Bunun yerine, aramayı yapmadan önce mümkün olduğunca çok arama 4 toplar. Değer, örneğin Skyframe'in yeniden başlatılması gerektiği için hemen kullanılamayabilir. Bu nedenle, arayan kişi, geri çağırma işlevi kullanarak sonuçtaki değerle ne yapılacağını belirtir.

StateMachine işlemcisi (Driver ve SkyFrame'e köprüleme), değerin bir sonraki durum başlamadan önce kullanılabilir olmasını sağlar. Bir örnek aşağıda 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 new Key() için arama yapar ve this öğesini tüketici olarak iletir. DoesLookup, Consumer<SkyValue>'yi uyguladığı için bu mümkündür.

Sözleşmeye göre, sonraki durum DoesLookup.processValue başlamadan önce DoesLookup.step ile ilgili tüm aramalar tamamlanır. Bu nedenle value, processValue'de 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 yinelemeli olarak daha fazla alt görev oluşturma veya SkyValue'ları arama gibi normal StateMachine'lerin yapabileceği her şeyi yapabilir. Durum makinesi sürücüsü, lookUp'ya benzer şekilde, bir sonraki adıma geçmeden önce tüm alt görevlerin tamamlanmasını sağlar. Bir örnek aşağıda 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ışır. Bu nedenle i öğesinin "eşzamanlı" güncellemesi için herhangi bir senkronizasyon gerekmez.

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

Bir sonraki duruma geçmeden önce her lookUp ve enqueue çözümlenmesi gerektiğinden eşzamanlılık doğal olarak ağaç yapılarıyla sınırlıdır. 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

Eşzamanlılık yapısının ağaç oluşturduğunu UML'den 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ık hakkında akıl yürütmek çok daha kolaydır.

Bileşim ve kontrol akışı kalıpları

Bu bölümde, birden fazla StateMachine'nin nasıl oluşturulabileceğ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ışı modelidir. Bunun bir örneği Stateful computations inside SkyKeyComputeState bölümünde gösterilmektedir.

Dallanma

StateMachines'deki dallanma durumları, aşağıdaki örnekte gösterildiği gibi normal Java kontrol akışı kullanılarak farklı değerler döndürülerek elde edilebilir.

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 dalların erken tamamlanma için DONE döndürmesi çok yaygındır.

Gelişmiş sıralı kompozisyon

StateMachine kontrol yapısı hafızasız olduğundan StateMachine tanımlarını alt görev olarak paylaşmak bazen zor olabilir. M1 ve M2, StateMachine paylaşan StateMachine örnekleri olsun. M, M1 ve M2 sırasıyla <A, S, B> ve <X, S, Y> dizileri olsun. Sorun, S tamamlandıktan sonra B veya Y'ye devam edip etmeyeceğini bilmiyor ve StateMachine, çağrı yığınını tam olarak tutmuyor. Bu bölümde, bu hedefe ulaşmaya yönelik bazı teknikler incelenmektedir.

Terminal dizisi öğesi olarak StateMachine

Bu, ilk sorunu çözmez. Yalnızca paylaşılan StateMachine, sıradaki son öğe olduğunda sıralı birleştirme gösterilir.

// 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 alınmış alt görevlerin bir sonraki durumdan önce tamamlanması garanti edildiğinden, alt görev mekanizmasını bazen biraz6 kötüye kullanmak 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 enjeksiyonu

Bazen, S yürütülmeden önce tamamlanması gereken başka paralel alt görevler veya Tasks.lookUp çağrıları olduğundan Tasks.enqueue kötüye kullanılamaz. Bu durumda, S'ye ne yapması gerektiğini bildirmek için S'ye bir runAfter parametresi ekleyebilirsiniz.

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 temizdir. Ancak bu yaklaşımı çok fazla kullanmak (ör. birden fazla StateMachine öğesini runAfter ile iç içe yerleştirmek) geri çağırma cehennemine yol açar. Bunun yerine, sıralı durumları normal sıralı durumlarla ayırmak daha iyidir.runAfter

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

ile 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 oluştuğunda erken aşamada iptal edilecek bir runAfterUnlessError düşünmüştük. Bu durum, hataların genellikle iki kez kontrol edilmesinden kaynaklanmaktadır. Bir kez StateMachine referansına sahip olan runAfter tarafından, bir kez de runAfter makinenin kendisi tarafından kontrol edilir.

Biraz düşündükten sonra kodun tekdüzeliğinin hata denetiminin yinelenmesini önlemekten daha önemli olduğuna karar verdik. runAfter mekanizması, her zaman hata kontrolü gerektiren tasks.enqueue mekanizmasıyla tutarlı bir şekilde çalışmazsa kafa karıştırıcı olur.

Doğrudan yetki verme

Her resmi durum geçişinde ana Driver döngüsü ilerler. Sözleşmeye göre, durumların ilerlemesi, bir sonraki durum yürütülmeden önce daha önce sıraya alınmış tüm SkyValue aramalarının ve alt görevlerin çözümlendiği anlamına gelir. Bazen bir temsilcinin StateMachine mantığı, aşama ilerletmeyi gereksiz veya verimsiz hale getirir. Örneğin, temsilcinin ilk step'sı, temsilci durumunun aramalarıyla paralelleştirilebilecek SkyKey aramaları gerçekleştiriyorsa aşama ilerlemesi bu aramaları sıralı hale getirir. Aşağıdaki örnekte gösterildiği gibi, doğrudan temsilci atama yapmak 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ışı

Önceki tartışmanın odak noktası, kontrol akışını yönetmekti. Bu bölümde, veri değerlerinin yayılması açıklanmaktadır.

Tasks.lookUp geri çağırmalarını uygulama

Tasks.lookUp geri çağırmasının SkyValue lookups içinde uygulanmasına ilişkin bir örnek verilmiştir. Bu bölümde, gerekçe sunulmakta ve birden fazla SkyValue'nun işlenmesi için yaklaşımlar önerilmektedir.

Tasks.lookUp geri çağırmalar

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 uygulamak için deyimsel yaklaşım olarak Java lambda'sı kullanabilirsiniz:

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

Burada myValue, arama işlemini yapan StateMachine örneğinin bir üye değişkenidir. Ancak lambda, Consumer<SkyValue> arayüzünü StateMachine uygulamasında uygulamaya kıyasla ek bellek ayırması gerektirir. Lambda, belirsiz olabilecek birden fazla arama olduğunda da kullanışlıdır.

Ayrıca, Tasks.lookUp'nın SkyFunction.Environment.getValueOrThrow'ye 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);
  }

Örnek bir uygulama aşağıda 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 olmadan yapılan aramalar gibi, StateMachine sınıfının geri çağırmayı doğrudan uygulaması da lambda için bellek ayırmasını önler.

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

Birden fazla SkyValue tüketme

Genellikle birden fazla SkyValue araması yapılması 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 uygulaması net bir şekilde paylaşılabilir. Bu durum söz konusu olmadığında, lambda tabanlı uygulamalara veya uygun geri çağırmaları uygulayan tam iç sınıf örneklerine geri dönmek mümkündür.

StateMachine'lar arasında değerleri yayma

Bu belge şu ana kadar yalnızca bir alt görevde çalışmanın nasıl düzenleneceğini açıklamıştır ancak alt görevlerin de arayan kişiye değerleri bildirmesi gerekir. Alt görevler mantıksal olarak eşzamansız olduğundan sonuçları, geri arama kullanılarak arayana geri iletilir. Bunun çalışması için alt görev, oluşturucusu aracılığıyla yerleştirilen bir alıcı 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;
  }
}

Arayan StateMachine daha sonra aşağıdaki gibi görünür.

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 gösterilmektedir. Caller, sonuçlarını geri yaymalı ve kendi Caller.ResultSink değerini tanımlamalıdır. Caller geri çağırma işlevlerini BarProducer.ResultSink uygular. Devam ettirildiğinde, hata oluşup oluşmadığını belirlemek için processResult, value değerinin boş olup olmadığını kontrol eder. Bu, alt görevden veya SkyValue aramasından çıkış kabul edildikten sonra görülen yaygın bir davranış kalıbıdır.

acceptBarError'nın uygulanmasının, hata yayma'nın gerektirdiği şekilde sonucu Caller.ResultSink'a hemen ilettiğini unutmayın.

Üst düzey StateMachine'lerin alternatifleri Driver'ler ve SkyFunctions'a geçiş bölümünde açıklanmıştır.

Hata işleme

Tasks.lookUp callbacks ve Propagating values between StateMachines içinde hata işlemeyle ilgili birkaç örnek bulunmaktadır. InterruptedException dışındaki istisnalar oluşturulmaz, bunun yerine geri çağırmalar aracılığıyla değer olarak iletilir. Bu tür geri çağırmalar genellikle özel-veya semantiğine sahiptir ve değer veya hata olmak üzere yalnızca biri iletilir.

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

Hata yayma (--nokeep_going)

Hata yayma sırasında, istenen tüm SkyValue'lar kullanılamıyor olsa bile bir SkyFunction yeniden başlatılabilir. Bu gibi durumlarda, Tasks API sözleşmesi nedeniyle sonraki duruma asla ulaşılamaz. Ancak StateMachine yine de istisnayı yaymalıdır.

Yayma, 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 yerine getirmelidir. İç içe yerleştirilmiş bir StateMachine için bu, üst geri çağırma işlevi çağrılarak gerçekleştirilir.

SkyFunction ile arayüz oluşturan üst düzeyde StateMachine, bu işlem ValueOrExceptionProducer öğesinin setException yöntemi çağrılarak yapılabilir. Bu durumda, SkyValue'lar eksik olsa bile ValueOrExceptionProducer.tryProduceValue istisna oluşturur.

Bir Driver doğrudan kullanılıyorsa makine işlemeyi tamamlamamış olsa bile SkyFunction'dan yayılan hataları kontrol etmek önemlidir.

Etkinlik İşleme

Etkinlik yayınlaması gereken SkyFunctions için SkyKeyComputeState'e StoredEventHandler yerleştirilir ve bu işlevlere ihtiyaç duyan StateMachine'lere daha da yerleştirilir. Geçmişte, Skyframe'in belirli etkinlikleri yeniden oynatılmadıkları sürece bırakması nedeniyle StoredEventHandler gerekliydi ancak bu durum daha sonra düzeltildi. Hata işleme geri çağırma işlevlerinden yayınlanan etkinliklerin uygulanmasını basitleştirdiği için StoredEventHandler yerleştirme korunur.

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

Bir Driver, belirli bir kök StateMachine ile başlayarak StateMachine'lerin yürütülmesini yönetmekten sorumludur. StateMachine, alt görev StateMachine'ları yinelemeli olarak sıraya alabileceğinden tek bir Driver çok sayıda alt görevi yönetebilir. Bu alt görevler, yapılandırılmış eşzamanlılık sonucunda bir ağaç yapısı oluşturur. Driver, verimliliği artırmak için alt görevlerdeki SkyValue aramalarını toplu olarak işler.

Aşağıdaki API ile Driver etrafında oluşturulmuş çeşitli sınıflar vardı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. Çağırma Driver.drive, StateMachine öğesini Skyframe yeniden başlatılmadan gidebildiği yere kadar yürütür. StateMachine tamamlandığında doğru, aksi takdirde yanlış değerini döndürür. Bu, tüm değerlerin kullanılamadığını gösterir.

Driver, StateMachine'nin eşzamanlı durumunu korur ve SkyKeyComputeState'ye yerleştirmek için uygundur.

Doğrudan Driver oluşturma

StateMachine uygulamaları, sonuçlarını geleneksel olarak geri çağırmalar aracılığıyla iletir. Aşağıdaki örnekte gösterildiği gibi, Driver doğrudan oluşturmak mümkündür.

Driver, SkyKeyComputeState uygulamasına yerleştirilir. Ayrıca, biraz daha aşağıda tanımlanacak olan ilgili ResultSink öğesinin uygulaması da yerleştirilir. En üst düzeyde, State nesnesi, Driver'den daha uzun süre yaşayacağı garanti edildiğinden hesaplamanın 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 öğesini taslak olarak gösterir.

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;
  }
}

Daha sonra, sonucu tembelce hesaplayan kod aşağıdaki gibi görünebilir.

@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;
}

Yerleştirme Driver

StateMachine bir değer oluşturur ve istisna oluşturmazsa aşağıdaki örnekte gösterildiği gibi Driver öğesinin yerleştirilmesi 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, aşağıdaki gibi görünen bir koda sahip olabilir (burada State, SkyKeyComputeState'nin işleve özgü türüdü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;
}

Driver öğesini StateMachine uygulamasına yerleştirmek, Skyframe'in eşzamanlı kodlama stiline daha uygundur.

İstisnalara neden olabilecek durum makineleri

Aksi takdirde, eşzamanlı SkyFunction koduyla eşleşecek eşzamanlı API'lere sahip SkyKeyComputeState-embeddable ValueOrExceptionProducer ve ValueOrException2Producer sınıfları vardır.

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 Embedding sürücüsündeki ResultProducer sınıfına çok benzer. Ayrıca SkyFunction ile benzer şekilde arayüz oluşturur. ResultSink tanımlamak yerine, bu etkinliklerden biri gerçekleştiğinde uygulamalar 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 biri ayarlandığında istisna oluşturur.

Daha önce belirtildiği gibi, hata yayma sırasında makine henüz tamamlanmamış olsa bile tüm girişler kullanılamadığından hata oluşabilir. Bunu karşılamak için tryProduceValue, makine tamamlanmadan önce bile ayarlanan tüm istisnaları atar.

Sonsöz: Geri aramalar nihayetinde kaldırılacak

StateMachine, eşzamansız hesaplama yapmak için oldukça verimli ancak standart kod yoğun bir yöntemdir. Devamlılıklar (özellikle Runnables passed to ListenableFuture biçiminde) Bazel kodunun belirli bölümlerinde yaygın olsa da SkyFunctions analizinde yaygın değildir. Analiz çoğunlukla CPU ile sınırlıdır ve disk G/Ç için verimli eşzamansız API'ler yoktur. Sonuç olarak, geri çağırmalar öğrenme eğrisine sahip oldukları ve okunabilirliği engelledikleri için optimize edilerek kaldırılmaları iyi olur.

En umut verici alternatiflerden biri Java sanal iş parçacıklarıdır. Geri çağırma işlevleri yazmak yerine her şey eşzamanlı ve engelleyici çağrılarla değiştirilir. Bu durum, platform iş parçacığının aksine sanal iş parçacığı kaynağının bağlanmasının ucuz olması gerektiğinden mümkündür. Ancak sanal iş parçacıklarıyla bile basit senkron işlemleri iş parçacığı oluşturma ve senkronizasyon temel öğeleriyle değiştirmek çok maliyetlidir. StateMachine'dan 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 iş parçacıkları hâlâ önizleme aşamasında olan bir özellik olduğundan, bu taşıma işleminin performans iyileştikten sonraki bir tarihte yapılması mümkündür.

Dikkate alınması gereken bir diğer yaklaşım da Loom eş yordamlarının kullanıma sunulmasını beklemektir. Buradaki avantaj, işbirlikçi çoklu görev kullanılarak senkronizasyon ek yükünün azaltılabilmesidir.

Diğer tüm yöntemler başarısız olursa düşük seviyeli bayt kodu yeniden yazma da uygun bir alternatif olabilir. Yeterli optimizasyonla, elle yazılmış geri çağırma kodu performansına yaklaşmak mümkün olabilir.

Ek

Geri Arama Cehennemi (Callback Hell)

Geri çağırma cehennemi, geri çağırmaların kullanıldığı eşzamansız kodda kötü şöhretli bir sorundur. Bunun nedeni, sonraki adımın devamının önceki adımın içine yerleştirilmiş olmasıdır. Çok fazla adım varsa bu iç içe yerleştirme son derece derin olabilir. Kontrol akışıyla birleştirildiğinde 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şkenlerin kullanılması zahmetli olabilir. Aşağıda gösterildiği gibi, derin iç içe yerleştirme, lambda yerine devamlılık olarak yöntem referansları döndürülerek ö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;
  }
}

runAfter yerleştirme kalıbı çok yoğun bir şekilde kullanıldığında da geri çağırma cehennemi oluşabilir. Ancak bu durum, yerleştirmeler arasına sıralı adımlar eklenerek önlenebilir.

Örnek: Zincirleme SkyValue aramaları

Uygulama mantığı genellikle SkyValue aramalarının bağımlı zincirlerini gerektirir. Örneğin, ikinci bir SkyKey, ilk SkyValue'ya bağlıysa. Bu durumu basitçe düşündüğümüzde, karmaşık ve derinlemesine iç içe yerleştirilmiş bir geri çağırma yapısı elde ederiz.

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 devam ettirme işlemleri yöntem referansları olarak belirtildiğinden kod, durum geçişlerinde prosedürel görünür: step2, step1'i takip eder. Burada value2 değerini atamak için lambda kullanıldığını unutmayın. Bu sayede kodun sıralaması, hesaplamanın yukarıdan aşağıya doğru sıralamasıyla eşleşir.

Diğer İpuçları

Okunabilirlik: Yürütme Sırası

Okunabilirliği artırmak için StateMachine.step uygulamalarını yürütme sırasına göre ve geri çağırma uygulamalarını kodda geçirildikleri yerden hemen sonra yerleştirmeye çalışın. Kontrol akışının dallandığı yerlerde bu her zaman mümkün olmayabilir. Bu gibi durumlarda ek yorumlar faydalı olabilir.

Örnek: Zincirleme SkyValue aramaları bölümünde, bunu sağlamak için ara yöntem referansı oluşturulur. Bu işlem, okunaklılık için küçük bir performans kaybına neden olur ancak bu durum burada muhtemelen kabul edilebilir.

Kuşak Hipotezi

Orta ömürlü Java nesneleri, Java çöp toplayıcısının nesil hipotezini bozar. Bu hipotez, çok kısa süre yaşayan veya sonsuza kadar yaşayan nesneleri işlemek üzere tasarlanmıştır. 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, eşzamansız hesaplamaların tamamlanmasını beklerken askıya alındıkları için orta düzeyde bir kullanım ömrüne sahiptir.

JDK19'da bu durum daha az kötü görünse de StateMachine kullanılırken, oluşturulan gerçek çöp miktarında önemli düşüşler olsa bile GC süresinde artış gözlemlemek bazen mümkündür. StateMachine'nin ömrü orta düzeyde olduğundan, eski nesle yükseltilebilir ve bu da daha hızlı dolmasına neden olabilir. Bu nedenle, temizleme için daha pahalı olan büyük veya tam GC'ler gerekir.

İlk önlem, StateMachine değişkenlerinin kullanımını en aza indirmektir ancak bu, örneğin bir değerin birden fazla durumda kullanılması gerektiğinde her zaman mümkün olmayabilir. Mümkün olduğunda, yerel yığın step değişkenleri genç nesil değişkenlerdir ve verimli bir şekilde GC'lenir.

StateMachine değişkenleri için işlemleri alt görevlere ayırmak ve değerleri StateMachine'ler arasında yaymak için önerilen kalıbı izlemek de faydalıdır. Bu kalıba uyulduğunda yalnızca alt StateMachine öğelerinin üst StateMachine öğelerine referans verdiğini, bunun tersinin geçerli olmadığını unutmayın. Bu nedenle, çocuklar sonuç geri aramalarını kullanarak ebeveynleri bilgilendirdikçe ve görevleri tamamlayıp güncelledikçe kapsam dışına çıkar ve GC için uygun hale gelir.

Son olarak, bazı durumlarda StateMachine değişkeni önceki durumlarda gerekirken sonraki durumlarda gerekmez. Büyük nesnelerin referanslarını artık gerekli olmadıkları bilindiğinde boşaltmak faydalı olabilir.

Durumları adlandırma

Bir yöntemi adlandırırken genellikle bu yöntemde gerçekleşen davranış için bir ad vermek mümkündür. StateMachine'da yığın olmadığı için bu işlemin nasıl yapılacağı daha az anlaşılırdır. Örneğin, foo yönteminin bar alt yöntemini çağırdığını varsayalım. StateMachine içinde bu, foo durum dizisine ve ardından bar durumuna çevrilebilir. foo artık bar davranışını içermiyor. Sonuç olarak, eyaletler için yöntem adları kapsam açısından daha dar olma eğilimindedir ve yerel davranışları yansıtma olasılığı vardır.

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'in değerler kullanılamadığında baştan başlatma kuralının aksine. 

  2. step öğesinin InterruptedException oluşturmasına izin verildiğini ancak örneklerde bunun atlandığını unutmayın. StateMachine'yi çalıştıran ve daha sonra açıklanacak olan Driver'ye kadar yayılan bu istisnayı oluşturan Bazel kodunda birkaç düşük yöntem vardır. Gerekli olmadığında atılacağını bildirmemek sorun değildir. 

  3. Eşzamanlı alt görevler, her bağımlılık için bağımsız çalışma gerçekleştiren ConfiguredTargetFunction tarafından desteklenir. Tüm bağımlılıkları aynı anda işleyen karmaşık veri yapılarını işlemek yerine, her bağımlılığın kendi bağımsız StateMachine'si vardır. Bu sayede verimsizlikler önlenir. 

  4. Tek bir adımda birden fazla tasks.lookUp çağrısı birlikte gruplandırılır. Ek toplu işleme, eşzamanlı alt görevlerde gerçekleşen aramalarla 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 ileti dizisi oluşturup bu diziye katılmaya benzer.