Genel Bakış
Skyframe StateMachine
, yığınta bulunan yapısı bozulmuş 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
, beklerken bir mesaj dizisi kaynağını bağlayamaz. Bunun yerine askıya alınıp devam ettirilmelidir. Bu nedenle, yapı sökme işlemi, önceki hesaplamaların atlanması için açık yeniden giriş noktalarını gösterir.
StateMachine
'ler, sıralamalar, dallanmalar ve yapılandırılmış mantıksal eşzamanlılığı ifade etmek için kullanılabilir ve özellikle Skyframe etkileşimine göre uyarlanmıştır. StateMachine
'ler daha büyük StateMachine
'ler halinde derlenebilir ve alt StateMachine
'ler paylaşabilir. Eşzamanlılık yapısı gereği her zaman hiyerarşiktir ve tamamen mantıksaldır. Her eşzamanlı alt görev, tek bir paylaşılan ü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'ın 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 bir SkyValue içeren bir SkyFunction'ın değerlendirmesine karşılık gelir. Hesaplama modeli, bir SkyFunction'ın SkyKey'ye göre SkyValue'ları arayabileceği şekildedir. Bu da ek SkyFunction'ların yinelemeli, paralel değerlendirmesini tetikler. İstekte bulunan SkyValue, hesaplamanın bir alt grafiği tamamlanmadığı için iş parçacığı bağlamak yerine bir null
getValue
yanıtı gözlemler ve eksik girişler nedeniyle tamamlanmadığını belirten bir SkyValue yerine null
yanıtı döndürür.
Skyframe, daha önce istenen tüm SkyValue'lar kullanılabilir olduğunda SkyFunctions'ı yeniden başlatır.
SkyKeyComputeState
kullanıma sunulmadan önce, yeniden başlatma işlemini yönetmenin geleneksel yolu hesaplamayı tamamen yeniden çalıştırmaktı. Bu yöntemin karmaşıklığı ikinci dereceden olsa da bu şekilde yazılan işlevler sonunda tamamlanır. Bunun nedeni, her yeniden çalıştırma işleminde daha az aramanın null
döndürmesidir. SkyKeyComputeState
ile manuel olarak belirtilen kontrol noktası verilerini bir SkyFunction ile ilişkilendirerek önemli ölçüde yeniden hesaplama tasarrufu elde edilebilir.
StateMachine
, SkyKeyComputeState
içinde bulunan ve SkyFunction yeniden başlatıldığında (SkyKeyComputeState
'ın önbellekten çıkmadığı varsayılarak) askıya alma ve devam ettirme yürütme kancalarını göstererek neredeyse tüm yeniden hesaplamayı ortadan kaldıran nesnelerdir.
SkyKeyComputeState
içindeki durum bilgisine sahip hesaplamalar
Nesne yönelimli tasarım açısından, salt veri değerleri yerine hesaplama nesnelerini SkyKeyComputeState
içinde depolamak mantıklı bir yaklaşımdır.
Java'da, davranış taşıyan bir nesnenin en az açıklaması bir işlevsel arayüzdür ve bu açıklama yeterlidir. StateMachine
, aşağıdaki ilginç şekilde yinelenen tanımı2 içerir.
@FunctionalInterface
public interface StateMachine {
StateMachine step(Tasks tasks) throws InterruptedException;
}
Tasks
arayüzü SkyFunction.Environment
ile benzerdir ancak eşzamansızlık için tasarlanmıştır ve mantıksal olarak eşzamanlı alt görevler için destek ekler3.
step
işlevinin döndürülen değeri başka bir StateMachine
değeridir. Bu değer, endüktif olarak bir dizi adımın spesifikasyonuna olanak tanır. 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ışı içeren bir StateMachine
'ü tanımlar.
hello
world
step2
, StateMachine
'un işlevsel arayüz tanımını karşıladığı için this::step2
yöntem referansının da StateMachine
olduğunu unutmayın. Yöntem referansları, StateMachine
içindeki sonraki durumu belirtmenin en yaygın yoludur.
Bir hesaplamayı monolitik bir işlev yerine StateMachine
adıma bölmek, hesaplamayı askıya almak ve devam ettirmek için gereken bağlantı noktalarını sağlar. StateMachine.step
döndürüldüğünde açık bir askıya alma noktası vardır. Döndürülen StateMachine
değeri tarafından belirtilen devam, açık bir devam noktasıdır. Böylece, hesaplamaya tam olarak kaldığı yerden devam edebileceği için yeniden hesaplamadan kaçınabilirsiniz.
Geri çağırma, devam ettirme ve eşzamansız hesaplama
Teknik açıdan bakıldığında StateMachine
, yürütülecek sonraki hesaplamayı belirleyen bir devam işlevi görür. StateMachine
, engellemek yerine step
işlevinden dönerek gönüllü olarak askıya alabilir. Bu durumda, kontrol bir Driver
örneğine geri aktarılır. Driver
, hazır bir StateMachine
'e geçebilir veya kontrolü Skyframe'a geri verebilir.
Geleneksel olarak geri çağırma ve devam işlemleri tek bir kavram altında birleştirilir.
Ancak StateMachine
öğeleri arasında bir ayrım vardır.
- Geri çağırma: Asenkron bir hesaplamanın sonucunun nereye depolanacağını tanımlar.
- Devam: Sonraki yürütme durumunu belirtir.
Eşzamansız bir işlem çağrılırken geri çağırma işlevi gereklidir. Bu, SkyValue araması durumunda olduğu gibi, gerçek işlemin yöntem çağrıldıktan hemen sonra gerçekleşmediği anlamına gelir. Geri çağırma işlevleri mümkün olduğunca basit tutulmalıdır.
Devamlar, StateMachine
işlevlerinin StateMachine
döndürdüğü değerlerdir ve tüm asenkron hesaplamalar çözüldükten sonra gelen karmaşık yürütmeyi kapsar. Bu yapılandırılmış yaklaşım, geri çağırma işlemlerinin karmaşıklığını yönetilebilir düzeyde tutmaya yardımcı olur.
Görevler
Tasks
arayüzü, StateMachine
'lara SkyKey'e göre SkyValue'ları aramak ve eşzamanlı alt görevleri planlamak için bir API sağlar.
interface Tasks {
void enqueue(StateMachine subtask);
void lookUp(SkyKey key, Consumer<SkyValue> sink);
<E extends Exception>
void lookUp(SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);
// lookUp overloads for 2 and 3 exception types exist, but are elided here.
}
SkyValue aramaları
StateMachine
s, SkyValues'ı aramak için Tasks.lookUp
aşırı yüklemelerini 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. Bunun yerine, aramayı yapmadan önce mümkün olduğunca çok sayıda aramayı4 gruplandırır. Değer hemen kullanılamayabilir (ör. Skyframe'ın yeniden başlatılmasını gerektirebilir). Bu nedenle, arayan, geri çağırma işlevi kullanarak elde edilen değerle ne yapılacağını belirtir.
StateMachine
işleyicisi (Driver
'ler ve SkyFrame'a köprü oluşturma), değerin bir sonraki durum başlamadan önce kullanılabileceğini garanti eder. Aşağıda bir örnek verilmiştir.
class DoesLookup implements StateMachine, Consumer<SkyValue> {
private Value value;
@Override
public StateMachine step(Tasks tasks) {
tasks.lookUp(new Key(), (Consumer<SkyValue>) this);
return this::processValue;
}
// The `lookUp` call in `step` causes this to be called before `processValue`.
@Override // Implementation of Consumer<SkyValue>.
public void accept(SkyValue value) {
this.value = (Value)value;
}
private StateMachine processValue(Tasks tasks) {
System.out.println(value); // Prints the string representation of `value`.
return DONE;
}
}
Yukarıdaki örnekte ilk adım new Key()
araması yapar ve tüketici olarak this
değerini iletir. Bunun nedeni, DoesLookup
'ün Consumer<SkyValue>
'ü uygulamasıdır.
Sözleşmeye göre, sonraki DoesLookup.processValue
durumu başlamadan önce DoesLookup.step
için tüm aramalar tamamlanı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
ile benzer şekilde durum makinesi sürücüsü, bir sonraki adıma geçmeden önce tüm alt görevlerin tamamlanmasını sağlar. Aşağıda bir örnek verilmiştir.
class Subtasks implements StateMachine {
private int i = 0;
@Override
public StateMachine step(Tasks tasks) {
tasks.enqueue(new Subtask1());
tasks.enqueue(new Subtask2());
// The next step is Subtasks.processResults. It won't be called until both
// Subtask1 and Subtask 2 are complete.
return this::processResults;
}
private StateMachine processResults(Tasks tasks) {
System.out.println(i); // Prints "3".
return DONE; // Subtasks is done.
}
private class Subtask1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
i += 1;
return DONE; // Subtask1 is done.
}
}
private class Subtask2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
i += 2;
return DONE; // Subtask2 is done.
}
}
}
Subtask1
ve Subtask2
mantıksal olarak eşzamanlı olsa da her şey tek bir iş parçacığında çalıştığından i
'nin "eşzamanlı" güncellemesi için senkronizasyon gerekmez.
Yapılandırılmış eşzamanlılık
Her lookUp
ve enqueue
, sonraki duruma geçmeden önce çözülmelidir. 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şturabilirsiniz.
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ılmış eşzamanlılığın mantığını anlamak çok daha kolaydır.
Kompozisyon ve kontrol akışı kalıpları
Bu bölümde, birden fazla StateMachine
'ün nasıl derlenebileceğine dair örnekler ve belirli kontrol akışı sorunlarına yönelik çözümler sunulmaktadır.
Sıralı durumlar
Bu, en yaygın ve basit kontrol akışı modelidir. Buna örnek olarak SkyKeyComputeState
içindeki duruma bağlı hesaplamalar bölümünü inceleyebilirsiniz.
Dallanma
StateMachine
'lerde dallanma durumlarına, aşağıdaki örnekte gösterildiği gibi normal Java kontrol akışı kullanılarak farklı değerler döndürülerek ulaşılabilir.
class Branch implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
// Returns different state machines, depending on condition.
if (shouldUseA()) {
return this::performA;
}
return this::performB;
}
…
}
Belirli şubelerin erken tamamlama için DONE
döndürmesi çok yaygındır.
Gelişmiş sıralı kompozisyon
StateMachine
kontrol yapısı bellek içermediğinden, StateMachine
tanımlarını alt görev olarak paylaşmak bazen zor olabilir. M1 ve M2, S adlı bir StateMachine
paylaşan StateMachine
örnekleri olsun. M1 ve M2 sırasıyla <A, S, B> ve <X, S, Y> sıralı dizilerdir. Sorun, S'nin 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 dizisi öğesi olarak StateMachine
Bu işlem, ortaya atılan ilk 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();
}
}
Bu, S'nin kendisi karmaşık bir durum makinesi olsa bile çalışır.
Sıralı beste için alt görev
Sıraya eklenen alt görevlerin sonraki durumdan önce tamamlanacağı garanti edildiğinden, bazen alt görev mekanizmasından biraz6 kötüye yararlanmak mümkündür.
class M1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performA();
// S starts after `step` returns and by contract must complete before `doB`
// begins. It is effectively sequential, inducing the sequence < A, S, B >.
tasks.enqueue(new S());
return this::doB;
}
private StateMachine doB(Tasks tasks) {
performB();
return DONE;
}
}
class M2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performX();
// Similarly, this induces the sequence < X, S, Y>.
tasks.enqueue(new S());
return this::doY;
}
private StateMachine doY(Tasks tasks) {
performY();
return DONE;
}
}
runAfter
ekleme
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'ye bir runAfter
parametresi ekleyerek S'yi ne yapacağını bildirebilirsiniz.
class S implements StateMachine {
// Specifies what to run after S completes.
private final StateMachine runAfter;
@Override
public StateMachine step(Tasks tasks) {
… // Performs some computations.
return this::processResults;
}
@Nullable
private StateMachine processResults(Tasks tasks) {
… // Does some additional processing.
// Executes the state machine defined by `runAfter` after S completes.
return runAfter;
}
}
class M1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performA();
// Passes `this::doB` as the `runAfter` parameter of S, resulting in the
// sequence < A, S, B >.
return new S(/* runAfter= */ this::doB);
}
private StateMachine doB(Tasks tasks) {
performB();
return DONE;
}
}
class M2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performX();
// Passes `this::doY` as the `runAfter` parameter of S, resulting in the
// sequence < X, S, Y >.
return new S(/* runAfter= */ this::doY);
}
private StateMachine doY(Tasks tasks) {
performY();
return DONE;
}
}
Bu yaklaşım, alt görevlerden kötüye kullanmaktan daha temizdir. Ancak bu özelliği çok serbest bir şekilde uygulamak (örneğin, birden fazla StateMachine
öğesini runAfter
içine yerleştirerek) geri çağırma cehennemine yol açar. Bunun yerine, sıralı runAfter
öğelerinin normal sıralı durumlarla bölünmesi daha iyidir.
return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))
aşağıdakilerle değiştirilebilir.
private StateMachine step1(Tasks tasks) {
doStep1();
return new S(/* runAfter= */ this::intermediateStep);
}
private StateMachine intermediateStep(Tasks tasks) {
return new T(/* runAfter= */ this::nextStep);
}
Yasaklanmış alternatif: runAfterUnlessError
Önceki bir taslakta, hataları erkenden sonlandıracak bir runAfterUnlessError
'yi değerlendirmiştik. Bunun nedeni, hataların genellikle iki kez kontrol edilmesidir. Hatalar bir kez runAfter
referansı olan StateMachine
tarafından, bir kez de runAfter
makinesinin kendisi tarafından kontrol edilir.
Yaptığımız değerlendirmenin ardından, hata kontrolünü tekilleştirmekten daha önemli olanın kodun tekdüzeliği olduğuna karar verdik. runAfter
mekanizmasının, her zaman hata kontrolü gerektiren tasks.enqueue
mekanizmasıyla tutarlı bir şekilde çalışmaması kafa karıştırıcı olur.
Doğrudan yetki
Her resmi durum geçişinde 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 bir temsilcinin StateMachine
mantığı, aşama ilerlemesini gereksiz veya verimsiz 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 verme işlemini gerçekleştirmek daha mantıklı olabilir.
class Parent implements StateMachine {
@Override
public StateMachine step(Tasks tasks ) {
tasks.lookUp(new Key1(), this);
// Directly delegates to `Delegate`.
//
// The (valid) alternative:
// return new Delegate(this::afterDelegation);
// would cause `Delegate.step` to execute after `step` completes which would
// cause lookups of `Key1` and `Key2` to be sequential instead of parallel.
return new Delegate(this::afterDelegation).step(tasks);
}
private StateMachine afterDelegation(Tasks tasks) {
…
}
}
class Delegate implements StateMachine {
private final StateMachine runAfter;
Delegate(StateMachine runAfter) {
this.runAfter = runAfter;
}
@Override
public StateMachine step(Tasks tasks) {
tasks.lookUp(new Key2(), this);
return …;
}
// Rest of implementation.
…
private StateMachine complete(Tasks tasks) {
…
return runAfter;
}
}
Veri akışı
Bir önceki tartışmada kontrol akışının yönetilmesine odaklandık. Bu bölümde, veri değerlerinin yayılması açıklanmaktadır.
Tasks.lookUp
geri aramalarını uygulama
SkyValue aramalarında bir Tasks.lookUp
geri çağırması uygulamaya ilişkin bir örneği bulabilirsiniz. Bu bölümde, birden fazla SkyValue'ı işlemeyle ilgili gerekçeler sunulmakta ve yaklaşımlar önerilmektedir.
Tasks.lookUp
geri arama
Tasks.lookUp
yöntemi, parametre olarak bir geri çağırma (sink
) alır.
void lookUp(SkyKey key, Consumer<SkyValue> sink);
Bu işlemi gerçekleştirmek için idiomatik yaklaşım, bir Java lambda kullanmaktır:
tasks.lookUp(key, value -> myValue = (MyValueClass)value);
myValue
, aramayı yapan StateMachine
örneğinin bir üye değişkenidir. Ancak lambda, StateMachine
uygulamasında Consumer<SkyValue>
arayüzünü uygulamaya kıyasla ek bellek ayırma gerektirir. Belirsiz olacak birden fazla arama olduğunda lambda yine de yararlıdır.
Tasks.lookUp
için SkyFunction.Environment.getValueOrThrow
'a benzer hata işleme aşırı yüklemeleri de vardır.
<E extends Exception> void lookUp(
SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);
interface ValueOrExceptionSink<E extends Exception> {
void acceptValueOrException(@Nullable SkyValue value, @Nullable E exception);
}
Aşağıda örnek bir uygulama gösterilmektedir.
class PerformLookupWithError extends StateMachine, ValueOrExceptionSink<MyException> {
private MyValue value;
private MyException error;
@Override
public StateMachine step(Tasks tasks) {
tasks.lookUp(new MyKey(), MyException.class, ValueOrExceptionSink<MyException>) this);
return this::processResult;
}
@Override
public acceptValueOrException(@Nullable SkyValue value, @Nullable MyException exception) {
if (value != null) {
this.value = (MyValue)value;
return;
}
if (exception != null) {
this.error = exception;
return;
}
throw new IllegalArgumentException("Both parameters were unexpectedly null.");
}
private StateMachine processResult(Tasks tasks) {
if (exception != null) {
// Handles the error.
…
return DONE;
}
// Processes `value`, which is non-null.
…
}
}
Hata işleme içermeyen aramalarda olduğu gibi, geri çağırmanın doğrudan StateMachine
sınıfının uygulanması, lamba için bellek ayırmadan tasarruf sağlar.
Hata işleme biraz daha ayrıntılı bilgi sağlar ancak esasen hataların yayılımı ile normal değerler arasında çok fazla fark yoktur.
Birden fazla SkyValue 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ğundan Consumer<SkyValue>
geri çağırma işlevi uygulaması net bir şekilde paylaşılabilir. Bu durum söz konusu değilse lambda tabanlı uygulamalara veya uygun geri çağırma işlevlerini uygulayan tam iç sınıf örneklerine geri dönmek uygundur.
Değerleri StateMachine
'ler arasında yayma
Bu dokümanda şimdiye kadar yalnızca alt görevdeki çalışmanın nasıl düzenleneceği açıklanıyordu ancak alt görevlerin, arayana bir değer de bildirmesi gerekir. Alt görevler mantıksal olarak eşzamanlı olmadığından, sonuçları bir geri çağırma kullanılarak arayana 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;
}
}
StateMachine
kimlikli bir arayanın görüntüsü aşağıdaki gibi olur.
class Caller implements StateMachine, BarProducer.ResultSink {
interface ResultSink {
void acceptCallerValue(Bar value);
void acceptCallerError(BarException error);
}
private final ResultSink sink;
private Bar value;
Caller(ResultSink sink) {
this.sink = sink;
}
@Override
@Nullable
public StateMachine step(Tasks tasks) {
tasks.enqueue(new BarProducer((BarProducer.ResultSink) this));
return this::processResult;
}
@Override
public void acceptBarValue(Bar value) {
this.value = value;
}
@Override
public void acceptBarError(BarException error) {
sink.acceptCallerError(error);
}
private StateMachine processResult(Tasks tasks) {
// Since all enqueued subtasks resolve before `processResult` starts, one of
// the `BarResultSink` callbacks must have been called by this point.
if (value == null) {
return DONE; // There was a previously reported error.
}
var finalResult = computeResult(value);
sink.acceptCallerValue(finalResult);
return DONE;
}
}
Önceki örnekte birkaç şey gösterilmektedir. Caller
, sonuçlarını geri yaymalı ve kendi Caller.ResultSink
'unu tanımlamalıdır. Caller
, BarProducer.ResultSink
geri çağırma işlevlerini uygular. Devam ettirildiğinde processResult
, hata olup olmadığını belirlemek için value
değerinin boş olup olmadığını kontrol eder. Bu, bir alt görev veya SkyValue aramasının çıkışını kabul ettikten sonra yaygın olarak görülen bir davranış kalıbıdır.
acceptBarError
'ün uygulanmasının, Hata kabartması gereği olarak sonucu Caller.ResultSink
'e hemen ilettiğini unutmayın.
Üst düzey StateMachine
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ğırma işlevleri aracılığıyla değer olarak iletilir. Bu tür geri çağırmalarda genellikle değer veya hatalardan yalnızca biri ile birlikte özel veya semantik kullanılır.
Bir sonraki bölümde Skyframe hata işlemesiyle ilgili incelikli ancak önemli bir etkileşim açıklanmaktadır.
Hata kabarcıkları (--nokeep_continue)
Hata kabarcıklaşması sırasında, istenen tüm SkyValues mevcut olmasa bile bir SkyFunction yeniden başlatılabilir. Bu gibi durumlarda, Tasks
API sözleşmesi nedeniyle sonraki duruma hiçbir zaman ulaşılamaz. Ancak StateMachine
yine de istisnayı yaymalıdır.
Yayma işlemi, bir sonraki duruma ulaşılıp ulaşılmadığına bakılmaksızın gerçekleşmesi gerektiğinden, hata işleme geri çağırma işlevi bu görevi gerçekleştirmelidir. 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
'te bu, ValueOrExceptionProducer
sınıfının setException
yöntemi çağrılarak yapılabilir.
ValueOrExceptionProducer.tryProduceValue
, SkyValues eksik olsa bile istisnayı atar.
Doğrudan bir Driver
kullanılıyorsa makinenin işlemeyi tamamlamamış olması bile SkyFunction'dan yayılan hataları kontrol etmeyi zorunlu kılar.
Olay İş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ü oluşturma
Belirtilen bir kök StateMachine
ile başlayan StateMachine
'ların yürütülmesini yönetmekten Driver
sorumludur. StateMachine
'ler, alt görev StateMachine
'leri yinelemeli olarak sıraya ekleyebileceğinden tek bir Driver
, çok sayıda alt görevi yönetebilir. Bu alt görevler, yapılandırılmış eşzamanlılığın bir sonucu olarak ağaç yapısı oluşturur. Driver
, daha iyi verimlilik için alt görevler arasında SkyValue aramalarını gruplandırır.
Aşağıdaki API'yi kullanarak Driver
etrafında oluşturulmuş çok sayıda sınıf bulunmaktadır.
public final class Driver {
public Driver(StateMachine root);
public boolean drive(SkyFunction.Environment env) throws InterruptedException;
}
Driver
, parametre olarak tek bir kök StateMachine
alır. Driver.drive
çağrısı, Skyframe yeniden başlatılmadan StateMachine
'ı mümkün olduğunca yürütür. StateMachine
tamamlandığında doğru, aksi takdirde yanlış değerini döndürerek tüm değerlerin kullanılamadığını belirtir.
Driver
, StateMachine
'un eşzamanlı durumunu korur ve SkyKeyComputeState
'e yerleştirilmeye uygundur.
Driver
öğesini doğrudan örneklendirme
StateMachine
uygulamaları, sonuçlarını geleneksel olarak geri çağırmalar yoluyla iletir. Aşağıdaki örnekte gösterildiği gibi, bir Driver
öğesini doğrudan örneklendirmek mümkündür.
Driver
, biraz aşağıda tanımlanacak ilgili ResultSink
uygulamasıyla birlikte SkyKeyComputeState
uygulamasına yerleştirilir. Üst düzeyde, State
nesnesi Driver
'ten daha uzun ömürlü olacağından hesaplama sonucu için uygun bir alıcıdır.
class State implements SkyKeyComputeState, ResultProducer.ResultSink {
// The `Driver` instance, containing the full tree of all `StateMachine`
// states. Responsible for calling `StateMachine.step` implementations when
// asynchronous values are available and performing batched SkyFrame lookups.
//
// Non-null while `result` is being computed.
private Driver resultProducer;
// Variable for storing the result of the `StateMachine`
//
// Will be non-null after the computation completes.
//
private ResultType result;
// Implements `ResultProducer.ResultSink`.
//
// `ResultProducer` propagates its final value through a callback that is
// implemented here.
@Override
public void acceptResult(ResultType result) {
this.result = result;
}
}
Aşağıdaki kod, ResultProducer
değerini özetler.
class ResultProducer implements StateMachine {
interface ResultSink {
void acceptResult(ResultType value);
}
private final Parameters parameters;
private final ResultSink sink;
… // Other internal state.
ResultProducer(Parameters parameters, ResultSink sink) {
this.parameters = parameters;
this.sink = sink;
}
@Override
public StateMachine step(Tasks tasks) {
… // Implementation.
return this::complete;
}
private StateMachine complete(Tasks tasks) {
sink.acceptResult(getResult());
return DONE;
}
}
Sonuçları tembel bir şekilde hesaplamak için kullanabileceğiniz kod aşağıdaki gibi olabilir.
@Nullable
private Result computeResult(State state, Skyfunction.Environment env)
throws InterruptedException {
if (state.result != null) {
return state.result;
}
if (state.resultProducer == null) {
state.resultProducer = new Driver(new ResultProducer(
new Parameters(), (ResultProducer.ResultSink)state));
}
if (state.resultProducer.drive(env)) {
// Clears the `Driver` instance as it is no longer needed.
state.resultProducer = null;
}
return state.result;
}
Driver
'ü yerleştirme
StateMachine
bir değer oluşturur ve istisna oluşturmazsa aşağıdaki örnekte gösterildiği gibi Driver
'i yerleştirmek de olası bir uygulamadır.
class ResultProducer implements StateMachine {
private final Parameters parameters;
private final Driver driver;
private ResultType result;
ResultProducer(Parameters parameters) {
this.parameters = parameters;
this.driver = new Driver(this);
}
@Nullable // Null when a Skyframe restart is needed.
public ResultType tryProduceValue( SkyFunction.Environment env)
throws InterruptedException {
if (!driver.drive(env)) {
return null;
}
return result;
}
@Override
public StateMachine step(Tasks tasks) {
… // Implementation.
}
SkyFunction'da aşağıdaki gibi görünen bir kod olabilir (State
, SkyKeyComputeState
işlevine özgü türdür).
@Nullable // Null when a Skyframe restart is needed.
Result computeResult(SkyFunction.Environment env, State state)
throws InterruptedException {
if (state.result != null) {
return state.result;
}
if (state.resultProducer == null) {
state.resultProducer = new ResultProducer(new Parameters());
}
var result = state.resultProducer.tryProduceValue(env);
if (result == null) {
return null;
}
state.resultProducer = null;
return state.result = result;
}
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şen eşzamanlı API'lere sahip 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 çok benzer. SkyFunction ile benzer bir şekilde arayüz oluşturur. Bir ResultSink
tanımlamak yerine, bu durumlardan biri gerçekleştiğinde uygulama setValue
veya setException
çağrısı yapar.
Her ikisi de gerçekleştiğinde istisna öncelikli olur. tryProduceValue
yöntemi, zaman uyumsuz geri çağırma kodunu zaman uyumlu koda köprüleyerek bir ayar yapıldığında istisna oluşturur.
Daha önce de belirtildiği gibi, hata kabarcıklaşması sırasında, tüm girişler mevcut olmadığı için makine henüz çalışmayı tamamlamamış olsa bile hata oluşabilir. Buna uyum sağlamak için tryProduceValue
, makine tamamlanmadan önce bile ayarlanan 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. Devamlar (özellikle ListenableFuture
'a iletilen Runnable
biçiminde), Bazel kodunun belirli bölümlerinde yaygındır ancak analiz SkyFunctions'da yaygın değildir. 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 yerine her şey senkronize, engelleyen çağrılarla değiştirilir. Bunun nedeni, platform iş parçacığının aksine sanal iş parçacığı kaynağının bağlamanın ucuz olmasıdır. Ancak sanal mesaj dizileri kullanılsa bile basit senkronize işlemlerin mesaj dizisi oluşturma ve senkronizasyon primitifleriyle değiştirilmesi çok pahalıdır. StateMachine
'lerden Java sanal iş parçacıklarına geçiş yaptık. Bu iş parçacıklar çok daha yavaştı ve uçtan uca analiz gecikmesinde neredeyse 3 kat artışa neden oldu. Sanal mesaj dizileri hâlâ önizleme aşamasında olduğundan bu taşıma işlemi, performansın arttığı daha sonraki bir tarihte gerçekleştirilebilir.
Kullanabileceğiniz başka bir yaklaşım da Loom coroutine'lerini (eğer kullanıma sunulursa) beklemektir. 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, manuel olarak yazılmış geri çağırma koduna yaklaşan bir performans elde etmek mümkün olabilir.
Ek
Geri Arama Cehennemi
Geri çağırma cehennemi, eşzamansız kodda geri çağırmaların kullanıldığı, çok bilinen bir sorundur. Bu durum, sonraki bir adımın devamının önceki adıma yerleştirilmesinden 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. Aşağıda gösterildiği gibi yöntem referansları lambda yerine devam ettirme olarak döndürülerek derin iç içe yerleştirme önlenir.
class CallbackHellAvoided implements StateMachine {
@Override
public StateMachine step(Tasks task) {
doA();
return this::step2;
}
private StateMachine step2(Tasks tasks) {
doB();
return this::step3;
}
private StateMachine step3(Tasks tasks) {
doC();
return DONE;
}
}
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: Zincirlenmiş SkyValue aramaları
Uygulama mantığının, SkyValue aramalarının bağımlı zincirlerini gerektirmesi sık karşılaşılan bir durumdur. Örneğin, ikinci bir SkyKey ilk SkyValue'a bağlıysa. Bu konuyu safça ele alırsak karmaşık ve çok iç içe yerleştirilmiş bir geri çağırma yapısıyla karşılaşırız.
private ValueType1 value1;
private ValueType2 value2;
private StateMachine step1(...) {
tasks.lookUp(key1, (Consumer<SkyValue>) this); // key1 has type KeyType1.
return this::step2;
}
@Override
public void accept(SkyValue value) {
this.value1 = (ValueType1) value;
}
private StateMachine step2(...) {
KeyType2 key2 = computeKey(value1);
tasks.lookup(key2, this::acceptValueType2);
return this::step3;
}
private void acceptValueType2(SkyValue value) {
this.value2 = (ValueType2) value;
}
Ancak devamlar yöntem referansları olarak belirtildiğinden kod, durum geçişlerinde prosedürel görünür: step2
, step1
'dan sonra gelir. Burada value2
atamak için bir lambda kullanıldığını unutmayın. Bu sayede kodun sıralaması, yukarıdan aşağıya doğru hesaplamanın sıralamasıyla eşleşir.
Çeşitli İpuçları
Okunabilirlik: Yürütme Sırası
Okunabilirliği artırmak için StateMachine.step
uygulamalarını yürütme sırasına ve geri çağırma uygulamalarını kodda iletildikleri yerin hemen sonrasına yerleştirmeye çalışın. Kontrol akışı dallandığı durumlarda bu her zaman mümkün değildir. Bu gibi durumlarda ek yorumlar faydalı olabilir.
Örnek: Zincirlenmiş SkyValue aramaları'nda, bu işlemi gerçekleştirmek için bir ara yöntem referansı oluşturulur. Bu, okunabilirlik için küçük bir miktar performanstan ödün vermenizi gerektirir. Bu durumda bu durum muhtemelen faydalı olacaktır.
Kuşak Hipotezi
Orta ömürlü Java nesneleri, çok kısa süreli ya da 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. Driver
köklü, hâlâ çalışan tüm StateMachine
'lerin oluşturulmuş ağacını içeren bu tür nesneler, asenkron hesaplamaların tamamlanmasını beklerken askıya alındığı için ara yaşam süresine sahiptir.
JDK19'da bu durum daha az kötü görünüyor ancak StateMachine
'ler kullanılırken, oluşturulan gerçek çöp miktarında önemli düşüşler olsa bile bazen GC süresinde artış gözlemlenebilir. StateMachine
'lerin kullanım ömrü orta düzeyde olduğu için eski nesle yükseltilebilirler. Bu da daha hızlı dolmasına neden olur. Bu durumda, büyük veya tam GC'lerin temizlenmesi gerekir.
İlk önlem, StateMachine
değişkenlerinin kullanımını en aza indirmektir ancak bu her zaman mümkün değildir (ör. birden fazla eyalette bir değere ihtiyaç varsa). Mümkün olduğunda yerel yığın step
değişkenleri genç nesil değişkenlerdir ve verimli bir şekilde GC'ye gönderilir.
StateMachine
değişkenleri için işleri alt görevlere ayırmak ve StateMachine
'ler arasında değerleri aktarma için önerilen kalıbı uygulamak da faydalıdır. Kalıpta, yalnızca alt StateMachine
öğelerinin üst StateMachine
öğelerine referans verdiğini, bunun tersinin geçerli olmadığını unutmayın. Bu, çocuklar sonuç geri çağırmalarını kullanarak ebeveynleri tamamlayıp güncelledikçe doğal olarak kapsamın dışına çıkar ve GC için uygun hale gelir.
Son olarak, bazı durumlarda önceki durumlarda bir StateMachine
değişkenine ihtiyaç duyulur ancak sonraki durumlarda ihtiyaç duyulmaz. Artık ihtiyaç duyulmadığı anlaşılan büyük nesnelerin referanslarını geçersiz kılmak yararlı olabilir.
Eyaletleri adlandırma
Bir yöntemi adlandırırken genellikle yöntemin içinde gerçekleşen davranışa göre adlandırmak mümkündür. Grup olmadığı için StateMachine
'lerde bu işlemin nasıl yapılacağı daha net değildir. Örneğin, foo
yönteminin bar
alt yöntemini çağırdığını varsayalım. StateMachine
içinde bu, foo
durum sırasının ardından bar
olarak ç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ğaç şeması
Aşağıda, yapılandırılmış eşzamanlılık bölümündeki diyagramın ağaç yapısını daha iyi gösteren alternatif bir görünümü verilmiştir. Bloklar küçük bir ağaç oluşturuyor.
-
Skyframe'ın, değerler mevcut olmadığında baştan başlatma kuralının aksine. ↩
-
step
işlevininInterruptedException
atmasına izin verildiğini ancak örneklerde bunun atlandığını unutmayın. Bazel kodunda bu istisnayı devre dışı bırakan birkaç düşük yöntem vardır ve bu istisna, daha sonra açıklanacak olanStateMachine
öğesini çalıştıranDriver
öğesine kadar yayılır. Gerekmediği durumlarda atılmayacağını belirtmemeniz sorun değildir. ↩ -
Eşzamanlı alt görevler, her bağımlılık için bağımsız çalışma yürüten
ConfiguredTargetFunction
tarafından desteklenmektedir. Tüm bağımlılıkları aynı anda işleyen karmaşık veri yapılarını değiştirerek verimsizliklere yol açmak yerine, her bağımlılığın kendi bağımsızStateMachine
'si vardır. ↩ -
Tek bir adımdaki birden fazla
tasks.lookUp
çağrısı birlikte gruplandırılır. Eşzamanlı alt görevlerde gerçekleşen aramalar sayesinde ek toplu işlemler oluşturulabilir. ↩ -
Bu, kavramsal olarak Java'nın yapılandırılmış eşzamanlılığına jeps/428 benzer. ↩
-
Bu işlem, sıralı kompozisyon elde etmek için bir mesaj dizisi oluşturmaya ve bu mesaj dizisini birleştirmeye benzer. ↩