Skyframe StateMachines का इस्तेमाल करने के लिए गाइड

किसी समस्या की शिकायत करना सोर्स देखना Nightly · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

खास जानकारी

Skyframe StateMachine, डिकॉन्स्ट्रक्ट किया गया फ़ंक्शन-ऑब्जेक्ट है, जो हेप में मौजूद होता है. ज़रूरी वैल्यू तुरंत उपलब्ध न होने पर, यह सुविधा1, ज़रूरत के हिसाब से और बिना किसी गड़बड़ी के आकलन करने में मदद करती है. हालांकि, इसके लिए वैल्यू को अलग-अलग समय पर कैलकुलेट किया जाता है. StateMachine, इंतज़ार करते समय थ्रेड संसाधन को इस्तेमाल नहीं कर सकता. इसके बजाय, उसे निलंबित और फिर से शुरू करना पड़ता है. इस तरह, डेकस्ट्रक्चर करने से फिर से एंट्री करने के लिए साफ़ तौर पर पॉइंट दिखते हैं, ताकि पहले की गणना को छोड़ा जा सके.

StateMachine का इस्तेमाल, क्रम, शाखाओं, और स्ट्रक्चर्ड लॉजिकल कंसिस्टेंसी को दिखाने के लिए किया जा सकता है. साथ ही, इन्हें खास तौर पर Skyframe इंटरैक्शन के लिए बनाया गया है. StateMachine को बड़े StateMachine में कंपोज किया जा सकता है और सब-StateMachine शेयर किए जा सकते हैं. एक साथ कई टास्क करने की सुविधा, हमेशा हैरारकी के हिसाब से और पूरी तरह से लॉजिकल होती है. एक साथ चलने वाला हर सबटास्क, एक ही शेयर किए गए पैरंट SkyFunction थ्रेड में चलता है.

परिचय

इस सेक्शन में, java.com.google.devtools.build.skyframe.state पैकेज में मौजूद StateMachine के बारे में कम शब्दों में बताया गया है.

Skyframe के रीस्टार्ट होने के बारे में खास जानकारी

Skyframe एक ऐसा फ़्रेमवर्क है जो डिपेंडेंसी ग्राफ़ का पैरलल तरीके से आकलन करता है. ग्राफ़ में मौजूद हर नोड, SkyFunction के आकलन से जुड़ा होता है. इसमें SkyKey, पैरामीटर की जानकारी देता है और SkyValue, नतीजे की जानकारी देता है. कैलकुलेशन मॉडल ऐसा है कि SkyFunction, SkyKey के हिसाब से SkyValues को लुकअप कर सकता है. इससे, अन्य SkyFunctions का बार-बार और एक साथ आकलन करने की प्रोसेस शुरू हो जाती है. जब अनुरोध की गई SkyValue पूरी तरह से तैयार नहीं होती, तो उसे ब्लॉक करने के बजाय, null getValue का जवाब दिया जाता है. ऐसा इसलिए किया जाता है, ताकि थ्रेड को रोका न जा सके. SkyFunction, SkyValue के बजाय null दिखाता है. इससे पता चलता है कि इनपुट मौजूद न होने की वजह से, SkyValue पूरी तरह से तैयार नहीं है. पहले से अनुरोध की गई सभी SkyValues उपलब्ध होने पर, Skyframe SkyFunctions को रीस्टार्ट करता है.

SkyKeyComputeState के आने से पहले, रीस्टार्ट को मैनेज करने का पारंपरिक तरीका यह था कि कैलकुलेशन को पूरी तरह से फिर से चलाया जाता था. हालांकि, इसकी जटिलता दोगुनी है, लेकिन इस तरह से लिखे गए फ़ंक्शन आखिर में पूरे हो जाते हैं, क्योंकि हर बार फिर से चलाने पर, कुछ लुकअप null दिखाते हैं. SkyKeyComputeState की मदद से, मैन्युअल तरीके से तय किए गए चेकपॉइंट डेटा को SkyFunction के साथ जोड़ा जा सकता है. इससे, डेटा को फिर से कैलकुलेट करने में लगने वाला समय बचता है.

StateMachine, SkyKeyComputeState में मौजूद ऑब्जेक्ट होते हैं. ये ऑब्जेक्ट, SkyFunction के फिर से शुरू होने पर, कैश मेमोरी में सेव किए गए वैल्यू का इस्तेमाल करके, फिर से कैलकुलेट किए जाने की ज़रूरत को कम कर देते हैं. ऐसा तब होता है, जब SkyKeyComputeState कैश मेमोरी में सेव न हो. इसके लिए, ये ऑब्जेक्ट, निलंबित और फिर से शुरू करने के हुक को एक्सपोज़ करते हैं.

SkyKeyComputeState में स्टेटफ़ुल कैलकुलेशन

ऑब्जेक्ट-ओरिएंटेड डिज़ाइन के हिसाब से, डेटा वैल्यू के बजाय SkyKeyComputeState में कैलकुलेट किए गए ऑब्जेक्ट को स्टोर करना सही होता है. Java में, किसी व्यवहार को कैरी करने वाले ऑब्जेक्ट के बारे में कम से कम जानकारी, फ़ंक्शनल इंटरफ़ेस होती है. यह जानकारी काफ़ी होती है. StateMachine की परिभाषा2, इस तरह की है:

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

Tasks इंटरफ़ेस, SkyFunction.Environment से मिलता-जुलता है, लेकिन इसे एक साथ काम न करने के लिए डिज़ाइन किया गया है. साथ ही, यह एक साथ काम करने वाले सबटास्क के लिए भी काम करता है3.

step की रिटर्न वैल्यू एक और StateMachine होती है. इससे, सिलसिलेवार तौर पर चरणों की जानकारी देने की अनुमति मिलती है. StateMachine पूरा होने पर, step DONE दिखाता है. उदाहरण के लिए:

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

इस फ़ंक्शन का इस्तेमाल करके, StateMachine फ़ंक्शन के आउटपुट के बारे में बताया गया है.

hello
world

ध्यान दें कि step2, StateMachine के फ़ंक्शनल इंटरफ़ेस की परिभाषा को पूरा करता है. इसलिए, this::step2 का तरीका रेफ़रंस भी StateMachine है. StateMachine में अगली स्थिति तय करने का सबसे सामान्य तरीका, तरीकों के रेफ़रंस देना है.

सस्पेंड और फिर से चालू करना

किसी कैलकुलेशन को एक बड़े फ़ंक्शन के बजाय StateMachine चरणों में बांटने से, कैलकुलेशन को निलंबित और फिर से शुरू करने के लिए ज़रूरी हुक मिलते हैं. StateMachine.step के वापस आने पर, निलंबन का एक साफ़ तौर पर बताया गया समय दिखता है. रिटर्न की गई StateMachine वैल्यू से तय किया गया कॉन्टिन्यूएशन, फिर से शुरू करने का एक साफ़ तौर पर बताया गया पॉइंट है. इस तरह, फिर से कैलकुलेट करने से बचा जा सकता है, क्योंकि कैलकुलेशन को वहीं से शुरू किया जा सकता है जहां से छोड़ा गया था.

कॉलबैक, कंटिन्यूएशन, और एसिंक्रोनस कैलकुलेशन

तकनीकी शब्दों में, StateMachine कंटिन्यूएशन के तौर पर काम करता है. इससे, बाद में किए जाने वाले कैलकुलेशन का पता चलता है. ब्लॉक करने के बजाय, StateMachine step फ़ंक्शन से वापस आकर, खुद को सस्पेंड कर सकता है. इससे कंट्रोल, Driver इंस्टेंस पर वापस चला जाता है. इसके बाद, Driver किसी तैयार StateMachine पर स्विच कर सकता है या Skyframe को फिर से कंट्रोल दे सकता है.

आम तौर पर, कॉलबैक और कंटिन्यूएशन को एक ही कॉन्सेप्ट के तौर पर देखा जाता है. हालांकि, StateMachines इन दोनों के बीच अंतर बनाए रखता है.

  • कॉलबैक - इससे पता चलता है कि एसिंक्रोनस कैलकुलेशन का नतीजा कहां सेव करना है.
  • जारी रखना - यह कार्रवाई के अगले चरण की स्थिति बताता है.

किसी असाइनॉन्स टाइप के ऑपरेशन को शुरू करने के लिए, कॉलबैक ज़रूरी होते हैं. इसका मतलब है कि SkyValue लुकअप के मामले में, जैसे ही कोई तरीका कॉल किया जाता है, असल ऑपरेशन तुरंत शुरू नहीं होता. कॉलबैक को जितना हो सके उतना आसान रखना चाहिए.

कंटिन्यूएशन, StateMachine की रिटर्न वैल्यू होती हैं. साथ ही, सभी असाइनोक्रोनस कैलकुलेशन के पूरा होने के बाद, कॉम्प्लेक्स एक्ज़ीक्यूशन को शामिल करते हैं.StateMachine इस स्ट्रक्चर्ड तरीके से, कॉलबैक की जटिलता को मैनेज करने में मदद मिलती है.

Tasks

Tasks इंटरफ़ेस, StateMachine को SkyKey के हिसाब से SkyValues को लुकअप करने और एक साथ कई सबटास्क शेड्यूल करने के लिए एपीआई उपलब्ध कराता है.

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 लुकअप

StateMachine, SkyValues देखने के लिए Tasks.lookUp ओवरलोड का इस्तेमाल करते हैं. ये SkyFunction.Environment.getValue और SkyFunction.Environment.getValueOrThrow से मिलते-जुलते हैं. साथ ही, इनमें अपवाद को मैनेज करने के लिए, एक जैसे सेमेटिक्स का इस्तेमाल किया जाता है. लागू करने पर, लुकअप तुरंत नहीं किया जाता. इसके बजाय, ऐसा करने से पहले, जितने हो सके उतने लुकअप को एक साथ4 किया जाता है. हो सकता है कि वैल्यू तुरंत उपलब्ध न हो, जैसे कि Skyframe को फिर से शुरू करना ज़रूरी हो. इसलिए, कॉलर यह तय करता है कि कॉलबैक का इस्तेमाल करके, नतीजे में मिली वैल्यू का क्या करना है.

StateMachine प्रोसेसर (Driver और SkyFrame से ब्रिजिंग), इस बात की गारंटी देता है कि अगली स्थिति शुरू होने से पहले वैल्यू उपलब्ध हो. इसका उदाहरण यहां दिया गया है.

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

ऊपर दिए गए उदाहरण में, पहला चरण new Key() के लिए लुकअप करता है और this को उपभोक्ता के तौर पर पास करता है. ऐसा इसलिए हो सकता है, क्योंकि DoesLookup, Consumer<SkyValue> को लागू करता है.

अनुबंध के मुताबिक, अगली स्थिति DoesLookup.processValue शुरू होने से पहले, DoesLookup.step के सभी लुकरअप पूरे हो जाते हैं. इसलिए, value तब उपलब्ध होता है, जब इसे processValue में ऐक्सेस किया जाता है.

सबटास्क

Tasks.enqueue, लॉजिक के हिसाब से एक साथ होने वाले सबटास्क को लागू करने का अनुरोध करता है. सबटास्क भी StateMachine होते हैं और ये वही काम कर सकते हैं जो सामान्य StateMachine कर सकते हैं. जैसे, बार-बार सबटास्क बनाना या SkyValues देखना. lookUp की तरह ही, स्टेट मशीन ड्राइवर यह पक्का करता है कि अगले चरण पर जाने से पहले, सभी सबटास्क पूरे हो जाएं. इसका उदाहरण यहां दिया गया है.

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 और Subtask2 एक साथ काम करते हैं, लेकिन सब कुछ एक ही थ्रेड में चलता है. इसलिए, i के "एक साथ" अपडेट होने के लिए, सिंक करने की ज़रूरत नहीं होती.

स्ट्रक्चर्ड कॉनकरेंसी

अगली स्थिति पर जाने से पहले, हर lookUp और enqueue को हल करना ज़रूरी है. इसका मतलब है कि एक साथ कई काम करने की सुविधा, ट्री-स्ट्रक्चर तक ही सीमित है. यहां दिए गए उदाहरण में दिखाए गए तरीके से, क्रम से लगाए गए5 प्रोसेस के लिए एक साथ कई टास्क चलाए जा सकते हैं.

स्ट्रक्चर्ड कॉनकरेंसी

UML से यह पता लगाना मुश्किल है कि एक साथ कई टास्क करने की सुविधा का स्ट्रक्चर, ट्री के तौर पर दिखता है. अन्य व्यू में, ट्री स्ट्रक्चर बेहतर तरीके से दिखता है.

अनस्ट्रक्चर्ड कॉनकरेंसी

स्ट्रक्चर्ड कंसिस्टेंसी को समझना काफ़ी आसान है.

कंपोज़िशन और कंट्रोल फ़्लो पैटर्न

इस सेक्शन में, एक से ज़्यादा StateMachine बनाने के तरीके और कंट्रोल फ़्लो से जुड़ी कुछ समस्याओं के समाधान के उदाहरण दिए गए हैं.

क्रम से चलने वाली स्थितियां

यह कंट्रोल फ़्लो का सबसे सामान्य और आसान पैटर्न है. इसका उदाहरण, SkyKeyComputeState में मौजूद स्टेटफ़ुल कैलकुलेशन में दिया गया है.

ब्रांचिंग

StateMachine में शाखाओं की स्थितियां, रेगुलर Java कंट्रोल फ़्लो का इस्तेमाल करके अलग-अलग वैल्यू दिखाकर हासिल की जा सकती हैं. इसका उदाहरण नीचे दिया गया है.

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

कुछ बैंकों के लिए, पेमेंट जल्दी प्रोसेस करने के लिए DONE दिखाना आम बात है.

ऐडवांस सीक्वेंशियल कंपोज़िशन

StateMachine कंट्रोल स्ट्रक्चर में मेमोरी नहीं होती. इसलिए, StateMachine के मकसद को सबटास्क के तौर पर शेयर करना कभी-कभी मुश्किल हो सकता है. मान लें कि M1 और M2 ऐसे StateMachine इंस्टेंस हैं जो StateMachine, S को शेयर करते हैं. साथ ही, M1 और M2 के क्रम <A, S, B> और <X, S, Y> हैं. समस्या यह है कि S को यह पता नहीं होता कि वह B पर जाना है या Y पर. साथ ही, StateMachines में कॉल स्टैक नहीं होता. इस सेक्शन में, ऐसा करने के लिए कुछ तकनीकों की समीक्षा की गई है.

टर्मिनल सीक्वेंस एलिमेंट के तौर पर StateMachine

इससे शुरुआती समस्या हल नहीं होती. यह सिर्फ़ तब क्रम से कॉम्पोज़ करने का तरीका दिखाता है, जब शेयर किया गया StateMachine क्रम में टर्मिनल होता है.

// 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 खुद एक जटिल स्टेट मशीन हो.

क्रम से लिखने के लिए सबटास्क

सूची में जोड़े गए सबटास्क, अगले स्टेटस में बदलने से पहले पूरे हो जाते हैं. इसलिए, कभी-कभी सबटास्क के काम करने के तरीके का थोड़ा 6 गलत इस्तेमाल किया जा सकता है.

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 इंजेक्शन

कभी-कभी, Tasks.enqueue का गलत इस्तेमाल करना मुमकिन नहीं होता, क्योंकि S के लागू होने से पहले, अन्य सबटास्क या Tasks.lookUp कॉल पूरे होने चाहिए. इस मामले में, S में runAfter पैरामीटर को इंजेक्ट करके, S को अगला कदम बताने के लिए इस्तेमाल किया जा सकता है.

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

यह तरीका, सबटास्क का गलत इस्तेमाल करने से बेहतर है. हालांकि, इसे बहुत ज़्यादा इस्तेमाल करने पर, जैसे कि runAfter के साथ कई StateMachine को नेस्ट करने पर, कॉलबैक हेल की समस्या आ सकती है. इसके बजाय, क्रम से होने वाली runAfters को सामान्य क्रम से होने वाली स्थितियों में बांटना बेहतर होता है.

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

को इनके साथ बदला जा सकता है.

  private StateMachine step1(Tasks tasks) {
     doStep1();
     return new S(/* runAfter= */ this::intermediateStep);
  }

  private StateMachine intermediateStep(Tasks tasks) {
    return new T(/* runAfter= */ this::nextStep);
  }

पाबंदी वाला विकल्प: runAfterUnlessError

पिछले ड्राफ़्ट में, हमने एक ऐसा runAfterUnlessError माना था जो गड़बड़ियों की वजह से, प्रोसेस को जल्दी बंद कर देगा. ऐसा इसलिए किया गया, क्योंकि गड़बड़ियों की जांच अक्सर दो बार की जाती है. एक बार उस StateMachine से जिसका runAfter रेफ़रंस है और एक बार runAfter मशीन से.

कुछ विचार-विमर्श के बाद, हमने फ़ैसला लिया कि गड़बड़ी की जांच को डुप्लीकेट करने के बजाय, कोड को एक जैसा रखना ज़्यादा ज़रूरी है. अगर runAfter का तरीका, tasks.enqueue के तरीके के साथ लगातार काम नहीं करता है, तो यह भ्रम की स्थिति पैदा कर सकता है. tasks.enqueue के तरीके में हमेशा गड़बड़ी की जांच की ज़रूरत होती है.

सीधे तौर पर अधिकार देना

जब भी किसी स्टेटस में बदलाव होता है, तो मुख्य Driver लूप आगे बढ़ता है. समझौते के मुताबिक, स्टेटस आगे बढ़ने का मतलब है कि अगले स्टेटस के लागू होने से पहले, पहले से लाइन में लगे सभी SkyValue के लुकअप और सबटास्क पूरे हो जाते हैं. कभी-कभी, किसी टास्क को किसी दूसरे व्यक्ति को सौंपने के StateMachine फ़ंक्शन के लॉजिक की वजह से, टास्क के किसी चरण को आगे बढ़ाना ज़रूरी नहीं होता या इससे काम का नतीजा नहीं मिलता. उदाहरण के लिए, अगर डेलिगेट का पहला step, SkyKey लुकअप करता है, जिन्हें डेलिगेट करने वाले स्टेटस के लुकअप के साथ पैरलल किया जा सकता है, तो फ़ेज़ अडवांस उन्हें क्रम में कर देगा. नीचे दिए गए उदाहरण में दिखाया गया है कि सीधे तौर पर अधिकार सौंपना ज़्यादा सही हो सकता है.

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

डेटा फ़्लो

पिछली चर्चा में, कंट्रोल फ़्लो को मैनेज करने पर फ़ोकस किया गया था. इस सेक्शन में, डेटा वैल्यू के प्रॉपेगेशन के बारे में बताया गया है.

Tasks.lookUp कॉलबैक लागू करना

SkyValue के लुकरअप में Tasks.lookUp कॉलबैक लागू करने का उदाहरण. इस सेक्शन में, एक से ज़्यादा SkyValues को मैनेज करने के तरीके के बारे में बताया गया है.

Tasks.lookUp कॉलबैक

Tasks.lookUp वाला तरीका, पैरामीटर के तौर पर कॉलबैक sink लेता है.

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

इसे लागू करने के लिए, Java लैम्ब्डा फ़ंक्शन का इस्तेमाल करना सबसे सही तरीका होगा:

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

myValue, लुकअप करने वाले StateMachine इंस्टेंस का सदस्य वैरिएबल है. हालांकि, StateMachine को लागू करने के दौरान Consumer<SkyValue> इंटरफ़ेस को लागू करने की तुलना में, लैम्ब्डा को ज़्यादा मेमोरी आवंट करने की ज़रूरत होती है. अगर कई ऐसे लुकअप हैं जिनमें वैल्यू का पता लगाना मुश्किल है, तब भी लैम्ब्डा फ़ंक्शन का इस्तेमाल किया जा सकता है.

Tasks.lookUp में गड़बड़ी को मैनेज करने के लिए, SkyFunction.Environment.getValueOrThrow जैसे ही ओवरलोड भी हैं.

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

इसे लागू करने का उदाहरण नीचे दिया गया है.

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

गड़बड़ी को मैनेज किए बिना लुकअप करने की तरह ही, StateMachine क्लास के सीधे कॉलबैक को लागू करने से, लैम्बा फ़ंक्शन के लिए मेमोरी का ऐलोकेशन बचता है.

गड़बड़ी को मैनेज करने की सुविधा से ज़्यादा जानकारी मिलती है. हालांकि, गड़बड़ियों के प्रसार और सामान्य वैल्यू के बीच काफ़ी अंतर नहीं होता.

एक से ज़्यादा SkyValues का इस्तेमाल करना

अक्सर, एक से ज़्यादा SkyValue लुकअप की ज़रूरत होती है. SkyValue के टाइप को चालू करने से, ज़्यादातर मामलों में समस्या हल हो जाती है. यहां एक उदाहरण दिया गया है, जिसे प्रोटोटाइप के प्रोडक्शन कोड से आसान बनाया गया है.

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

Consumer<SkyValue> कॉलबैक लागू करने का तरीका साफ़ तौर पर शेयर किया जा सकता है, क्योंकि वैल्यू टाइप अलग-अलग होते हैं. अगर ऐसा नहीं है, तो लैम्ब्डा-आधारित लागू करने या सही कॉलबैक लागू करने वाले पूरे इनर-क्लास इंस्टेंस पर वापस जाना मुमकिन है.

StateMachine के बीच वैल्यू का प्रसार करना

अब तक, इस दस्तावेज़ में सिर्फ़ सबटास्क में काम को व्यवस्थित करने का तरीका बताया गया है. हालांकि, सबटास्क को कॉलर को वैल्यू भी रिपोर्ट करनी होती है. सबटास्क, लॉजिक के हिसाब से एक साथ नहीं होते. इसलिए, उनके नतीजे कॉलबैक का इस्तेमाल करके, कॉल करने वाले को भेजे जाते हैं. इसे काम करने के लिए, सबटास्क एक सिंक इंटरफ़ेस तय करता है, जिसे उसके कंस्ट्रक्टर के ज़रिए इंजेक्ट किया जाता है.

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 कुछ इस तरह दिखेगा.

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

ऊपर दिए गए उदाहरण में कुछ चीज़ें दिखाई गई हैं. Caller को अपने नतीजे वापस भेजने होंगे और अपना Caller.ResultSink तय करना होगा. Caller, BarProducer.ResultSink कॉलबैक लागू करता है. फिर से शुरू होने पर, processResult यह जांच करता है कि value शून्य है या नहीं, ताकि यह पता लगाया जा सके कि कोई गड़बड़ी हुई है या नहीं. किसी सबटास्क या SkyValue लुकअप से आउटपुट स्वीकार करने के बाद, यह आम तौर पर दिखने वाला व्यवहार पैटर्न होता है.

ध्यान दें कि गड़बड़ी की जानकारी को ऊपर भेजने की सुविधा के मुताबिक, acceptBarError को लागू करने पर, नतीजा तुरंत Caller.ResultSink को भेज दिया जाता है.

टॉप-लेवल StateMachine के विकल्पों के बारे में Driver और SkyFunctions से ब्रिज करने में बताया गया है.

गड़बड़ी ठीक करना

Tasks.lookUp कॉलबैक और StateMachines के बीच वैल्यू को प्रोपैगेट करने में, गड़बड़ी को मैनेज करने के कुछ उदाहरण पहले से मौजूद हैं. InterruptedException के अलावा, किसी और एक्सेप्शन को थ्रो नहीं किया जाता. इसके बजाय, उन्हें वैल्यू के तौर पर कॉलबैक के ज़रिए पास किया जाता है. ऐसे कॉलबैक में अक्सर एक्सक्लूज़िव-या सेमेंटेक्स होते हैं. इनमें वैल्यू या गड़बड़ी में से सिर्फ़ एक को पास किया जाता है.

अगले सेक्शन में, Skyframe के गड़बड़ी मैनेजमेंट के साथ होने वाले छोटे, लेकिन अहम इंटरैक्शन के बारे में बताया गया है.

गड़बड़ी का बबल (--nokeep_going)

गड़बड़ी के बबल होने के दौरान, SkyFunction को फिर से शुरू किया जा सकता है. भले ही, अनुरोध की गई सभी SkyValues उपलब्ध न हों. ऐसे मामलों में, Tasks API समझौते की वजह से, अगली स्थिति कभी हासिल नहीं की जा सकेगी. हालांकि, StateMachine को अब भी अपवाद को प्रसारित करना चाहिए.

अगली स्थिति तक पहुंचने के बावजूद, प्रॉपेगेशन होना चाहिए. इसलिए, गड़बड़ी को मैनेज करने वाले कॉलबैक को यह काम करना चाहिए. किसी इनर StateMachine के लिए, यह पैरंट कॉलबैक को लागू करके किया जाता है.

SkyFunction के साथ इंटरफ़ेस करने वाले टॉप-लेवल StateMachine पर, ऐसा करने के लिए ValueOrExceptionProducer के setException तरीके को कॉल करें. इसके बाद, ValueOrExceptionProducer.tryProduceValue अपवाद दिखाएगा. भले ही, SkyValues मौजूद न हों.

अगर किसी Driver का सीधे तौर पर इस्तेमाल किया जा रहा है, तो SkyFunction से भेजी गई गड़बड़ियों की जांच करना ज़रूरी है. भले ही, मशीन ने प्रोसेस पूरी न की हो.

इवेंट मैनेज करना

जिन SkyFunctions को इवेंट उत्सर्जित करने की ज़रूरत होती है उनके लिए, SkyKeyComputeState में एक StoredEventHandler इंजेक्ट किया जाता है. इसके बाद, उन StateMachine में इंजेक्ट किया जाता है जिनके लिए ज़रूरी होता है. पहले, Skyframe कुछ इवेंट को तब तक ड्रॉप करता था, जब तक उन्हें फिर से चलाया नहीं जाता. इस वजह से, StoredEventHandler की ज़रूरत पड़ती थी. हालांकि, बाद में इस समस्या को ठीक कर दिया गया. StoredEventHandler इंजेक्शन को बनाए रखा जाता है, क्योंकि इससे गड़बड़ी को मैनेज करने वाले कॉलबैक से उत्सर्जित इवेंट को लागू करना आसान हो जाता है.

Driver और SkyFunctions से ब्रिज करना

Driver की ज़िम्मेदारी, किसी तय रूट StateMachine से शुरू होने वाले StateMachine को लागू करने की होती है. StateMachine, सबटास्क StateMachine को बार-बार लाइन में जोड़ सकते हैं. इसलिए, एक Driver कई सबटास्क मैनेज कर सकता है. ये सबटास्क, एक साथ कई टास्क करने की सुविधा की वजह से एक ट्री स्ट्रक्चर बनाते हैं. Driver, बेहतर परफ़ॉर्मेंस के लिए सभी सबटास्क में SkyValue के लिए एक साथ कई लुकअप करता है.

Driver के लिए, यहां दिए गए एपीआई के साथ कई क्लास बनाई गई हैं.

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

Driver, पैरामीटर के तौर पर एक रूट StateMachine लेता है. Driver.drive को कॉल करने पर, StateMachine को तब तक चलाया जाता है, जब तक Skyframe को रीस्टार्ट किए बिना ऐसा किया जा सकता है. StateMachine पूरा होने पर यह फ़ंक्शन True दिखाता है और ऐसा न होने पर False दिखाता है. इससे पता चलता है कि सभी वैल्यू उपलब्ध नहीं थीं.

Driver, StateMachine की एक साथ चलने वाली स्थिति को बनाए रखता है. साथ ही, यह SkyKeyComputeState में एम्बेड करने के लिए सही है.

Driver को सीधे इंस्टैंशिएट करना

StateMachine लागू करने के लिए, आम तौर पर कॉलबैक के ज़रिए नतीजे शेयर किए जाते हैं. Driver को सीधे तौर पर इंस्टैंशिएट किया जा सकता है, जैसा कि नीचे दिए गए उदाहरण में दिखाया गया है.

Driver को SkyKeyComputeState के लागू होने के साथ-साथ, उससे जुड़े ResultSink के लागू होने के साथ एम्बेड किया जाता है. ResultSink के बारे में थोड़ी देर बाद बताया जाएगा. सबसे ऊपर के लेवल पर, State ऑब्जेक्ट, कैलकुलेशन के नतीजे के लिए सही रिसीवर है, क्योंकि यह Driver से ज़्यादा समय तक रहेगा.

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 को स्केच करता है.

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

इसके बाद, नतीजे का हिसाब लगाने के लिए, कोड कुछ ऐसा दिख सकता है.

@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 को एम्बेड करना

अगर StateMachine कोई वैल्यू दिखाता है और कोई अपवाद नहीं दिखाता है, तो Driver को एम्बेड करना एक और तरीका है. इसका उदाहरण नीचे दिया गया है.

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 में ऐसा कोड हो सकता है जो यहां दिया गया है. यहां State, SkyKeyComputeState का फ़ंक्शन टाइप है.

@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 को लागू करने के दौरान Driver को एम्बेड करना, Skyframe के सिंक करने वाले कोडिंग स्टाइल के लिए बेहतर विकल्प है.

StateMachines, जिनसे अपवाद हो सकते हैं

इसके अलावा, SkyKeyComputeState-एम्बेड की जा सकने वाली ValueOrExceptionProducer और ValueOrException2Producer क्लास भी होती हैं. इनमें सिंक्रोनस SkyFunction कोड से मैच करने के लिए, सिंक्रोनस एपीआई होते हैं.

ValueOrExceptionProducer ऐब्सट्रैक्ट क्लास में ये तरीके शामिल हैं.

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

इसमें एम्बेड किया गया Driver इंस्टेंस शामिल होता है. यह ड्राइवर को एम्बेड करने में मौजूद ResultProducer क्लास से काफ़ी मिलता-जुलता है. साथ ही, यह SkyFunction के साथ उसी तरह इंटरफ़ेस करता है. ResultSink तय करने के बजाय, इंप्लिकेशन में इनमें से कोई भी होने पर setValue या setException को कॉल किया जाता है. जब दोनों मौजूद हों, तो अपवाद को प्राथमिकता दी जाती है. tryProduceValue तरीका, एसिंक्रोनस कॉलबैक कोड को सिंक्रोनस कोड से जोड़ता है. साथ ही, कोई कोड सेट होने पर अपवाद दिखाता है.

जैसा कि पहले बताया गया है, गड़बड़ी के बबल होने के दौरान, गड़बड़ी तब भी हो सकती है, जब मशीन का काम पूरा न हुआ हो. ऐसा इसलिए, क्योंकि सभी इनपुट उपलब्ध नहीं होते. इसे ध्यान में रखते हुए, tryProduceValue मशीन के काम पूरा होने से पहले ही, सेट की गई किसी भी अपवाद को दिखाता है.

आखिर में: कॉलबैक हटाना

StateMachine, असाइनमेंट के हिसाब से अलग-अलग क्रम में होने वाले कंप्यूटेशन को पूरा करने का एक बेहतरीन तरीका है. हालांकि, इसमें बोइलरप्लेट का ज़्यादा इस्तेमाल होता है. Bazel कोड के कुछ हिस्सों में, खास तौर पर Runnables के तौर पर, ListenableFuture को पास करने वाले कॉन्टिन्यूएशन का इस्तेमाल बहुत ज़्यादा किया जाता है. हालांकि, विश्लेषण के लिए इस्तेमाल होने वाले SkyFunctions में, कॉन्टिन्यूएशन का इस्तेमाल ज़्यादा नहीं किया जाता. विश्लेषण का ज़्यादातर हिस्सा सीपीयू पर निर्भर करता है और डिस्क I/O के लिए, असिंक्रोनस एपीआई का इस्तेमाल नहीं किया जा सकता. आखिर में, कॉलबैक को ऑप्टिमाइज़ करना अच्छा होगा, क्योंकि इनमें सीखने की प्रक्रिया ज़्यादा होती है और इन्हें पढ़ना मुश्किल होता है.

Java वर्चुअल थ्रेड, सबसे बेहतर विकल्पों में से एक है. कॉलबैक लिखने के बजाय, सब कुछ सिंक्रोनस, ब्लॉकिंग कॉल से बदल दिया जाता है. ऐसा इसलिए किया जा सकता है, क्योंकि प्लैटफ़ॉर्म थ्रेड के मुकाबले, वर्चुअल थ्रेड रिसॉर्स को जोड़ना सस्ता होता है. हालांकि, वर्चुअल थ्रेड का इस्तेमाल करने पर भी, सिंक्रोनस ऑपरेशन को थ्रेड बनाने और सिंक करने के प्राइमिटिव से बदलना बहुत महंगा होता है. हमने StateMachine से Java वर्चुअल थ्रेड पर माइग्रेशन किया. इससे विश्लेषण में लगने वाला समय बहुत ज़्यादा बढ़ गया. साथ ही, एंड-टू-एंड विश्लेषण में लगने वाला समय करीब तीन गुना बढ़ गया. वर्चुअल थ्रेड अब भी झलक के तौर पर उपलब्ध है. इसलिए, हो सकता है कि परफ़ॉर्मेंस बेहतर होने पर, इसे बाद में माइग्रेट किया जाए.

एक और तरीका यह है कि Loom के कोरुटाइन उपलब्ध होने का इंतज़ार करें. इसका फ़ायदा यह है कि साथ मिलकर मल्टीटास्किंग का इस्तेमाल करके, सिंक करने में लगने वाले समय को कम किया जा सकता है.

अगर इन सभी तरीकों से भी समस्या हल नहीं होती है, तो लो-लेवल बाइटकोड को फिर से लिखना भी एक सही विकल्प हो सकता है. ज़रूरत के मुताबिक ऑप्टिमाइज़ेशन करने पर, मैन्युअल तरीके से लिखे गए कॉलबैक कोड जैसी परफ़ॉर्मेंस मिल सकती है.

अन्य जानकारी

कॉलबैक हेल

कॉलबैक हेल, एसिंक्रोनस कोड में एक आम समस्या है. इसमें कॉलबैक का इस्तेमाल किया जाता है. यह इस बात से जुड़ा है कि अगले चरण के लिए जारी रखने की सुविधा, पिछले चरण में नेस्ट की गई है. अगर कई चरण हैं, तो यह नेस्टिंग काफ़ी गहरी हो सकती है. कंट्रोल फ़्लो के साथ जोड़ने पर, कोड को मैनेज करना मुश्किल हो जाता है.

class CallbackHell implements StateMachine {
  @Override
  public StateMachine step(Tasks task) {
    doA();
    return (t, l) -> {
      doB();
      return (t1, l2) -> {
        doC();
        return DONE;
      };
    };
  }
}

नेस्ट किए गए लागू करने के तरीकों का एक फ़ायदा यह है कि बाहरी चरण के स्टैक फ़्रेम को सुरक्षित रखा जा सकता है. Java में, कैप्चर किए गए लैम्ब्डा वैरिएबल को फ़ाइनल माना जाना चाहिए, ताकि ऐसे वैरिएबल का इस्तेमाल करना मुश्किल हो. डीप नेस्टिंग से बचने के लिए, मेथड रेफ़रंस को लैम्ब्डा के बजाय, कॉन्टिन्यूएशन के तौर पर दिखाया जाता है.

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 इंजेक्शन पैटर्न का बहुत ज़्यादा इस्तेमाल करने पर भी कॉलबैक हेल की समस्या हो सकती है. हालांकि, क्रम से चले जाने वाले चरणों के साथ इंजेक्शन को शामिल करके, इससे बचा जा सकता है.

उदाहरण: चेन किए गए SkyValue लुकअप

अक्सर ऐसा होता है कि ऐप्लिकेशन लॉजिक के लिए, SkyValue लुकअप की डिपेंडेंट चेन की ज़रूरत होती है. उदाहरण के लिए, अगर दूसरा SkyKey, पहले SkyValue पर निर्भर करता है. इस बारे में सोचते समय, यह ध्यान रखें कि इससे आपको एक जटिल और नेस्ट किया गया कॉलबैक स्ट्रक्चर मिलेगा.

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

हालांकि, जारी रखने की सुविधा को तरीके के रेफ़रंस के तौर पर बताया गया है. इसलिए, कोड सभी स्टेट ट्रांज़िशन में प्रोसेस वाला दिखता है: step2, step1 के बाद आता है. ध्यान दें कि यहां value2 असाइन करने के लिए, lambda का इस्तेमाल किया गया है. इससे कोड का क्रम, ऊपर से नीचे तक कैलकुलेशन के क्रम से मैच हो जाता है.

अन्य सलाह

समझने में आसान होना: प्लान लागू करने का क्रम

कोड को आसानी से पढ़ने के लिए, StateMachine.step कोड को लागू करने के क्रम को क्रम से रखें. साथ ही, कोड में जहां भी StateMachine.step कोड को लागू किया गया है उसके तुरंत बाद कॉलबैक को लागू करें. जहां कंट्रोल फ़्लो की शाखाएं होती हैं वहां ऐसा हमेशा नहीं किया जा सकता. ऐसे मामलों में, अतिरिक्त टिप्पणियां मददगार हो सकती हैं.

उदाहरण: चेन किए गए SkyValue लुकअप में, ऐसा करने के लिए एक इंटरमीडिएट मैथड रेफ़रंस बनाया जाता है. इससे, पढ़ने में आसानी के लिए परफ़ॉर्मेंस में थोड़ी गिरावट आती है. हालांकि, यहां यह फ़ायदेमंद हो सकता है.

जनरेशनल हाइपोथीसिस

कुछ समय तक काम करने वाले Java ऑब्जेक्ट, Java के गार्बेज कलेक्टर के जनरेशन के अनुमान को गलत साबित करते हैं. गार्बेज कलेक्टर को ऐसे ऑब्जेक्ट को मैनेज करने के लिए डिज़ाइन किया गया है जो बहुत कम समय तक काम करते हैं या हमेशा काम करते हैं. परिभाषा के मुताबिक, SkyKeyComputeState में मौजूद ऑब्जेक्ट इस अनुमान का उल्लंघन करते हैं. ऐसे ऑब्जेक्ट, जिनमें Driver पर रूट किए गए, अब भी चल रहे सभी StateMachines का बना हुआ ट्री शामिल होता है, उनका जीवनकाल कुछ समय के लिए होता है. ऐसा इसलिए होता है, क्योंकि वे असाइनमेंट के साथ-साथ चलने वाले कैलकुलेशन के पूरा होने का इंतज़ार करते हुए, निलंबित रहते हैं.

JDK19 में यह समस्या कम है. हालांकि, StateMachine का इस्तेमाल करने पर, कभी-कभी जीसी के समय में बढ़ोतरी देखी जा सकती है. भले ही, जनरेट किए गए असल कचरे में काफ़ी कमी आई हो. StateMachines का लाइफ़साइकल सामान्य होता है, इसलिए इन्हें पुराने जनरेशन के तौर पर प्रमोट किया जा सकता है. इससे यह ज़्यादा तेज़ी से भर जाता है. इसलिए, इसे खाली करने के लिए ज़्यादा महंगे मेजर या फ़ुल जीसी की ज़रूरत पड़ती है.

शुरुआती सावधानी यह है कि StateMachine वैरिएबल का इस्तेमाल कम से कम किया जाए. हालांकि, ऐसा हमेशा संभव नहीं होता. उदाहरण के लिए, अगर एक से ज़्यादा राज्यों में किसी वैल्यू की ज़रूरत है. जहां संभव हो, वहां लोकल स्टैक step वैरिएबल, यंग जनरेशन वैरिएबल होते हैं और इन्हें जीसी (गैबरैग कलेक्टर) की मदद से आसानी से हटाया जा सकता है.

StateMachine वैरिएबल के लिए, चीज़ों को सबटास्क में बांटना और StateMachines के बीच वैल्यू को प्रसारित करने के लिए सुझाए गए पैटर्न का पालन करना भी मददगार होता है. ध्यान दें कि पैटर्न का पालन करते समय, सिर्फ़ चाइल्ड StateMachine में पैरंट StateMachine के रेफ़रंस होते हैं, न कि इसके उलट. इसका मतलब है कि जब बच्चे नतीजे के कॉलबैक का इस्तेमाल करके, माता-पिता को अपडेट करते हैं और प्रोसेस पूरी करते हैं, तो वे अपने-आप इस दायरे से बाहर हो जाते हैं और जीसी के लिए ज़रूरी शर्तें पूरी कर लेते हैं.

आखिर में, कुछ मामलों में, पहले स्टेटस में StateMachine वैरिएबल की ज़रूरत होती है, लेकिन बाद के स्टेटस में नहीं. जब यह पता चल जाए कि बड़े ऑब्जेक्ट की ज़रूरत नहीं है, तो उनके रेफ़रंस को शून्य कर दिया जा सकता है.

स्टेटस के नाम

किसी तरीके को नाम देते समय, आम तौर पर उस तरीके में होने वाली गतिविधि के लिए नाम दिया जा सकता है. StateMachine में ऐसा करने का तरीका साफ़ तौर पर नहीं पता चलता, क्योंकि इसमें स्टैक नहीं होता. उदाहरण के लिए, मान लें कि कोई तरीका foo किसी सब-तरीके bar को कॉल करता है. StateMachine में, इसे foo के बाद bar के क्रम में बदला जा सकता है. foo में अब bar शामिल नहीं है. इस वजह से, राज्यों के लिए तरीके के नाम छोटे होते हैं. इनसे स्थानीय व्यवहार के बारे में पता चलता है.

कॉनकरेंसी ट्री डायग्राम

यहां स्ट्रक्चर्ड प्रोसेस के साथ एक साथ कई टास्क करने की सुविधा में डायग्राम का एक अन्य व्यू दिया गया है. इससे ट्री स्ट्रक्चर को बेहतर तरीके से समझा जा सकता है. ब्लॉक से एक छोटा पेड़ बनता है.

स्ट्रक्चर्ड कॉनकरेंसी 3D


  1. Skyframe में वैल्यू उपलब्ध न होने पर, शुरू से फिर से शुरू करने के बजाय, 

  2. ध्यान दें कि step को InterruptedException को थ्रो करने की अनुमति है, लेकिन उदाहरणों में इसे शामिल नहीं किया गया है. Bazel कोड में कुछ ऐसे लो लेवल के तरीके हैं जो यह अपवाद दिखाते हैं. यह अपवाद Driver तक पहुंच जाता है. इसके बारे में बाद में बताया जाएगा. यह StateMachine को चलाता है. ज़रूरत न होने पर, इसे थ्रो किया गया नहीं माना जा सकता. 

  3. एक साथ पूरे होने वाले सबटास्क, ConfiguredTargetFunction से प्रेरित थे. यह हर डिपेंडेंसी के लिए अलग काम करता है. एक साथ सभी डिपेंडेंसी को प्रोसेस करने वाले जटिल डेटा स्ट्रक्चर में बदलाव करने के बजाय, हर डिपेंडेंसी का अपना अलग StateMachine होता है. 

  4. एक चरण में कई tasks.lookUp कॉल एक साथ किए जाते हैं. एक साथ किए जा रहे सबटास्क में होने वाले लुकअप की मदद से, अतिरिक्त बैच बनाए जा सकते हैं. 

  5. यह कॉन्सेप्ट, Java के स्ट्रक्चर्ड कॉन्करेंसी jeps/428 से मिलता-जुलता है. 

  6. ऐसा करना, क्रम से चलने वाले कंपोज़िशन पाने के लिए, थ्रेड को स्पैन करने और उसमें शामिल होने जैसा ही है.