Skyframe StateMachines Kılavuzu

Sorun bildirin Kaynağı göster

Genel bakış

Skyframe StateMachine, yığında yer alan çözülmüş bir işlev nesnesidir. Gerekli değerler hemen mevcut olmadığında ancak eşzamansız olarak hesaplandığında yedeklilik olmadan1 esnek ve değerlendirmeyi destekler. StateMachine, bekleme sırasında bir mesaj dizisi kaynağını bağlayamaz ancak bunun yerine askıya alınıp devam ettirilmelidir. Bu şekilde ayrıştırma, daha önce hesaplamaların atlanması için açık yeniden giriş noktalarını açığa çıkarır.

StateMachine öğeleri sıralamaları, kollara ayırma işlemlerini, yapılandırılmış mantıksal eşzamanlılıkları ifade etmek için kullanılabilir ve özellikle Skyframe etkileşimine göre özelleştirilir. StateMachine öğeleri, daha büyük StateMachine'lerde oluşturulabilir ve StateMachine alt öğelerini paylaşabilir. Eşzamanlılık, her zaman yapı bazında hiyerarşiktir ve tamamen mantıksaldır. Her eşzamanlı alt görev, tek bir üst üst düzey SkyFunction ileti dizisinde çalışır.

Giriş

Bu bölümde kısaca motivasyon sağlanır ve java.com.google.devtools.build.skyframe.state paketinde bulunan StateMachine'ler tanıtılır.

Skyframe'e kısa bir giriş

Skyframe, bağımlılık grafiklerini paralel olarak değerlendiren bir çerçevedir. Grafikteki her düğüm, parametrelerini belirten bir SkyKey ve sonucunu içeren SkyValue içeren bir SkyFunction'ın değerlendirmesine karşılık gelir. Hesaplamalı model, bir SkyFunction, SkyValue'ları SkyKey'de arayarak ek SkyFunction'ların yinelenen ve paralel olarak değerlendirilmesini tetikleyebilir. İstenen SkyValue'nun bir hesaplama alt kümesi tamamlanmadığı için hazır olmadığı bir ileti dizisini bağlayacak olan bir ileti dizisi engellemek yerine, istekte bulunan Skyky işlevi null getValue yanıtı görüyor ve bir SkyValue yerine null girişi döndürmesi gerekiyor. Bu da girişlerin eksik olması nedeniyle işlemin tamamlanmadığını gösteriyor. Daha önce istenen tüm SkyValue'lar kullanılabilir hale geldiğinde Skyframe SkyFunctions işlevlerini yeniden başlatır.

SkyKeyComputeState uygulaması kullanıma sunulmadan önce, yeniden başlatma işleminde kullanılan normal yöntem, hesaplamayı tamamen yeniden yürütmekti. Bu ikinci dereceden karmaşıklığa sahip olsa da, bu şekilde yazılan işlevler bir kez daha tamamlandığından her bir işlev daha az sayıda null döndürdüğünde tamamlanır. SkyKeyComputeState sayesinde, elle belirtilen kontrol noktası verilerini bir SkyFunction ile ilişkilendirerek önemli miktarda hesaplama işlemi yapabilirsiniz.

StateMachine'ler, SkyKeyComputeState içinde bulunan ve bir SkyFunction yeniden başlatıldığında (SkyKeyComputeState'ın önbellekten düşmediğini varsayarak) askıya alma ve yürütme kancalarını açığa çıkararak sanal olarak tüm yeniden hesaplamaları ortadan kaldıran nesnelerdir.

SkyKeyComputeState içindeki durum bilgili hesaplamalar

Nesne odaklı tasarım açısından, sayısal nesneleri saf veri değerleri yerine SkyKeyComputeState deposunda tutmayı düşünebilirsiniz. Java'da nesne taşıma davranışının en az basit açıklaması işlevsel bir arayüzdür ve bunun yeterli olduğu ortaya çıkar. Bir StateMachine, aşağıdaki merakla tekrar eden tanımına2 sahiptir.

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

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

step işlevinin dönüş değeri, başka bir StateMachine öğesidir. Böylece, bir dizi adımın hatalı bir şekilde belirtilmesine izin verilir. StateMachine tamamlandığında step, 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ışa sahip bir StateMachine açıklar.

hello
world

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

Askıya alma ve devam ettirme

Sezgisel olarak, bir hesaplamayı monolitik işlev yerine StateMachine adıma bölmeyi, bir hesaplamayı askıya almak ve devam ettirmek için gerekli olan kancaları sağlar. StateMachine.step geri döndüğünde, açık bir askıya alma noktası var. Döndürülen StateMachine değeri ile belirtilen devamlılık, açık bir devam ettirme noktasıdır. Bu nedenle, hesaplama tam olarak kaldığı yerden alınabildiğinden, hesaplamadan kaçınılabilir.

Geri çağırmalar, devamlılıklar ve eşzamansız hesaplama

Teknik terimler açısından, bir StateMachine devamlılık görevi görür ve yürütülecek sonraki hesaplamayı belirler. StateMachine, engellemek yerine step işlevinden geri dönerek gönüllü olarak askıya alabilir ve kontrol tekrar bir Driver örneğine aktarılır. Driver daha sonra hazır bir StateMachine moduna geçebilir veya kontrolü Skyframe'e geri gönderebilir.

Geleneksel olarak geri çağırmalar ve devamlılıklar tek bir kavram altında birleştirilir. Ancak StateMachine'ler birbirlerinden ayırt edilebilir.

  • Callback - Eşzamansız hesaplamanın sonucunun nerede depolanacağını açıklar.
  • Devam - Sonraki yürütme durumunu belirtir.

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

Devamlar, StateMachine'lerin StateMachine döndürme değerleridir ve tüm eşzamansız hesaplamalar çözümlendikten sonraki karmaşık yürütme işlemini içerir. Bu yapılandırılmış yaklaşım, geri çağırmaların karmaşıklığını yönetmeyi sağlar.

Görevler

Tasks arayüzü, SkyKey'den SkyValues'a arama yapmak ve eş zamanlı alt görevler planlamak için StateMachine'lara 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.
}
StateMachine

SkyValue aramaları

StateMachine, SkyValues araması yapmak için Tasks.lookUp aşırı yüklemeyi kullanır. Bunlar, SkyFunction.Environment.getValue ve SkyFunction.Environment.getValueOrThrow ile benzerdir ve benzer istisna işleme semantiğine sahiptir. Uygulama, aramayı hemen gerçekleştirmez ancak bunun yerine, mümkün olduğunca fazla aramayı 4 olarak gruplandırır. Değer, örneğin bir Skyframe'in yeniden başlatılmasını zorunlu tuttuğunda hemen kullanılamayabilir. Böylece arayan, bir geri çağırma kullanarak sonuç değeriyle ilgili olarak ne yapılacağını belirtebilir.

StateMachine işlemcisi (Driver ve SkyFrame'e geçiş), değerin bir sonraki durum başlamadan önce kullanılabileceğini garanti eder. Aşağıda bununla ilgili 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 new Key() için arama yapar ve tüketici olarak this değerini geçirir. DoesLookup, Consumer<SkyValue> yöntemini uyguladığı için bu mümkündür.

Sözleşme gereği, DoesLookup.processValue eyaleti başlamadan önce DoesLookup.step için tüm aramalar tamamlanır. Dolayısıyla, value özelliğine processValue üzerinden 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 görevidir ve düzenli olarak daha fazla alt görev oluşturma veya SkyValue'ları arama dahil olmak üzere normal StateMachineherhangi bir işlemi yapabilir. Durum makinesi sürücüsü, lookUp özelliğine benzer şekilde bir sonraki adıma geçmeden önce tüm alt görevlerin tamamlanmasını sağlar. Aşağıda bununla ilgili 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ışır. Bu nedenle, i'nin "eşzamanlı" güncellemesinin senkronize edilmesi 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şturulabilir.

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 alternatif bir görünüm vardır.

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

Yapılandırılmış eşzamanlılığı hesaba katmak çok daha kolaydır.

Akış bileşimi ve denetimi

Bu bölümde, birden fazla StateMachine öğesini nasıl oluşturabileceğinize dair örnekler ve belirli kontrol akışı sorunlarına çözümler sunulmaktadır.

Sıralı durumlar

Bu, en yaygın ve basit olan kontrol akışı kalıbıdır. Bunun bir örneği, SkyKeyComputeStateiçindeki durum hesaplamaları bölümünde gösterilmektedir.

Dal

Aşağıdaki örnekte gösterildiği gibi, StateMachine konumundaki dallara 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;
  }
  …
}

Bazı dalların erken tamamlama için DONE sonucu döndürmesi çok yaygındır.

Gelişmiş sıralı beste

StateMachine kontrol yapısı belleksiz olduğundan, alt görevler olarak StateMachine tanımlarını paylaşmak bazen garip olabilir. M1 ve M2'nin, StateMachine, S'yi paylaşan StateMachine örnekleri olmasına ve M1 ile M2'nin sırasıyla <A, S, B> ve <X, S, Y> adımlarından oluşan sıralar olmasına izin verin. Sorun, S tamamlandıktan sonra B veya Y'ye devam edip etmeyeceğini bilemez ve StateMachine'ler bir çağrı yığınını tam olarak tutmaz. Bu bölümde, bunu başarmaya yönelik bazı teknikler açıklanmaktadır.

Terminal dizisi öğesi olarak StateMachine

Bu, ortaya çıkan ilk sorunu çözmez. Yalnızca paylaşılan StateMachine, dizideki terminal terminali olduğunda sıralı kombinasyonu 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 yöntem S'nin kendisi karmaşık durum makinesi olsa bile çalışır.

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

Sıraya alınmış alt görevlerin bir sonraki durumdan önce tamamlanması garanti edildiğinden, bazen alt görev mekanizmasını6biraz 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 enjeksiyon

S yürütülmeden önce tamamlanması gereken başka paralel alt görevler veya Tasks.lookUp çağrılar olması nedeniyle bazen Tasks.enqueue kötüye kullanımı mümkün değildir. Bu durumda, S'ye runAfter parametresi eklemek, bir sonraki adımda ne yapılacağını S bildirmek için kullanılabilir.

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. Bununla birlikte, örneğin birden fazla StateMachine öğesini runAfter ile iç içe yerleştirerek Callback Hell'in aşırı yol aldığını görebilirsiniz. Bunun yerine, sıralı runAfter değerlerini normal sıralı durumlarla ayırmak daha iyidir.

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

şununla 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);
  }

Yasak alternatif: runAfterUnlessError

Daha önceki bir taslakta, hataları erkenden iptal edecek bir runAfterUnlessError dikkate alıyorduk. Bu durum, hataların genellikle runAfter referanslı StateMachine tarafından ve bir kez runAfter makinesinin kendisi tarafından kontrol edilmesiyle sonuçlandı.

Yapılan inceleme sonucunda, kodun tek tipliğinin hata denetiminin tekilleştirilmesinden daha önemli olduğuna karar verdik. runAfter mekanizması, her zaman hata kontrolü gerektiren tasks.enqueue mekanizmasıyla tutarlı bir şekilde çalışmadığında kafa karıştırıcı olur.

Doğrudan yetki verme

Resmi eyalet geçişi her olduğunda ana Driver döngüsü ilerler. Sözleşmeye göre, eyaletlerin ilerlemesi daha önce sıraya alınan tüm SkyValue aramalarının ve alt görevlerinin bir sonraki eyalet yürütülmeden önce çözülmesi anlamına gelir. Yetki verilmiş bir kullanıcının StateMachine mantığı, aşamayı gereksiz kılar veya son derece üretken olmaz. Örneğin, yetki verilen kullanıcının ilk step öğesi, yetki verme durumuyla ilgili aramalarla paralel hale getirilebilecek SkyKey aramalarını gerçekleştirirse aşamalı bir aşama, bunların sıralı olmasını sağlar. Aşağıdaki örnekte gösterildiği gibi, doğrudan yetki vermek 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ın yönetilmesiydi. Bu bölümde, veri değerlerinin nasıl üretildiği açıklanmaktadır.

Tasks.lookUp geri çağırmaları uygulama

SkyValue aramalarında bir Tasks.lookUp geri çağırmanın örneğine örnek verilmiştir. Bu bölümde sunulan gerekçe, birden çok SkyValue'nun işlenmesi için yaklaşımlar önermektedir.

Tasks.lookUp geri arama numarası

Tasks.lookUp yöntemi, parametre olarak sink geri çağırmasını alır.

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

Deyimsel yaklaşımda, bunu uygulamak için bir Java lambda kullanılır:

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

myValue, StateMachine örneğinin arama değişkeni yapan bir üye değişkeni olur. Ancak lambda, StateMachine uygulamasında Consumer<SkyValue> arayüzünü uygulamaya kıyasla ek bir bellek ayırma gerektirir. Lambda, belirsiz olabilecek birden fazla arama olduğunda yine de yararlıdır.

Ayrıca, SkyFunction.Environment.getValueOrThrow aşırı olan Tasks.lookUp aşırı yükleme işlemiyle başa çıkılı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);
  }

Uygulama örneği aşağıda gösterilmiştir.

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, StateMachine sınıfının doğrudan geri çağırmayı uygulaması, lamba için bellek kullanımı kaydeder.

Hata işleme biraz daha ayrıntı sağlar ancak hatalar ile normal değerler arasındaki yayılma çok fazla değildir.

Birden çok SkyValue kullanma

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ı açık bir şekilde paylaşılabilir. Böyle olmadığı durumlarda, lambda tabanlı uygulamalara veya uygun geri çağırmaları uygulayan tam sınıf içi örneklere başvurmak uygundur.

Değerler StateMachine arasında aktarılıyor

Şu ana kadar bu dokümanda yalnızca bir alt görevde çalışmanın nasıl düzenleneceği açıklanmaktadır. Ancak alt görevlerin arayana bir değer bildirmesi de gerekir. Alt görevler mantıksal olarak eşzamansız olduğundan sonuçları bir geri çağırma kullanılarak arayana geri bildirilir. Bu görevi gerçekleştirmek için alt görev, kurucusu tarafından yerleştirilen bir havuz arayüzünü 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 özelliği 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ı tekrar dağıtmalı ve kendi Caller.ResultSink öğesini tanımlamalıdır. Caller, BarProducer.ResultSink geri çağırmalarını uygular. processResult, devam ettirildiğinde bir hata oluşup oluşmadığını belirlemek için value parametresinin boş olup olmadığını kontrol eder. Bu, alt görevden veya SkyValue aramasından çıktı almayı kabul ettikten sonra yaygın olarak kullanılan bir davranış kalıbıdır.

acceptBarError işlevinin uygulanmasının, Hata ayıklama (ayrıştırma) özelliğinin gerektirdiği şekilde sonucu Caller.ResultSink öğesine yönlendireceğini unutmayın.

Üst düzey StateMachine öğelerinin alternatifleri Driver ve SkyFunction'lara köprü oluşturma bölümünde açıklanmıştır.

Hata işleme

Tasks.lookUp Geri çağırmalarda StateMachines InterruptedException haricindeki istisnalar atılmaz ancak geri çağırmalar olarak değerler olarak aktarılır. Bu tür geri çağırmalar genellikle özel veya semantik içerir ve tam olarak bir değer veya hatanın iletilmesini sağlar.

Bir sonraki bölümde, Skyframe hata işleme süreciyle ilgili hafif ancak önemli bir etkileşim açıklanmaktadır.

Hata ayıklama (--nokeep_getting)

Hata ayıklama esnasında, istenen tüm SkyValue değerleri mevcut olmasa bile SkyFunction yeniden başlatılabilir. Bu gibi durumlarda Tasks API sözleşmesi nedeniyle bir sonraki duruma hiçbir zaman ulaşılmaz. Bununla birlikte, StateMachine istisnayı yaymaya devam eder.

Yayılım, bir sonraki duruma ulaşılıp ulaşılmadığına bağlı olarak gerçekleşir. Bu nedenle, hata giderme geri çağırması bu görevi gerçekleştirmelidir. Dahili bir StateMachine için bu, üst geri çağırma başlatılarak gerçekleştirilir.

SkyFunction'ın arayüzü olan en üst StateMachine seviyesinde bunu, ValueOrExceptionProducer setException yöntemini çağırarak yapabilirsiniz. Bu durumda ValueOrExceptionProducer.tryProduceValue, SkyValue değerleri eksik olsa bile istisnayı atar.

Bir Driver doğrudan kullanılıyorsa makinenin işlenmesi bitmese bile SkyFunction'dan kaynaklanan yayılımlı hataları kontrol etmeniz önemlidir.

Etkinlik İşleme

Etkinlik yayınlaması gereken SkyFunctions işlevleri için bir StoredEventHandler, SkyKeyComputeState öğesine yerleştirilir ve daha sonra, bu parametrelerin kullanılmasını gerektiren StateMachine öğelerine de eklenir. Geçmişte, SkyFrame belirli bir etkinliği tekrar oynatamadığı için bıraktığından StoredEventHandler gerekliydi ancak bu durum daha sonra düzeltildi. StoredEventHandler ekleme, hata geri çağırmaların işlenmesinden kaynaklanan etkinliklerin uygulanmasını kolaylaştırdığı için korunur.

Driver ve SkyFunction'lara geçiş

Driver, belirtilen bir kök StateMachine ile başlayan StateMachine işlemlerinin yürütülmesinden sorumludur. StateMachine'ler StateMachine alt görevi yinelenen şekilde sıraya koyabileceği için 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, daha yüksek verimlilik için SkyValue aramalarını alt görevlerde gruplandırır.

Driver çevresinde, aşağıdaki API ile bir dizi sınıf oluşturulur.

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

Driver, tek bir kök StateMachine parametresini parametre olarak alır. Driver.drive çağırmak, StateMachine işlemini bir Skyframe yeniden başlatılmadan mümkün olduğu kadar yürütür. StateMachine tamamlandığında doğru, diğer durumlarda yanlış değerini döndürürse bu, tüm değerlerin mevcut olmadığını gösterir.

Driver, StateMachine eşzamanlı durumunu korur ve SkyKeyComputeState öğesine yerleştirmek için uygundur.

Driver örneğini doğrudan gösterme

StateMachine uygulamaları, sonuçlarını genellikle geri çağırmalar yoluyla bildirir. Aşağıdaki örnekte gösterildiği gibi, Driver öğesini doğrudan örneklendirmek mümkündür.

Driver, SkyKeyComputeState uygulamasına yerleştirilmiştir. Ayrıca, karşılık gelen ResultSink daha küçük bir tanım olarak uygulanır. En üst düzeyde State nesnesi, Driver değerinin karşılanacağı 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;
  }
}

ResultProducer kodu, aşağıdaki kodda gösterilmiştir.

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

Sonucun geç hesaplanmasına yönelik 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;
}

Driver yerleştiriliyor

StateMachine bir değer oluşturuyor ve istisna oluşturmuyorsa aşağıdaki örnekte gösterildiği gibi Driver öğesini 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 aşağıdakine benzer bir kod içerebilir (burada State, işleve özgü SkyKeyComputeState 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;
}

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

İstisnalara neden olabilecek StateMachines

Aksi takdirde, eşzamanlı SkyFunction koduyla eşleşecek eşzamanlı API'leri olan SkyKeyComputeState yerleştirilebilir 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 yerleştirme sürücüsü'ndeki ResultProducer sınıfına benzer ve SkyFunction ile benzer bir şekilde arayüz oluşturur. Uygulamalar, bir ResultSink tanımlamak yerine bunlardan birini yaptığında setValue veya setException yöntemini çağırır. Her ikisi de meydana geldiğinde, istisna öncelikli olur. tryProduceValue yöntemi, eşzamansız geri çağırma kodunu eşzamanlı koda köprüler ve bu kod ayarlandığında bir istisna oluşturur.

Daha önce de belirtildiği gibi, hata ayıklama sırasında, tüm girişler kullanılabilir olmadığı için makine henüz tamamlanmadı olsa bile hata oluşabilir. Bu ücreti karşılamak için tryProduceValue, makine çalıştırmadan önce bile belirlenen istisnaları atar.

Epilogue: Sonunda geri çağırmaları kaldırma

StateMachine'ler eşzamansız hesaplamalar yapmak için oldukça verimli ancak ortak kullanılan yoğun bir yöntemdir. Devamlılıklar (özellikle ListenableFuture özelliğine iletilen Runnable biçimindedir) Bazel kodunun belirli bölümlerinde yaygın biçimde görülür ancak SkyFunctions işlevlerinin analiz edilmesinde yaygın değildir. Analiz büyük ölçüde CPU'ya bağlıdır ve disk G/Ç'si için verimli ve eşzamansız API'ler yoktur. Sonunda, öğrenme eğrisi olan ve okunabilirliği engellediği için geri çağırmaları optimize etmek faydalı olacaktır.

En umut verici alternatiflerden biri Java sanal ileti dizileridir. Geri çağırma yazmak zorunda kalmamak için her şeyin yerine eşzamanlı, engelleme çağrıları gelir. Sanal iş parçacığı kaynağının, platform ileti dizisinden farklı olarak ucuz olması gerektiği için bu mümkündür. Ancak sanal mesaj dizilerinde bile basit eşzamanlı işlemleri, mesaj dizisi oluşturma ve senkronizasyon temelleriyle değiştirmek çok pahalıdır. StateMachine sanal makinelerinden Java sanal ileti dizilerine taşıma işlemi gerçekleştirdik. Bunların ileti dizisi daha yavaştı ve sonuçta uçtan uca analiz gecikmesinde neredeyse 3 kat artış görüldü. Sanal ileti dizileri hâlâ bir önizleme özelliği olduğundan bu taşıma işleminin, performansın düzeldiği daha sonraki bir tarihte gerçekleştirilmesi mümkündür.

Göz önünde bulundurulması gereken bir başka yaklaşım da, Loom coroutine'lerin kullanıma sunulmasını beklemektir. Bunun avantajı, ortak çalışmayla çoklu görev kullanarak senkronizasyon ek yükünü azaltmanın mümkün olmasıdır.

Diğer tüm işlemler başarısız olursa alt düzey bayt kodu yazma işlemi de uygun bir alternatif olabilir. Yeterli optimizasyonla, elle yazılmış geri çağırma koduna yaklaşan bir performans elde etmek mümkün olabilir.

Ek

Geri Çağırma

Geri çağırma işlemi, geri çağırma yöntemi kullanan eşzamansız bir kodda yaygın olarak karşılaşılan bir sorundur. Bunun nedeni, sonraki adımın devamının önceki adımın içine yerleştirilmiş olmasıdır. Çok sayıda adım varsa bu iç içe yerleştirme son derece derin olabilir. Kod, denetim akışıyla birleştirilirse kullanılamaz 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 değişkenleri kullanmak hantal olabilir. Derin bağlantı, aşağıda gösterildiği gibi, lambda yerine devam yöntemi olarak yöntem referanslarının kullanılmasıyla önlenebilir.

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 şekilde kullanıldığında geri çağırma cebine de rastlanabilir ancak bu durum, sıralı adımlarla enjeksiyon eklenmesiyle önlenebilir.

Örnek: Zincirli SkyValue aramaları

Uygulama mantığı genellikle Skyky aramalarının bağlı zincirlerini gerektirir (örneğin, ikinci SkyKey ilk SkyValue'ya bağlıysa). Bu yaklaşımın doğal olarak dikkate alınması, karmaşık ve derinlemesine iç içe yerleştirilmiş bir geri çağırma yapısıyla sonuçlanır.

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ği için kod, eyalet geçişlerinde yordam olarak görünür: step2, step1 sonrasında gelir. Burada, value2 atamak için lambda kullanılır. Böylece kodun sıralaması, hesaplamanın yukarıdan aşağıya doğru sıralamasıyla eşleşir.

Çeşitli İpuçları

Okunabilirlik: Yürütme Sıralaması

Okunabilirliği iyileştirmek için StateMachine.step uygulamalarını kod içinde iletildikleri yerden hemen sonra yürütme ve geri çağırma uygulamalarında tutmaya çalışın. Kontrol akışı şubeleri olduğunda bu her zaman mümkün değildir. Bu tür durumlarda ek yorumlar yararlı olabilir.

Örnek: Zincirli SkyValue aramaları, bunu başarmak için bir ara yöntem referansı oluşturur. Bu işlem, okunabilirlik için az miktarda bir performans sağlar. Bu durum büyük olasılıkla uygundur.

Nesil Hipotez

Orta ömürlü Java nesneleri, çok kısa bir süre boyunca yaşayan nesneleri veya sonsuza kadar yaşayan nesneleri işlemek için tasarlanmış Java çöp toplayıcısının nesil hipotezini bozar. Doğası gereği, SkyKeyComputeState hipotezini ihlal eden nesneler. Hâlâ devam eden tüm StateMachine'lerin yapılandırılmış ağacını içeren ve Driver kökünde bulunan bu nesnelerin askıya alınma süresi ortancadır ve eşzamansız hesaplamaların tamamlanması beklenir.

JDK19'da bu durum daha kötü görünüyor ancak StateMachine'leri kullanırken bazen gerçek çöp oranında ciddi düşüşler olsa bile GC süresinde artış gözlemlenebilir. StateMachine ömürleri orta seviyede olduğu için eski nesle daha hızlı dolabilirler. Bu da temizlenmeleri için daha pahalı büyük veya tam GC'lerin kullanılmasını gerektirir.

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

StateMachine değişkenleri için işlemleri alt görevlere bölmek ve StateMachine değerleri arasında yayılım için önerilen kalıbı uygulamak da faydalıdır. Kalıbı izlerken yalnızca StateMachine alt öğelerinin üst StateMachine alt öğelerine başvuruyor (bunun tersi de geçerlidir). Bu, çocuklar sonuç geri çağırmalarını kullanarak ebeveynleri doldurup güncelledikçe çocukların doğal olarak kapsam dışında kalacağı ve GC'ye uygun olacağı anlamına gelir.

Son olarak, bazı durumlarda önceki durumlarda değil, bir StateMachine değişkeni gerekir. Artık gerekli olmadığı bilinen büyük nesnelere yapılan referansları boş hale getirmek faydalı olabilir.

Adlandırma durumları

Bir yönteme ad verirken, genellikle bir yöntemde gerçekleşen davranış için bir yöntem adlandırılabilir. Yığın olmadığı için StateMachine uygulamasında bunu nasıl yapacağınız daha az anlaşılır. Örneğin, foo yönteminin bir bar alt yöntemi çağırdığını varsayalım. Bu ad, StateMachine dilinde foo durum sırasına ve ardından bar çevrilebilir. foo artık bar davranışını içermiyor. Bu nedenle, eyaletlerin yöntem adları daha dar olabilir ve yerel davranışları yansıtır.

Eşzamanlılık şeması

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

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


  1. Skyframe'in değerleri kullanılabilir olmadığında en baştan yeniden başlatması yaklaşımının aksine. 

  2. step, InterruptedException değerini atabilir, ancak örneklerde bunu göz ardı edilmektedir. Bazel kodunda bu istisnayı atlayan birkaç düşük yöntem vardır ve bu durum, daha sonra açıklanacak olan ve StateMachine özelliğini çalıştıran Driver öğesine kadar yayılır. Gerektiğinde bunun atılacağını açıklamamak sorun değildir.

  3. Eşzamanlı alt görevler, her bir bağımlılık için bağımsız çalışmalar gerçekleştiren ConfiguredTargetFunction tarafından motive edildi. Tüm bağımlıları aynı anda işleyen karmaşık veri yapılarını manipüle etmek yerine, verimsizlikler ortaya çıkararak her bir bağımlının kendi bağımsız StateMachine bileşeni vardır.

  4. Tek bir adımda birden çok tasks.lookUp çağrısı bir arada gruplandırılır. Eşzamanlı alt görevlerde yapılan aramalarla ek toplu işlem oluşturulabilir. 

  5. Bu, kavramsal olarak Java'nın yapılandırılmış eşzamanlılık jep/428 özelliğine benzer. 

  6. Bu işlem, ileti dizisi oluşturmak ve ileti dizisini birleştirerek sıralı kompozisyon elde etmek gibidir.