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

รายงานปัญหา ดูแหล่งที่มา

ภาพรวม

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 เมื่อมีการขอ SkyValues ทั้งหมดก่อนหน้านี้พร้อมใช้งาน

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

Callback, ความต่อเนื่อง และการคำนวณแบบอะซิงโครนัส

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

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

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

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

Continuations คือค่าการแสดงผล 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 เป็นประจำทำได้ ซึ่งรวมถึงการสร้างงานย่อยๆ เพิ่มซ้ำๆ หรือค้นหา 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 หลังจากทำงานเสร็จแล้ว และ 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

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

โปรดดูตัวอย่างการใช้ Callback Tasks.lookUp ในการค้นหา SkyValue ส่วนนี้จะให้เหตุผลและแนะนำวิธีในการจัดการ SkyValues หลายรายการ

Callback Tasks.lookUp รายการ

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

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

วิธีที่มีลักษณะเฉพาะคือการใช้ Java lambda ในการนําสิ่งนี้ไปใช้

  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 กับ Callback โดยตรงจะเป็นการบันทึกการจัดสรรหน่วยความจำสำหรับ 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);
  }

คุณแชร์การใช้งาน Callback Consumer<SkyValue> ได้อย่างชัดเจนเนื่องจากประเภทค่าแตกต่างกัน หากไม่เป็นเช่นนั้น การเปลี่ยนกลับไปใช้แบบที่ใช้ lambda หรืออินสแตนซ์คลาสภายในเต็มรูปแบบที่ใช้ Callback ที่เหมาะสมก็สามารถทำได้

กำลังเผยแพร่ค่าระหว่าง 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 จะใช้ Callback BarProducer.ResultSink เมื่อกลับมาดำเนินการต่อ processResult จะตรวจสอบว่า value เป็นค่าว่างหรือไม่เพื่อพิจารณาว่าเกิดข้อผิดพลาดหรือไม่ ซึ่งเป็นรูปแบบลักษณะการทำงานทั่วไปหลังจากยอมรับเอาต์พุตจากงานย่อยหรือการค้นหา SkyValue

โปรดทราบว่าการใช้งาน acceptBarError จะส่งต่อผลลัพธ์ไปยัง Caller.ResultSink อย่างตั้งใจ ตามที่เกิดข้อผิดพลาดในการฟองอากาศ

ทางเลือกสำหรับ StateMachine ระดับบนสุดจะอธิบายไว้ใน Driver และการบริดจ์กับ SkyFunctions

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

มี 2-3 ตัวอย่างของการจัดการข้อผิดพลาดที่มีอยู่แล้วใน Tasks.lookUp callback และการเผยแพร่ค่าระหว่าง StateMachines ระบบจะไม่ส่งข้อยกเว้นนอกเหนือจาก InterruptedException แต่จะส่งผ่าน Callback เป็นค่าแทน การเรียกกลับดังกล่าวมักมีเอกสิทธิ์หรืออรรถศาสตร์เฉพาะตัว โดยมีค่าหรือข้อผิดพลาดอย่างใดอย่างหนึ่งที่ส่งไป

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

เกิดข้อผิดพลาดขณะทำบับเบิล (--nokeep_going)

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

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

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

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

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

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

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

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

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

บทส่งท้าย: การนำ Callback ออกในที่สุด

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

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

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

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

ภาคผนวก

เรียกกลับนรก

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

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

ข้อดีอย่างหนึ่งของการติดตั้งใช้งานที่ซ้อนกันคือจะเก็บรักษาสแต็กเฟรมของขั้นตอนด้านนอกได้ ใน Java ตัวแปร lambda ที่บันทึกไว้ต้องเป็นตัวแปรสุดท้ายที่มีประสิทธิภาพ ดังนั้นการใช้ตัวแปรเช่นนี้อาจเป็นเรื่องยุ่งยาก การซ้อนแบบ Deep Link นั้นหลีกเลี่ยงโดยการส่งคืนเมธอดที่อ้างอิงเป็นความต่อเนื่องแทน 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 รายการแรก หากคิดไปเองแบบไร้เดียงสา สิ่งนี้จะทำให้เกิดโครงสร้าง Callback ที่ซับซ้อนและฝังลึกลงไปอีก

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 โปรดทราบว่าในที่นี้ จะใช้ lambda เพื่อกำหนด value2 ซึ่งทำให้การจัดลำดับโค้ดตรงกับการเรียงลำดับการคำนวณจากบนลงล่าง

เคล็ดลับเบ็ดเตล็ด

ความอ่านง่าย: การสั่งดำเนินการ

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

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

สมมติฐานเกี่ยวกับรุ่น

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

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

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

สำหรับตัวแปร 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 ซึ่งทำงานอิสระสำหรับทรัพยากร Dependency แต่ละอย่าง แทนที่จะจัดการโครงสร้างข้อมูลที่ซับซ้อนซึ่งประมวลผลทรัพยากร Dependency ทั้งหมดในคราวเดียว ทำให้เกิดความด้อยประสิทธิภาพ ทรัพยากร Dependency แต่ละรายการจะมี StateMachine เป็นของตนเอง

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

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

  6. การดำเนินการนี้จะคล้ายกับการสร้างชุดข้อความและการรวมชุดข้อความเข้าด้วยกันเพื่อสร้างองค์ประกอบตามลำดับ