คู่มือสำหรับ Skyframe StateMachines

7.3 · 7.2 · 7.1 · 7.0 · 6.5

ภาพรวม

Skyframe StateMachine คือออบเจ็กต์ฟังก์ชันที่แยกวิเคราะห์แล้วซึ่งอยู่ในกอง รองรับการประเมินที่ยืดหยุ่นและไม่ซ้ำซ้อน1 เมื่อค่าที่จำเป็นไม่พร้อมใช้งานทันที แต่คํานวณแบบไม่พร้อมกัน StateMachine จะไม่สามารถผูกทรัพยากรเธรดขณะรอได้ แต่ต้องถูกระงับและกลับมาทำงานต่อ ดังนั้นการแยกโครงสร้างจึงแสดงจุดเข้าใหม่อย่างชัดเจนเพื่อให้ข้ามการคํานวณก่อนหน้าได้

StateMachine ใช้เพื่อแสดงลําดับ การแยกย่อย การทำงานพร้อมกันแบบมีโครงสร้างเชิงตรรกะ และปรับให้เหมาะกับการโต้ตอบกับ Skyframe โดยเฉพาะ StateMachine สามารถประกอบเป็น StateMachine ที่ใหญ่ขึ้นและแชร์ StateMachine ย่อยได้ การเกิดขึ้นพร้อมกันมีลำดับชั้นเสมอตามการสร้างและ ใช้ตรรกะเท่านั้น งานย่อยที่ทำงานพร้อมกันทุกรายการจะทํางานในเธรด SkyFunction หลักที่แชร์รายการเดียว

บทนำ

ส่วนนี้ช่วยสร้างแรงบันดาลใจและแนะนำ StateMachine ในแพ็กเกจ java.com.google.devtools.build.skyframe.state

บทแนะนำสั้นๆ เกี่ยวกับการรีสตาร์ท Skyframe

Skyframe เป็นเฟรมเวิร์กที่ใช้ประเมินกราฟทรัพยากร Dependency แบบขนาน โหนดแต่ละโหนดในกราฟจะสอดคล้องกับการประเมิน SkyFunction ที่มี SkyKey ที่ระบุพารามิเตอร์และ SkyValue ที่ระบุผลลัพธ์ รูปแบบการประมวลผลเป็นแบบที่ SkyFunction อาจค้นหา SkyValues ตาม SkyKey ซึ่งจะทริกเกอร์การประเมิน SkyFunction เพิ่มเติมแบบซ้ำซ้อนและขนานกัน แทนที่จะบล็อก ซึ่งจะผูกเธรดไว้ เมื่อ SkyValue ที่ขอยังไม่พร้อมเนื่องจากการคำนวณกราฟย่อยบางส่วนไม่สมบูรณ์ SkyFunction ที่ขอจะสังเกตการตอบกลับ null getValue และควรแสดงผล null แทน SkyValue เพื่อบ่งบอกว่าไม่สมบูรณ์เนื่องจากไม่มีอินพุต Skyframe จะรีสตาร์ท SkyFunctions เมื่อ SkyValue ที่ขอก่อนหน้านี้ทั้งหมดพร้อมใช้งาน

ก่อนที่จะมีการเปิดตัว SkyKeyComputeState วิธีการดั้งเดิมในการจัดการการรีสตาร์ทคือการเรียกใช้การคํานวณอีกครั้งโดยสมบูรณ์ แม้ว่าวิธีนี้จะมีความซับซ้อนแบบ 2 เท่า แต่ฟังก์ชันที่เขียนด้วยวิธีนี้จะทำงานเสร็จสมบูรณ์ในที่สุดเนื่องจากการเรียกใช้แต่ละครั้งจะมีการค้นหาที่แสดงผล 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 อีกแบบหนึ่ง ซึ่งทำให้สามารถกำหนดลำดับขั้นตอนได้ในแบบอุปนัย step จะแสดงผลเป็น DONE เมื่อStateMachineเสร็จสิ้น เช่น

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

โปรดทราบว่าการอ้างอิงเมธอด this::step2 ก็เป็น StateMachine ด้วยเนื่องจาก step2 เป็นไปตามคําจํากัดความอินเทอร์เฟซฟังก์ชันของ StateMachine การอ้างอิงเมธอดเป็นวิธีที่พบบ่อยที่สุดในการระบุสถานะถัดไปใน StateMachine

การระงับและการดําเนินการต่อ

การแจกแจงการคำนวณเป็น StateMachine ขั้นตอนโดยสัญชาตญาณ แทนที่จะเป็นฟังก์ชันโมโนลิธ จะให้ฮุกที่จำเป็นสำหรับระงับและคำนวณต่อต่อ เมื่อ StateMachine.step แสดงผล จะมีจุดการระงับที่ชัดเจน ความต่อเนื่องที่ระบุโดยค่า StateMachine ที่แสดงผลเป็นจุดกลับมาทำงานอีกครั้งอย่างชัดเจน จึงหลีกเลี่ยงการคํานวณใหม่ได้เนื่องจากสามารถดําเนินการคํานวณต่อจากจุดที่ค้างไว้ได้

การเรียกกลับ การดําเนินการต่อ และการคำนวณแบบอะซิงโครนัส

ในทางเทคนิคแล้ว StateMachine จะทำหน้าที่เป็นความต่อเนื่อง ซึ่งจะกำหนดการคำนวณลำดับต่อมา แทนที่จะบล็อก StateMachine สามารถหยุดชั่วคราวโดยสมัครใจได้โดยกลับจากฟังก์ชัน step ซึ่งจะโอนการควบคุมกลับไปยังอินสแตนซ์ Driver จากนั้น Driver จะเปลี่ยนไปเป็น StateMachine ที่พร้อมใช้งานหรือมอบการควบคุมกลับไปยัง Skyframe ก็ได้

โดยทั่วไปแล้ว การเรียกกลับและการดําเนินการต่อจะรวมกันเป็นแนวคิดเดียว อย่างไรก็ตาม StateMachine จะยังคงแยกความแตกต่างระหว่าง 2 รายการนี้

  • Callback - อธิบายตำแหน่งสำหรับจัดเก็บผลลัพธ์ของการคำนวณแบบอะซิงโครนัส
  • ความต่อเนื่อง - ระบุสถานะการดำเนินการถัดไป

ต้องมีคอลแบ็กเมื่อเรียกใช้การดำเนินการแบบไม่พร้อมกัน ซึ่งหมายความว่าการดำเนินการจริงจะไม่เกิดขึ้นทันทีที่เรียกใช้เมธอด ดังในกรณีการค้นหา SkyValue ควรทำให้การเรียกกลับเรียบง่ายที่สุด

การดำเนินการต่อคือค่าที่ StateMachine แสดงผลของ StateMachine และรวมการดำเนินการที่ซับซ้อนซึ่งจะตามมาเมื่อการคํานวณแบบไม่สอดคล้องกันทั้งหมดเสร็จสมบูรณ์ แนวทางที่มีโครงสร้างนี้ช่วยให้จัดการความซับซ้อนของ callback ได้

งาน

อินเทอร์เฟซ Tasks มี API สำหรับ StateMachine เพื่อค้นหา SkyValues ตาม SkyKey และกำหนดเวลางานย่อยพร้อมกัน

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ใช้การโอเวอร์โหลด Tasks.lookUp เพื่อค้นหา SkyValues ซึ่งคล้ายกับ SkyFunction.Environment.getValue และ SkyFunction.Environment.getValueOrThrow และมีความหมายคล้ายกับการจัดการข้อยกเว้น การใช้งานนี้ไม่ได้ทำการค้นหาในทันที แต่จะค้นหาแบบกลุ่ม4เป็นจำนวนมากที่สุดเท่าที่จะเป็นไปได้ก่อนดำเนินการแทน ค่าดังกล่าวอาจไม่พร้อมใช้งานทันที เช่น ต้องรีสตาร์ท Skyframe ดังนั้นผู้เรียกใช้จึงระบุสิ่งที่ต้องทำกับค่าผลลัพธ์โดยใช้ Callback

โปรเซสเซอร์ 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.step ทั้งหมดจะเสร็จสมบูรณ์ก่อนที่สถานะถัดไป DoesLookup.processValue จะเริ่มขึ้น ดังนั้น value จึงพร้อมใช้งานเมื่อเข้าถึงใน processValue

งานย่อย

Tasks.enqueue ส่งคําขอดําเนินการงานย่อยที่เกิดขึ้นพร้อมกันตามตรรกะ งานย่อยยังเป็น StateMachine ด้วยและทําสิ่งต่างๆ ได้เช่นเดียวกับ StateMachine ทั่วไป ซึ่งรวมถึงการสร้างงานย่อยเพิ่มเติมแบบซ้ำๆ หรือค้นหา SkyValue ไดรเวอร์เครื่องสถานะจะคล้ายกับ 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

การแยก

คุณจะดำเนินการสถานะ Branch ใน 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 หลังจากทำงานเสร็จแล้ว และ StateMachine ไม่ค่อยเก็บสแต็กการเรียกใช้ ส่วนนี้จะอธิบายเทคนิคบางอย่างเพื่อให้บรรลุเป้าหมายนี้

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 injection

บางครั้งการละเมิด Tasks.enqueue ก็ทำไม่ได้เพราะมีงานย่อยอื่นๆ ที่ทำพร้อมกันหรือการเรียก Tasks.lookUp ที่ต้องทำให้เสร็จก่อนที่ S จะทำงาน ในกรณีนี้ สามารถใช้การแทรกพารามิเตอร์ runAfter ลงใน S เพื่อบอก 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;
  }
}

วิธีนี้สะอาดกว่าการใช้งานย่อยในทางที่ผิด อย่างไรก็ตาม การใช้รูปแบบนี้มากเกินไป เช่น การทำ StateMachine ซ้อนกันหลายรายการกับ runAfter จะเป็นหนทางไปสู่ความยุ่งเหยิงของคอลแบ็ก คุณควรแบ่ง runAfter ตามลำดับออกเป็นสถานะตามลำดับธรรมดาแทน

  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 ที่จะหยุดทำงานหากพบข้อผิดพลาดตั้งแต่เนิ่นๆ นี่เกิดจากการที่ข้อผิดพลาดมักจะได้รับการตรวจสอบ 2 ครั้ง โดยครั้งหนึ่งโดย StateMachine ที่มีการอ้างอิง runAfter และอีกครั้งหนึ่งโดยเครื่อง runAfter

หลังจากพิจารณาอย่างรอบคอบแล้ว เราตัดสินใจว่าความสอดคล้องกันของโค้ดสำคัญกว่าการทำซ้ำการตรวจสอบข้อผิดพลาด การดำเนินการจะเกิดความสับสนหากกลไก runAfter ไม่ทำงานอย่างสอดคล้องกันกับกลไก 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;
  }
}

โฟลว์ข้อมูล

การสนทนาก่อนหน้านี้มุ่งเน้นที่การจัดการโฟลว์การควบคุม ส่วนนี้จะอธิบายการนำไปใช้งานของค่าข้อมูล

กําลังติดตั้งใช้งาน Callback Tasks.lookUp รายการ

ตัวอย่างการใช้งาน Tasks.lookUp callback มีอยู่ใน SkyValue lookups ส่วนนี้จะอธิบายเหตุผลและแนะนําแนวทางการจัดการ SkyValue หลายรายการ

Callback Tasks.lookUp รายการ

เมธอด Tasks.lookUp จะใช้ Callback sink เป็นพารามิเตอร์

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

แนวทางที่นิยมใช้คือการใช้ Lambda ของ Java เพื่อติดตั้งใช้งานดังนี้

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

โดยที่ myValue เป็นตัวแปรสมาชิกของอินสแตนซ์ StateMachine ที่ใช้การค้นหา อย่างไรก็ตาม lambda ต้องมีการจัดสรรหน่วยความจำเพิ่มเติมเมื่อเทียบกับการใช้งานอินเทอร์เฟซ Consumer<SkyValue> ในการใช้งาน StateMachine lambda ยังคงมีประโยชน์เมื่อมีการค้นหาหลายรายการที่ไม่ชัดเจน

นอกจากนี้ยังมี 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 ใช้การเรียกกลับโดยตรงจะช่วยประหยัดการจัดสรรหน่วยความจำสําหรับ Lamba

การจัดการข้อผิดพลาดจะอธิบายรายละเอียดเพิ่มเติม แต่โดยพื้นฐานแล้ว การนำไปใช้งานข้อผิดพลาดและค่าปกตินั้นไม่แตกต่างกันมากนัก

การใช้ 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> callback สามารถแชร์ได้โดยไม่เกิดความสับสนเนื่องจากประเภทค่าแตกต่างกัน หากไม่เป็นเช่นนั้น คุณสามารถเปลี่ยนไปใช้การติดตั้งใช้งานแบบ Lambda หรืออินสแตนซ์คลาสภายในแบบสมบูรณ์ที่ใช้การเรียกกลับที่เหมาะสมได้

การนำไปใช้ค่าระหว่าง StateMachine

จนถึงตอนนี้ เอกสารนี้อธิบายวิธีจัดเรียงงานในภารกิจย่อยเท่านั้น แต่ภารกิจย่อยยังต้องรายงานค่ากลับไปยังผู้เรียกด้วย เนื่องจากงานย่อยทำงานไม่พร้อมกันตามตรรกะ ระบบจึงจะแจ้งผลลัพธ์ของงานกลับไปยังผู้โทรโดยใช้ฟังก์ชัน Callback วิธีการทํางานคืองานย่อยจะกําหนดอินเทอร์เฟซซิงค์ที่แทรกผ่านคอนสตรัคเตอร์

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

การจัดการข้อผิดพลาด

มีตัวอย่างการจัดการข้อผิดพลาด 2 ตัวอย่างใน Tasks.lookUp callbacks และ การเผยแพร่ค่าระหว่าง StateMachines ระบบจะไม่ส่งข้อยกเว้นนอกเหนือจาก InterruptedException แต่จะส่งผ่าน Callback เป็นค่าแทน โดยทั่วไปแล้ว การเรียกกลับดังกล่าวจะมีความหมายแบบ Exclusive-Or โดยระบบจะส่งค่าหรือข้อผิดพลาดเพียงรายการเดียว

ส่วนถัดไปจะอธิบายการโต้ตอบเล็กๆ น้อยๆ แต่สําคัญกับการจัดการข้อผิดพลาดของ Skyframe

การแจ้งข้อผิดพลาด (--nokeep_going)

ในระหว่างการบับเบิลข้อผิดพลาด ระบบอาจรีสตาร์ท SkyFunction แม้ว่า SkyValue ที่ขอจะใช้งานไม่ได้ทั้งหมด ในกรณีเช่นนี้ ระบบจะไม่เข้าสู่สถานะถัดไปเนื่องจากสัญญา Tasks API อย่างไรก็ตาม StateMachine จะยังคงเผยแพร่ข้อยกเว้นอยู่

เนื่องจากต้องมีการนำไปใช้ไม่ว่าจะถึงสถานะถัดไปหรือไม่ แคล็กแบ็กการจัดการข้อผิดพลาดจึงต้องดำเนินการนี้ สําหรับ StateMachine ภายใน การดำเนินการนี้จะทำได้โดยการเรียกใช้การเรียกกลับของรายการหลัก

ที่ StateMachine ระดับบนสุด ซึ่งอินเทอร์เฟซกับ SkyFunction การดำเนินการนี้ทำได้โดยการเรียกใช้เมธอด setException ของ ValueOrExceptionProducer ValueOrExceptionProducer.tryProduceValue จะส่งกลับข้อยกเว้น แม้ว่าจะไม่มี SkyValues ขาดหายไปก็ตาม

หากใช้ Driver โดยตรง คุณต้องตรวจหาข้อผิดพลาดที่เผยแพร่จาก SkyFunction แม้ว่าเครื่องจะประมวลผลไม่เสร็จสิ้นก็ตาม

การจัดการเหตุการณ์

สำหรับ SkyFunction ที่ต้องการปล่อยเหตุการณ์ ระบบจะแทรก StoredEventHandler ลงใน SkyKeyComputeState และแทรกลงไปใน StateMachine ที่จำเป็นต้องใช้ ก่อนหน้านี้ StoredEventHandler จําเป็นเนื่องจาก Skyframe ทิ้งเหตุการณ์บางอย่าง เว้นแต่จะมีการเล่นซ้ำ แต่ปัญหานี้ได้รับการแก้ไขแล้วในภายหลัง การแทรก StoredEventHandler จะยังคงอยู่เนื่องจากช่วยให้การติดตั้งใช้งานเหตุการณ์ที่ส่งออกมาจากคอลแบ็กการจัดการข้อผิดพลาดง่ายขึ้น

Driver และบริดจ์กับ SkyFunctions

Driver จะมีหน้าที่จัดการการเรียกใช้ StateMachine โดยเริ่มจาก StateMachine รูทที่ระบุ เนื่องจาก StateMachine สามารถส่ง StateMachine งานย่อยเข้าคิวซ้ำได้ Driver รายการเดียวจึงจัดการงานย่อยได้หลายรายการ ภารกิจย่อยเหล่านี้จะสร้างโครงสร้างต้นไม้ ซึ่งเป็นผลมาจากการทำงานพร้อมกันแบบมีโครงสร้าง Driver จะจัดกลุ่มการค้นหา SkyValue ในงานย่อยต่างๆ เพื่อประสิทธิภาพที่ดีขึ้น

มีคลาสจำนวนมากที่สร้างขึ้นรอบๆ Driver ด้วย API ต่อไปนี้

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

Driver จะรับรูท StateMachine รายการเดียวเป็นพารามิเตอร์ การเรียกใช้ Driver.drive จะเรียกใช้ StateMachine เท่าที่จะทำได้โดยไม่ต้องรีสตาร์ท Skyframe โดยจะแสดงผลเป็น "จริง" เมื่อ StateMachine เสร็จสมบูรณ์ และแสดงผลเป็น "เท็จ" หากไม่เป็นเช่นนั้น ซึ่งบ่งบอกว่ามีบางค่าที่ไม่พร้อมใช้งาน

Driver จะรักษาสถานะพร้อมกันของ StateMachine และเหมาะสําหรับการฝังใน SkyKeyComputeState

การสร้างอินสแตนซ์ Driver โดยตรง

การติดตั้งใช้งาน StateMachine โดยทั่วไปจะสื่อสารผลลัพธ์ผ่าน callback คุณจะสร้างอินสแตนซ์ Driver ได้โดยตรงดังที่แสดงในตัวอย่างต่อไปนี้

Driver ฝังอยู่ในการใช้งาน SkyKeyComputeState พร้อมกับการใช้งาน 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;
  }
}

โค้ดสำหรับการคำนวณแบบ Lazy Loading อาจมีลักษณะดังนี้

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

การฝัง Driver ในการใช้งาน StateMachine เหมาะสําหรับรูปแบบการเขียนโค้ดแบบซิงโครนัสของ Skyframe มากกว่า

StateMachines ที่อาจสร้างข้อยกเว้น

มิเช่นนั้น จะมีคลาส SkyKeyComputeState แบบฝังได้ ValueOrExceptionProducer และ ValueOrException2Producer ที่มี API แบบซิงโครนัสเพื่อจับคู่โค้ด SkyFunction แบบซิงโครนัส

คลาส Abstract ของ 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 ในลักษณะที่คล้ายกัน การติดตั้งใช้งานจะเรียก setValue หรือ setException เมื่อเกิดเหตุการณ์ใดเหตุการณ์หนึ่งแทนการกำหนด ResultSink เมื่อทั้ง 2 รายการเกิดขึ้น ข้อยกเว้นจะมีลำดับความสำคัญสูงกว่า เมธอด tryProduceValue จะเชื่อมโยงโค้ด Callback แบบไม่พร้อมกันกับโค้ดแบบซิงโครนัสและยกเว้นเมื่อมีการตั้งค่า

ดังที่กล่าวไว้ก่อนหน้านี้ ในระหว่างการบับเบิลข้อผิดพลาด อาจเกิดข้อผิดพลาดขึ้นได้แม้ว่าเครื่องจะยังไม่เสร็จสิ้นเนื่องจากอินพุตไม่พร้อมใช้งานทั้งหมด tryProduceValue จึงแสดงข้อยกเว้นที่ตั้งไว้แม้ว่าเครื่องจะยังไม่เสร็จสิ้น

สรุป: การนําการเรียกกลับออกในที่สุด

StateMachine เป็นวิธีที่มีประสิทธิภาพสูงแต่ใช้การเขียนโค้ดซ้ำๆ มากในการทําการคํานวณแบบไม่พร้อมกัน การดําเนินการต่อ (โดยเฉพาะในรูปแบบของ Runnable ที่ส่งไปยัง ListenableFuture) พบได้ทั่วไปในบางส่วนของโค้ด Bazel แต่ไม่ค่อยพบใน SkyFunction การวิเคราะห์ การวิเคราะห์ส่วนใหญ่จะทำงานบน CPU และไม่มี Asynchronous API ที่มีประสิทธิภาพสําหรับ I/O ของดิสก์ ท้ายที่สุดแล้ว คุณควรเพิ่มประสิทธิภาพเพื่อหลีกเลี่ยงการใช้คอลแบ็ก เนื่องจากคอลแบ็กมีช่วงการเรียนรู้และทำให้อ่านได้ยาก

ทางเลือกหนึ่งที่ดูน่าเชื่อถือที่สุดคือ Java virtual thread ทุกอย่างจะแทนที่ด้วยการเรียกแบบซิงค์ที่บล็อกแทนการเขียนการเรียกกลับ ซึ่งเป็นไปได้เนื่องจากการจองทรัพยากรเธรดเสมือนนั้นควรจะใช้ทรัพยากรน้อย ต่างจากเธรดแพลตฟอร์ม อย่างไรก็ตาม แม้จะใช้เธรดเสมือน แต่การแทนที่การดำเนินการแบบซิงค์อย่างง่ายด้วยการสร้างเธรดและการดำเนินการพื้นฐานแบบซิงค์ก็ยังมีค่าใช้จ่ายสูงเกินไป เราดำเนินการย้ายข้อมูลจาก StateMachine ไปยังชุดข้อความเสมือนของ Java ซึ่งมีลำดับที่ช้ากว่า ทำให้เวลาในการตอบสนองของการวิเคราะห์จากต้นทางถึงปลายทางเพิ่มขึ้นเกือบ 3 เท่า เนื่องจากเทรดเสมือนยังคงเป็นฟีเจอร์พรีวิว จึงอาจดำเนินการย้ายข้อมูลนี้ในภายหลังได้เมื่อประสิทธิภาพดีขึ้น

อีกวิธีการหนึ่งที่ควรพิจารณาคือรอฟังโครูทีน Loom หากมี ข้อดีก็คือคุณสามารถลดค่าใช้จ่ายในการซิงค์ข้อมูลได้โดยใช้การทำงานหลายอย่างพร้อมกัน

หากไม่มีวิธีใดได้ผล การเขียนไบต์โค้ดระดับต่ำใหม่ก็อาจใช้ได้ผลดี หากเพิ่มประสิทธิภาพมากพอ คุณอาจได้รับประสิทธิภาพที่ใกล้เคียงกับโค้ดการเรียกกลับที่เขียนด้วยตนเอง

ภาคผนวก

Callback Hell

ปัญหา Callback Hell เป็นปัญหาที่มีชื่อเสียงในโค้ดแบบอะซิงโครนัสที่ใช้การเรียกกลับ ปัญหานี้เกิดจากความจริงที่ว่าการดำเนินการต่อสำหรับขั้นตอนถัดไปจะฝังอยู่ภายในขั้นตอนก่อนหน้า หากมีขั้นตอนจํานวนมาก การฝังนี้อาจมีความลึกมาก หากใช้ร่วมกับโฟลว์การควบคุม โค้ดจะจัดการไม่ได้

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

ข้อดีอย่างหนึ่งของการติดตั้งใช้งานที่ฝังอยู่คือสามารถเก็บเฟรมสแต็กของขั้นตอนภายนอกได้ ใน Java ตัวแปร Lambda ที่บันทึกไว้ต้องเป็นแบบสุดท้ายอย่างมีประสิทธิภาพ ดังนั้นการใช้ตัวแปรดังกล่าวจึงอาจยุ่งยาก หลีกเลี่ยงการฝังซ้อนหลายชั้นโดยการแสดงผลข้อมูลอ้างอิงเมธอดเป็นการดำเนินการต่อแทนการใช้ Lambda ดังที่แสดงต่อไปนี้

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

ข้อผิดพลาด Callback Hell อาจเกิดขึ้นหากมีการใช้รูปแบบการแทรก runAfter มากเกินไป แต่สามารถหลีกเลี่ยงได้โดยการแทรกแบบแทรกสลับด้วยขั้นตอนตามลำดับ

ตัวอย่าง: การค้นหา Chained SkyValue

บ่อยครั้งที่ตรรกะของแอปพลิเคชันต้องใช้เชนการค้นหา SkyValue แบบที่ 2 เช่น SkyValue ที่ 2 จะขึ้นอยู่กับ 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 ซึ่งทําให้ลําดับของโค้ดตรงกับลําดับการคํานวณจากบนลงล่าง

เคล็ดลับอื่นๆ

ความอ่านง่าย: ลําดับการดําเนินการ

พยายามทำให้การติดตั้งใช้งาน StateMachine.step อยู่ในลําดับการดําเนินการและการติดตั้งใช้งานการเรียกกลับตามหลังทันทีที่ส่งในโค้ด เพื่อปรับปรุงความสามารถในการอ่าน ซึ่งอาจไม่เป็นไปเสมอไปที่ ขั้นตอนของการควบคุม ในกรณีนี้ ความคิดเห็นเพิ่มเติมอาจมีประโยชน์

ในตัวอย่าง: การค้นหา Chained SkyValue ระบบจะสร้างการอ้างอิงเมธอดขั้นกลางเพื่อให้บรรลุเป้าหมายนี้ ซึ่งจะแลกประสิทธิภาพเพียงเล็กน้อยเพื่อความสามารถในการอ่าน ซึ่งน่าจะคุ้มค่าในกรณีนี้

สมมติฐานรุ่น

ออบเจ็กต์ Java ที่มีอายุการใช้งานปานกลางเป็นการทำลายสมมติฐานการสร้างของโปรแกรมเก็บขยะ Java ซึ่งออกแบบมาเพื่อจัดการวัตถุที่มีอายุการใช้งานสั้นมากหรือวัตถุที่มีชีวิตตลอดไป ตามคำจำกัดความแล้ว ออบเจ็กต์ใน SkyKeyComputeState ละเมิดสมมติฐานนี้ ออบเจ็กต์ดังกล่าวซึ่งมีต้นไม้ที่สร้างขึ้นจาก StateMachine ทั้งหมดที่ยังคงทํางานอยู่โดยรูทที่ Driver จะมีอายุการใช้งานระดับกลางขณะที่หยุดชั่วคราวเพื่อรอการประมวลผลแบบไม่สอดคล้องกันให้เสร็จสมบูรณ์

ดูเหมือนว่าใน JDK19 จะมีปัญหาน้อยกว่า แต่เมื่อใช้ StateMachine บางครั้งก็เป็นไปได้ที่จะสังเกตเห็นว่าเวลา GC เพิ่มขึ้น แม้ว่าขยะจริงที่สร้างขึ้นจะลดลงอย่างมากก็ตาม เนื่องจาก StateMachine มีอายุการใช้งานปานกลางอาจได้รับการโปรโมตเป็นเวอร์ชันเก่า ทำให้เติมได้เร็วขึ้น ทำให้ต้องมี GC หลักหรือ GC แบบเต็มที่มีราคาแพงกว่าในการทำความสะอาด

ข้อควรระวังเบื้องต้นคือลดการใช้ตัวแปร StateMachine แต่อาจไม่สามารถทำได้เสมอไป เช่น หากต้องใช้ค่าในหลายสถานะ ตัวแปร step ในกองซ้อนภายในจะเป็นตัวแปรรุ่นเยาว์และ GC จะทำงานได้อย่างมีประสิทธิภาพ

สําหรับตัวแปร StateMachine การแบ่งงานออกเป็นงานย่อยและทําตามรูปแบบที่แนะนําสําหรับการเผยแพร่ค่าระหว่าง StateMachine นั้นมีประโยชน์เช่นกัน โปรดทราบว่าเมื่อใช้รูปแบบนี้ จะมีเพียง StateMachine ย่อยเท่านั้นที่อ้างอิงถึง StateMachine หลัก แต่ StateMachine หลักจะไม่อ้างอิงถึง StateMachine ย่อย ซึ่งหมายความว่าเมื่อบุตรหลานดำเนินการเสร็จสิ้นและอัปเดตผู้ปกครองโดยใช้การเรียกกลับผลลัพธ์ บุตรหลานจะออกจากขอบเขตโดยอัตโนมัติและมีสิทธิ์ได้รับ GC

สุดท้าย ในบางกรณี คุณต้องใช้ตัวแปร StateMachine ในสถานะก่อนหน้า แต่ไม่ต้องใช้ในสถานะถัดไป การทำให้การอ้างอิงออบเจ็กต์ขนาดใหญ่เป็นค่าว่างจะมีประโยชน์เมื่อทราบว่าไม่จำเป็นต้องใช้ออบเจ็กต์เหล่านั้นอีกต่อไป

การตั้งชื่อสถานะ

เมื่อตั้งชื่อเมธอด โดยทั่วไปแล้วคุณจะตั้งชื่อเมธอดตามลักษณะการทำงานที่เกิดขึ้นภายในเมธอดนั้นได้ วิธีนี้มีความชัดเจนน้อยลงใน StateMachine เพราะไม่มีสแต็ก ตัวอย่างเช่น สมมติว่าเมธอด foo เรียกเมธอดย่อย bar ใน StateMachine การดำเนินการนี้อาจแปลเป็นลำดับสถานะ foo ตามด้วย bar foo ไม่ได้รวมพฤติกรรม bar แล้ว ด้วยเหตุนี้ ชื่อวิธีการสำหรับรัฐต่างๆ จึงมีแนวโน้มที่จะมีขอบเขตแคบลง และอาจสะท้อนให้เห็นถึงพฤติกรรมในท้องถิ่น

แผนภูมิต้นไม้การเกิดขึ้นพร้อมกัน

ต่อไปนี้เป็นมุมมองอื่นของแผนภาพในการทำงานพร้อมกันแบบมีโครงสร้างซึ่งแสดงโครงสร้างต้นไม้ได้ดีขึ้น บล็อกต่างๆ ประกอบกันเป็นต้นไม้เล็กๆ

การเกิดขึ้นพร้อมกันแบบมีโครงสร้าง 3 มิติ


  1. ต่างจากรูปแบบที่ Skyframe จะรีสตาร์ทตั้งแต่ต้นเมื่อไม่มีค่า 

  2. โปรดทราบว่า step ได้รับอนุญาตให้แสดงข้อยกเว้น InterruptedException แต่ตัวอย่างไม่ได้แสดงข้อยกเว้นนี้ มีเมธอดระดับต่ำ 2-3 รายการในโค้ด Bazel ที่ทำให้เกิดข้อยกเว้นนี้ และจะเผยแพร่ถึง Driver ซึ่งจะอธิบายในภายหลัง ซึ่งจะเรียกใช้ StateMachine คุณไม่จำเป็นต้องประกาศว่าจะมีการยกเว้นข้อยกเว้นเมื่อไม่จำเป็น 

  3. ภารกิจย่อยที่เกิดขึ้นพร้อมกันจะทำงานตาม ConfiguredTargetFunction ซึ่งทำงานอิสระสำหรับแต่ละรายการที่เกี่ยวข้อง แทนที่จะจัดการโครงสร้างข้อมูลที่ซับซ้อนซึ่งประมวลผลข้อมูลที่ต้องพึ่งพาทั้งหมดพร้อมกัน ซึ่งทำให้เกิดความไม่มีประสิทธิภาพ ข้อมูลที่ต้องพึ่งพาแต่ละรายการจะมีStateMachineอิสระของตนเอง 

  4. การเรียกใช้ tasks.lookUp หลายรายการในขั้นตอนเดียวจะรวมกันเป็นกลุ่ม คุณจะสร้างกลุ่มเพิ่มเติมได้โดยการค้นหาที่เกิดขึ้นภายในงานย่อยที่เกิดขึ้นพร้อมกัน 

  5. แนวคิดนี้คล้ายกับการทำงานพร้อมกันแบบมีโครงสร้างของ Java jeps/428 

  6. ซึ่งคล้ายกับการสร้างชุดข้อความและเข้าร่วมชุดข้อความนั้นเพื่อให้ได้องค์ประกอบตามลำดับ