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

รายงานปัญหา ดูแหล่งที่มา Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

ภาพรวม

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

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

บทนำ

ส่วนนี้จะอธิบายและแนะนำ StateMachines ที่พบในแพ็กเกจ java.com.google.devtools.build.skyframe.state โดยย่อ

ข้อมูลเบื้องต้นโดยย่อเกี่ยวกับการรีสตาร์ท Skyframe

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

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

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

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

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

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

Continuations คือStateMachine ค่าที่ส่งคืนของ StateMachines และ ห่อหุ้มการดำเนินการที่ซับซ้อนซึ่งจะเกิดขึ้นเมื่อการคำนวณแบบอะซิงโครนัสทั้งหมด ได้รับการแก้ไข แนวทางที่มีโครงสร้างนี้ช่วยให้จัดการความซับซ้อนของ การเรียกกลับได้

งาน

Tasks อินเทอร์เฟซมี API สำหรับ StateMachines เพื่อค้นหา 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 และมีลักษณะการจัดการข้อยกเว้นที่คล้ายกัน การติดตั้งใช้งานไม่ได้ทำการค้นหาในทันที แต่จะจัดกลุ่มการค้นหาให้ได้มากที่สุดก่อนดำเนินการ ค่าอาจไม่พร้อมใช้งานในทันที เช่น ต้องรีสตาร์ท Skyframe ดังนั้นผู้เรียกจึงระบุสิ่งที่ต้องทำกับค่าที่ได้โดยใช้การเรียกกลับ

StateMachine โปรเซสเซอร์ (Drivers และการเชื่อมต่อกับ 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 ขอให้ดำเนินการงานย่อยที่ทำงานพร้อมกันอย่างมีตรรกะ นอกจากนี้ งานย่อยยังเป็น StateMachines และทำทุกอย่างที่ StateMachines ปกติทำได้ รวมถึงการสร้างงานย่อยเพิ่มเติมแบบเรียกซ้ำหรือการค้นหา 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, S โดย M1 และ M2 เป็นลำดับ <A, S, B> และ <X, S, Y> ตามลำดับStateMachine ปัญหาคือ 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 ในทางที่ผิดก็เป็นไปไม่ได้เนื่องจากมีงานย่อยอื่นๆ ที่ทำงานคู่ขนานกันหรือการเรียก 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;
  }
}

โฟลว์ข้อมูล

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

การใช้การเรียกกลับ Tasks.lookUp

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

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

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

การใช้ SkyValue หลายรายการ

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

การเผยแพร่ค่าระหว่าง 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 โดยทันทีตามที่ Error bubbling กำหนด

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

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

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

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

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

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

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

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

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

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

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

จากนั้นโค้ดสำหรับการคำนวณผลลัพธ์แบบเลื่อนเวลาอาจมีลักษณะดังนี้

@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 มากกว่า

StateMachine ที่อาจทำให้เกิดข้อยกเว้น

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

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

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

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

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

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

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

ภาคผนวก

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

ตัวอย่าง: การค้นหา SkyValue แบบเชื่อมโยง

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

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

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

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

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

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

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

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

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

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

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

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

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

แผนผังต้นไม้แบบพร้อมกัน

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

Structured Concurrency 3D


  1. ซึ่งแตกต่างจากธรรมเนียมของ Skyframe ที่จะรีสตาร์ทจากจุดเริ่มต้นเมื่อไม่มีค่า 

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

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

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

  5. ซึ่งมีแนวคิดคล้ายกับ Structured Concurrency ของ Java jeps/428 

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