Skyframe StateMachines Kılavuzu

Sorun bildirin Kaynağı göster

Genel bakış

Skyframe StateMachine, yığında bulunan yapılandırılmış bir işlev nesnesidir. Gerekli değerler hemen mevcut olmadığında ancak eşzamansız olarak hesaplandığında, esnek ve yedekli1 olmayan değerlendirmeleri destekler. StateMachine, beklemedeyken bir iş parçacığı kaynağını bağlanamaz. Bunun yerine askıya alınması ve devam ettirilmesi gerekir. Bu şekilde ayrıştırma işleminde açık yeniden giriş noktaları ortaya çıkar. Böylece önceki hesaplamalar atlanabilir.

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şiktir ve tamamen mantıksaldır. Eşzamanlı her alt görev, paylaşılan tek bir üst SkyFunction iş parçacığında çalışır.

Giriş

Bu bölüm kısaca kullanıcıları motive eder ve java.com.google.devtools.build.skyframe.state paketinde bulunan StateMachine öğelerini tanıtır.

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

Skyframe, bağımlılık grafiklerinin paralel olarak değerlendirildiği bir çerçevedir. Grafikteki her düğüm, parametrelerini belirten bir SkyKey ve sonucunu belirten SkyValue'nun belirtildiği bir SkyFunction'ın değerlendirmesine karşılık gelir. Bu hesaplama modeli, SkyFunction'ın SkyKey ile SkyValues'u arayarak ek SkyFunctions'ın yinelemeli ve paralel değerlendirmesini tetikleyebilmesini sağlar. İstekte bulunan SkyValue, hesaplamanın bir alt grafiği tamamlanmadığı için iş parçacığı bağlamak yerine engelleme yerine null getValue yanıtı gözlemler ve eksik girişler nedeniyle tamamlanmadığını belirten SkyValue yerine null yanıtı döndürür. Skyframe, daha önce istenen tüm SkyValue'lar kullanılabilir olduğunda 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ı. Burada ikinci dereceden karmaşıklığa sahip olsa da bu şekilde yazılan işlevler her tekrar çalıştırıldığında daha az arama null sonucunu döndürür. SkyKeyComputeState sayesinde, elle belirtilen kontrol noktası verilerini SkyFunction ile ilişkilendirerek önemli yeniden hesaplama işlemleri yapabilirsiniz.

StateMachine öğeleri, SkyKeyComputeState içinde yaşayan ve askıya alma ve devam ettirme kancalarını açığa çıkararak SkyFunction yeniden başlatıldığında (SkyKeyComputeState, önbellekten düştüğünde) neredeyse tüm yeniden hesaplama işlemlerini ortadan kaldırır.

SkyKeyComputeState içindeki durum bilgili 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. Bir StateMachine, merakla tekrarlanan şöyle bir tanıma sahiptir2.

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

Tasks arayüzü, SkyFunction.Environment arayüzüne benzer, ancak eşzamansız kullanım için tasarlanmıştır ve mantıksal olarak eşzamanlı alt görevler için destek ekler3.

step işlevinin döndürülen değeri başka bir StateMachine'dir. Bu değer, bir dizi adımın tümleşik olarak spesifikasyonuna 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ışa sahip bir StateMachine öğesini açıklar.

hello
world

this::step2 yöntem referansının, step2 StateMachine ürününün işlevsel arayüz tanımına uygun olması nedeniyle bir StateMachine olduğunu unutmayın. Yöntem referansları, StateMachine içindeki sonraki durumu belirtmenin en yaygın yoludur.

Askıya alma ve devam ettirme

Bir hesaplamayı monolitik bir işlev yerine StateMachine adımlarına ayırmak, hesaplamayı suspend ve suspend için gereken kancaları sağlar. StateMachine.step geri döndüğünde açık bir askıya alma noktası uygulanır. Döndürülen StateMachine değeri tarafından belirtilen devamlılık, açık bir devam ettirme noktasıdır. Böylece, hesaplamaya tam olarak kaldığı yerden devam edebileceği için yeniden hesaplamadan kaçınabilirsiniz.

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

Teknik açıdan bakıldığında StateMachine, yürütülecek sonraki hesaplamayı belirleyen continuation görevi görür. StateMachine, engellemek yerine, kontrolü tekrar Driver örneğine aktaran step işlevinden dönerek suspend. Ardından Driver, hazır bir StateMachine moduna geçebilir veya kontrolü tekrar Skyframe'e bırakabilir.

Geleneksel olarak callbacks ve callbacks işlemleri tek bir kavramda kullanılır. Ancak StateMachine öğeleri arasında bir ayrım vardır.

  • Geri çağırma, eşzamansız bir hesaplama sonucunun nerede depolanacağını açıklar.
  • Devam: Bir sonraki yürütme durumunu belirtir.

Eşzamansız bir işlem çağrılırken geri çağırmalar gerekir. Yani, SkyValue aramasında olduğu gibi, yöntem çağrıldıktan hemen sonra gerçek işlem gerçekleşmez. Geri çağırma işlevleri mümkün olduğunca basit olmalıdır.

Devamlar, StateMachine değerlerinin StateMachine döndürüldüğü değerlerdir ve tüm eşzamansız hesaplamaların çözümlenmesinin ardından gelen karmaşık yürütme işlemini kapsüller. Bu yapılandırılmış yaklaşım, geri çağırmaların karmaşıklığını yönetmeye yardımcı olur.

Görevler

Tasks arayüzü, StateMachine kullanıcılarına SkyKey ile SkyValues'u aramak ve eşzamanlı alt görevler planlamak için bir API sunar.

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, SkyValues'u aramak için Tasks.lookUp aşırı yükleme kullanıyor. 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, mümkün olduğunca fazla sayıda aramayı4 gruplar.

StateMachine işlemcisi (Driver'ler ve SkyFrame'e köprü), değerin bir sonraki durum başlamadan önce kullanılabileceğini garanti eder. 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() araması yapar ve tüketici olarak this değerini iletir. DoesLookup, Consumer<SkyValue> yöntemini uyguladığı için bu mümkündü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, processValue üzerinden erişildiğinde value kullanılabilir.

Alt görevler

Tasks.enqueue, mantıksal olarak eşzamanlı alt görevlerin yürütülmesini ister. Alt görevler aynı zamanda StateMachine'tır ve yinelenen şekilde daha fazla alt görev oluşturmak veya SkyValue'ları aramak da dahil olmak üzere normal StateMachine'ların yapabildiği her şeyi yapabilir. lookUp gibi, durum makinesi sürücüsü de bir sonraki adıma geçmeden önce tüm alt görevlerin tamamlandığından emin olur. 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 "eş zamanlı" güncellemesi için herhangi bir senkronizasyon yapılması 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

Eşzamanlılık yapısının bir ağaç oluşturduğunu UML'den 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ı

Yapılandırılmış eşzamanlılık hakkında akıl yürütebilmek çok daha kolaydır.

Düzen ve kontrol akışı kalıpları

Bu bölümde, birden fazla StateMachine öğesinin nasıl oluşturulabileceğine dair örnekler ve belirli kontrol akışı sorunlarının çözümleri sunulmaktadır.

Sıralı durumlar

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

Dallara ayırma

Aşağıdaki örnekte gösterildiği gibi, StateMachine saniyelerdeki dallara ayırma durumları normal Java kontrol akışı kullanılarak farklı değerler döndürülerek sağlanabilir.

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 tamamlanmak için DONE iade etmesi çok yaygın bir durumdur.

Gelişmiş sıralı bileşim

StateMachine kontrol yapısı belleksiz olduğundan, alt görevler bazen tuhaf olabildiğinden StateMachine tanımlarını paylaşmak. M1 ve M2 bir StateMachine, S'yi paylaşan StateMachine örnekleri olsun. M1 ve M2 sırasıyla <A, S, B> ve <X, S, Y> dizileridir. Sorun, S'nin işlem tamamlandıktan sonra B'ye mi yoksa Y'ye mi geçeceğini bilmemesi ve StateMachine'ların tam olarak bir çağrı yığını tutmamasıdır. Bu bölümde, bunu başarmaya yönelik bazı teknikler incelenmektedir.

Terminal sıra öğesi olarak StateMachine

Bu ise ilk ortaya çıkan sorunu çözmez. Sıralı bileşimi, yalnızca paylaşılan StateMachine, dizide terminal olduğunda 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();
  }
}

S karmaşık bir durum makinesi olsa bile bu yöntem işe yarar.

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

Kuyruğa alınan alt görevlerin sonraki durumdan önce tamamlanması garanti edildiğinden, alt görev mekanizmasının biraz kötüye kullanılması6 bazen 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

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 öğesini kötüye kullanmak mümkün değildir. Bu durumda S içine runAfter parametresi eklemek, bir sonraki adımda S'ye bilgi vermek amacıyla 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 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 gider. Bunun yerine, sıralı runAfter öğelerinin normal sıralı durumlarla bölünmesi 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);
  }
runAfter

Yasak alternatifi: runAfterUnlessError

Daha önceki bir taslakta, hataları erken iptal edecek bir runAfterUnlessError kullanmayı düşünüyorduk. Hataların genellikle iki kez kontrol edilmesi, runAfter referansına sahip StateMachine tarafından bir kez, runAfter makinesinin kendisi tarafından kontrol edilmesi de bu hatadan kaynaklanmaktadır.

Biraz kafa yorduktan sonra, kodun tek tipliğinin, hata kontrolünü tekilleştirmekten daha önemli olduğuna karar verdik. runAfter mekanizması, her zaman hata kontrolü gerektiren tasks.enqueue mekanizmasıyla tutarlı bir şekilde çalışmasaydı kafa karıştırıcı olurdu.

Doğrudan yetki

Resmi durum geçişi her yapıldığında ana Driver döngüsü ilerler. Sözleşmeye göre, ilerleme durumları, daha önce sıraya alınan tüm SkyValue aramalarının ve alt görevlerinin bir sonraki durum yürütülmeden önce çözümlenmesi anlamına gelir. Bazen yetki verilen bir StateMachine işlevinin mantığı, bir aşama ilerlemesini gereksiz veya olumsuz hale getirir. Örneğin, yetki verilen kullanıcının ilk step kullanıcısı, yetki veren ülkenin aramalarıyla paralel hale getirilebilecek SkyKey aramaları gerçekleştiriyorsa bir aşama avansı, bu aramaları sıralı hale getirir. 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ışı

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

Tasks.lookUp geri çağırma uygulama

SkyValue aramalarında bir Tasks.lookUp geri çağırması uygulamaya ilişkin bir örneği bulabilirsiniz. Bu bölümde, nedenler gösteriliyor ve birden fazla SkyValue'nun nasıl işleneceğiyle ilgili yaklaşımlar belirtilmiştir.

Tasks.lookUp geri arama

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

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

Deyimsel yaklaşım, şunu uygulamak için bir Java lambdası kullanmak olur:

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

myValue, aramayı yapan StateMachine örneğinin üye değişkenidir. Ancak lambda, StateMachine uygulamasında Consumer<SkyValue> arayüzünün uygulanmasına kıyasla ek bellek tahsisi gerektirir. Belirsiz olacak birden fazla arama olduğunda lambda yine de yararlıdır.

Ayrıca, SkyFunction.Environment.getValueOrThrow koduna benzeyen Tasks.lookUp aşırı yüklemeleri işlenirken de hatalar meydana gelir.

  <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ö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, 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 fazla ayrıntı sağlar ancak esasen hataların yayılımı ile normal değerler arasında çok fazla fark yoktur.

Birden çok SkyValues kullanma

Genellikle birden çok SkyValue araması yapılması gerekir. SkyValue türünü açmak, çoğu zaman işe yarar bir yaklaşımdır. 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ğu için Consumer<SkyValue> geri çağırma uygulaması açık bir şekilde paylaşılabilir. Böyle bir durum söz konusu olmadığında, uygun geri çağırmaları uygulayan lambda tabanlı uygulamalara veya tam sınıf içi örneklere geri dönmek uygulanabilir.

Değerleri StateMachine saniyeler 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 çalışmasını sağlamak için alt görev, oluşturucusu yoluyla yerleştirilen bir havuz 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;
  }
}

Bu durumda arayan (StateMachine) 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 açıklanmaktadır. Caller, sonuçlarını geri yaymalıdır ve kendi Caller.ResultSink değerini tanımlar. Caller, BarProducer.ResultSink geri çağırmasını 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 işlevinin, Hata baloncuklarının gerektirdiği şekilde sonucu Caller.ResultSink öğesine yönlendireceğini unutmayın.

Üst düzey StateMachine için alternatifler, Driver öğelerinde ve SkyFunctions'a köprü oluşturma konusunda açıklanmaktadır.

Hata işleme

Halihazırda Tasks.lookUp geri çağırma işlevinde ve StateMachines arasında değer çoğaltırken hata işlemeyle ilgili birkaç örnek bulunmaktadır. InterruptedException dışındaki istisnalar atılmaz, bunun yerine geri çağırmalardan değer olarak geçirilir. Bu tür geri çağırmalar genellikle özel veya anlamı vardır ve tam olarak bir değer ya da hata iletilmektedir.

Bir sonraki bölümde Skyframe hata işlemesiyle ilgili incelikli ancak önemli bir etkileşim açıklanmaktadır.

Hata kabarıklığı (--nokeep_continue)

Hata şişirme sırasında, istenen tüm SkyValue'lar kullanılabilir 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, istisnayı yine de yaymaya devam edecektir.

Sonraki duruma ulaşılıp ulaşılmadığından bağımsız olarak dağıtımın gerçekleşmesi gerektiğinden, geri çağırma işleme hatasının bu görevi gerçekleştirmesi gerekir. Dahili StateMachine için bu, üst çağrının çağrılmasıyla gerçekleştirilir.

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

Driver doğrudan kullanılıyorsa makinenin işlemeyi tamamlamamış olsa bile SkyFunction'dan yayılmış hataları kontrol etmeniz gerekir.

Etkinlik İşleme

Etkinlik yayınlaması gereken SkyFunctions için SkyKeyComputeState'e StoredEventHandler ve bu öğelerin gerekli olduğu 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şlemelerden kaynaklanan etkinliklerin uygulanmasını basitleştirdiği için StoredEventHandler ekleme işlemi korunur.

Driver'ler ve SkyFunctions'a köprü

Driver, belirtilen bir kök StateMachine ile başlayan StateMachine öğelerinin yürütülmesini yönetmekten 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 sonucu olarak bir ağaç yapısı oluşturur. Driver, verimliliği artırmak için SkyValue aramalarını alt görevlerde toplu olarak yapar.

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ıldığında StateMachine öğesi Skyframe yeniden başlatılmadan çalışabileceği kadar yürütülebilir. StateMachine tamamlandığında "doğru", aksi takdirde, tüm değerlerin kullanılamadığını belirterek "yanlış" değerini döndürür.

Driver, StateMachine öğesinin eşzamanlı durumunu korur ve SkyKeyComputeState hücresine yerleştirme için çok uygundur.

Driver öğesini doğrudan örneklendirme

StateMachine uygulamaları, sonuçlarını geleneksel olarak geri çağırma yoluyla iletir. Aşağıdaki örnekte gösterildiği gibi bir Driver örneği doğrudan oluşturulabilir.

Driver, biraz daha aşağıda tanımlanacak karşılık gelen ResultSink uygulamasıyla birlikte SkyKeyComputeState uygulamasına yerleştirilmiş. En üst düzeyde State nesnesi, Driver süresinin biteceği 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 taslağını çiziyor.

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 geç hesaplama kodu 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 öğe yerleştiriliyor

StateMachine bir değer üretir ve herhangi bir istisna oluşturmuyorsa aşağıdaki örnekte gösterildiği gibi Driver yerleştirilmiş başka bir olası 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 koda sahip olabilir (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;
}

Driver öğesinin StateMachine uygulamasına yerleştirilmesi, Skyframe'in eşzamanlı kodlama stiline daha uygun bir seçenektir.

İstisna oluşturabilecek StateMachines

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şik bir Driver örneği içerir, Yerleştirme sürücüsü'ndeki ResultProducer sınıfına çok benzer ve SkyFunction ile benzer şekilde arayüz oluşturur. Uygulamalar bir ResultSink tanımlamak yerine, bunlardan biri gerçekleştiğinde setValue veya setException yöntemini çağırır. Her ikisi de gerçekleştiğinde istisna, öncelik kazanır. 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 belirtildiği gibi, hata köprüleme sırasında, tüm girişler kullanılabilir olmadığı için makine henüz bitmemiş olsa bile bir hata oluşabilir. Buna uyum sağlamak için tryProduceValue, makine tamamlanmadan önce bile ayarlanmış tüm istisnaları uygular.

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

StateMachine'ler, eşzamansız hesaplamalar yapmak için son derece verimli ancak yoğun bir şekilde kullanılan ortak yöntemlerdir. Bazel kodunun belirli bölümlerinde devamlı kullanımlar (özellikle de ListenableFuture'a iletilen Runnable sayısı) yaygın olsa da SkyFunctions analizinde sık görülmez. Analysis çoğunlukla CPU'ya bağlıdır ve disk G/Ç için verimli bir eşzamansız API yoktur. Sonuç olarak, bir öğrenme eğrisi olan ve okunabilirliği engelledikleri için geri çağırmaların optimize edilmesi iyi olur.

En umut verici alternatiflerden biri, Java sanal iş parçacıklarıdır. Geri çağırma yazmak zorunda kalmak yerine, her şeyin yerini eşzamanlı ve engelleyen çağrılarla alır. Platform iş parçacığının aksine sanal iş parçacığı kaynağını bağlamanın ucuz olması gerektiği için bu mümkündür. Ancak sanal iş parçacıklarında bile basit eşzamanlı işlemlerin iş parçacığı oluşturma ve senkronizasyon temel öğeleriyle değiştirilmesi çok pahalıdır. StateMachine'lerden Java sanal iş parçacıklarına geçiş yaptık ve bu iş parçacıkları çok büyük ölçüde daha yavaştı. Bu da uçtan uca analiz gecikmesinde neredeyse 3 kat artış sağladı. Sanal iş parçacıkları hâlâ bir önizleme özelliği olduğundan bu taşıma işleminin, performansın artacağı daha sonraki bir tarihte gerçekleştirilmesi mümkündür.

Dikkate alınması gereken bir diğer yaklaşım da Loom eş yordamlarının kullanıma sunulursa beklemektir. Bunun avantajı, iş birliğine dayalı çoklu görev kullanarak senkronizasyon ek yükünün azaltılmasıdır.

Diğer tüm yöntemler başarısız olursa düşük düzeyde bayt kodu yeniden yazma yöntemi de uygun bir alternatif olabilir. Yeterli optimizasyonla, elle yazılmış geri arama koduna yaklaşım getiren bir performans elde etmek mümkün olabilir.

Ek

Cehennem Geri Çağırma

Geri çağırma cehennemi, eşzamansız kodda geri çağırmaların kullanıldığı, çok bilinen bir sorundur. Bu, sonraki adımın devamının, önceki adımla iç içe yerleştirilmiş olmasından kaynaklanır. Birçok adım varsa iç içe yerleştirme işlemi son derece derin olabilir. Kontrol akışıyla birleştirilirse 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şkenlerinin etkili bir şekilde nihai olması gerekir. Bu nedenle, bu tür değişkenlerin kullanılması zahmetli olabilir. Derin iç içe yerleştirme, aşağıdaki gibi lambdalar yerine devamlılık olarak yöntem referanslarının döndürülmesiyle ö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 ekleme kalıbı çok yoğun bir şekilde kullanıldığında da geri çağırma cehennemi meydana gelebilir. Ancak sıralı adımlarla araya ekleme yaparak bu durumu önleyebilirsiniz.

Örnek: Zincirli SkyValue aramaları

Çoğu durumda uygulama mantığı, bağlı SkyValue arama zincirleri gerektirir (örneğin, ikinci bir SkyKey, ilk SkyValue'ya bağlıysa). Bunu naif 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;
}

Bununla birlikte, devamlılıklar yöntem referansları olarak belirtildiğinden, kod durum geçişlerinde prosedürel görünür: step2, step1'ın ardından gelir. Burada, value2 öğesini 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 iyileştirmek için StateMachine.step uygulamalarını yürütme sırasında tutmaya çalışın. Geri çağırma uygulamalarını ise kodda iletildikleri yerden hemen sonra tutun. 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. Bununla birlikte, biraz performanstan ödün vererek okunabilirliği öne çıkarır.

Kuşak Hipotez

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 nesilsel hipotezini çürütür. Tanımı gereği, SkyKeyComputeState içindeki nesneler bu hipotezi ihlal eder. Kökü Driver olan ve hâlâ çalışan tüm StateMachine'lerin yapılandırılmış ağacını içeren bu tür nesneler, askıya alındıkça ara bir ömür boyuna sahiptir ve eşzamansız hesaplamaların tamamlanmasını bekler.

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 kullanım ömrü orta düzeyde olduğu için eski nesline yükseltilebilirler. Bu da daha hızlı dolmasına neden olur. Bu durumda, büyük veya tam GC'lerin temizlenmesi daha pahalı olur.

İ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 gerekliyse bu mümkün değildir. Mümkün olduğunda, yerel yığın step değişkenleri yeni nesil değişkenlerdir ve verimli bir şekilde GC'ye eklenir.

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ıp uygulanırken yalnızca alt StateMachine öğelerinin üst StateMachine öğeleri için referansa sahip olduğunu ve bunun tersinin 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.

Adlandırma eyaletleri

Bir yöntemi adlandırırken, genellikle bu yöntem içinde gerçekleşen davranış için bir yöntem belirtmek mümkündür. Yığın olmadığından, StateMachine içinde bunun nasıl yapılacağı daha az açık. Örneğin, foo yönteminin bir 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, eyaletlere ilişkin yöntem adlarının kapsamı daha dar olma eğilimindedir. Bu da yerel davranışı yansıtabilir.

Eşzamanlılık ağacı diyagramı

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şturur.

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


  1. Skyframe'in değerler mevcut olmadığında baştan yeniden başlatma yönteminin aksine. 

  2. step işlevinin InterruptedException öğesini yayınlamasına izin verilir, ancak örneklerde bu atlanır. Bazel kodunda bu istisnayı devre dışı bırakan birkaç düşük yöntem vardır ve bu istisna, daha sonra açıklanacak olan StateMachine öğesini çalıştıran Driver etiketine kadar yayılır. Gerektiğinde atıldığını bildirmemekte bir sakınca yoktur.

  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ı manipüle etmek ve verimsizlik yaratmak yerine her bağımlılığın kendine ait bağımsız StateMachine vardır.

  4. Tek bir adımda 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, kavram olarak Java'nın yapılandırılmış eş zamanlılık jeps/428 özelliğine benzer. 

  6. Bu işlem, sıralı birleşim elde etmek için bir ileti dizisi oluşturup birleştirmeye benzer.