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.
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 StateMachine
herhangi 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.
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ı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, SkyKeyComputeState
iç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.
-
Skyframe'in değerleri kullanılabilir olmadığında en baştan yeniden başlatması yaklaşımının aksine. ↩
-
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 veStateMachine
özelliğini çalıştıranDriver
öğesine kadar yayılır. Gerektiğinde bunun atılacağını açıklamamak sorun değildir.↩ -
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ızStateMachine
bileşeni vardır.↩ -
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. ↩ -
Bu, kavramsal olarak Java'nın yapılandırılmış eşzamanlılık jep/428 özelliğine benzer. ↩
-
Bu işlem, ileti dizisi oluşturmak ve ileti dizisini birleştirerek sıralı kompozisyon elde etmek gibidir. ↩