ภาพรวม
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 อย่าง
- การเรียกกลับ - อธิบายตำแหน่งที่จะจัดเก็บผลลัพธ์ของการคำนวณแบบอะซิงโครนัส
- Continuation - ระบุสถานะการดำเนินการถัดไป
ต้องใช้ Callback เมื่อเรียกใช้การดำเนินการแบบไม่พร้อมกัน ซึ่งหมายความว่าการดำเนินการจริงจะไม่เกิดขึ้นทันทีเมื่อเรียกใช้เมธอด เช่น ในกรณีของการค้นหา SkyValue ควรทำให้การเรียกกลับเรียบง่ายที่สุด
Continuations คือค่าที่ส่งคืนของ StateMachine และ
ห่อหุ้มการดำเนินการที่ซับซ้อนซึ่งจะเกิดขึ้นเมื่อการคำนวณแบบอะซิงโครนัสทั้งหมด
ได้รับการแก้ไขStateMachine แนวทางที่มีโครงสร้างนี้ช่วยให้จัดการความซับซ้อนของ
การเรียกกลับได้
งาน
อินเทอร์เฟซ 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 ขอให้ดำเนินการงานย่อยที่ทำงานพร้อมกันอย่างมีตรรกะ
โดย Subtask ก็เป็น StateMachine เช่นกัน และทำทุกอย่างที่ StateMachine ปกติทำได้ รวมถึงสร้าง Subtask เพิ่มเติมแบบเรียกซ้ำหรือค้นหา 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;
}
}
โฟลว์ข้อมูล
การสนทนาก่อนหน้านี้มุ่งเน้นไปที่การจัดการโฟลว์การควบคุม ส่วนนี้จะอธิบายการส่งต่อค่าข้อมูล
การใช้การเรียกกลับ 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
จนถึงตอนนี้ เอกสารนี้อธิบายเพียงวิธีจัดเรียงงานในงานย่อย แต่ งานย่อยยังต้องรายงานค่ากลับไปยังผู้เรียกด้วย เนื่องจากงานย่อยทำงานแบบไม่พร้อมกันตามตรรกะ ระบบจึงจะสื่อสารผลลัพธ์กลับไปยังผู้เรียกใช้โดยใช้การเรียกกลับ โดยงานย่อยจะกำหนดอินเทอร์เฟซ Sink ที่จะแทรกผ่านตัวสร้าง
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 Callback เมื่อกลับมาทำงานต่อ processResult จะตรวจสอบว่า
value เป็นค่าว่างหรือไม่เพื่อดูว่าเกิดข้อผิดพลาดหรือไม่ ซึ่งเป็นรูปแบบลักษณะการทำงานทั่วไป หลังจากยอมรับเอาต์พุตจากทั้งงานย่อยหรือการค้นหา SkyValue
โปรดทราบว่าการใช้งาน acceptBarError จะส่งต่อผลลัพธ์ไปยัง Caller.ResultSink โดยทันทีตามที่ Error bubbling กำหนด
ทางเลือกสำหรับ StateMachine ระดับบนสุดอธิบายไว้ในDriverและการเชื่อมต่อกับ SkyFunctions
การจัดการข้อผิดพลาด
มีตัวอย่างการจัดการข้อผิดพลาด 2 ตัวอย่างในTasks.lookUp
การเรียกกลับและการส่งต่อค่าระหว่าง
StateMachines ระบบจะไม่แสดงข้อยกเว้นอื่นๆ นอกเหนือจาก
InterruptedException แต่จะส่งผ่านข้อยกเว้นเหล่านั้นผ่าน
การเรียกกลับเป็นค่าแทน โดยทั่วไปแล้ว การเรียกกลับดังกล่าวมักมีความหมายของ "หรือ" แบบเอกซ์คลูซีฟ โดยจะมีการส่งค่าหรือข้อผิดพลาดอย่างใดอย่างหนึ่งเท่านั้น
ส่วนถัดไปจะอธิบายการโต้ตอบที่ละเอียดแต่สําคัญกับการจัดการข้อผิดพลาดของ Skyframe
การส่งต่อข้อผิดพลาด (--nokeep_going)
ในระหว่างการส่งต่อข้อผิดพลาด ระบบอาจรีสตาร์ท SkyFunction แม้ว่าจะไม่มี SkyValue ที่ขอทั้งหมดก็ตาม ในกรณีดังกล่าว ระบบจะไม่เข้าสู่สถานะถัดไปเนื่องจากTasksสัญญา API อย่างไรก็ตาม StateMachine ควร
ยังคงส่งต่อข้อยกเว้น
เนื่องจากการเผยแพร่ต้องเกิดขึ้นไม่ว่าสถานะถัดไปจะถึงหรือไม่ก็ตาม
คอลแบ็กการจัดการข้อผิดพลาดจึงต้องทำงานนี้ สำหรับ StateMachine ภายใน
จะทำได้โดยการเรียกใช้การเรียกกลับขององค์ประกอบหลัก
ที่ระดับบนสุด StateMachine ซึ่งเชื่อมต่อกับ SkyFunction คุณสามารถทำได้โดยการเรียกใช้เมธอด setException ของ ValueOrExceptionProducer
ValueOrExceptionProducer.tryProduceValue จะส่งข้อยกเว้นออกมา แม้ว่าจะมี SkyValues ที่ขาดหายไปก็ตาม
หากมีการใช้ Driver โดยตรง คุณต้องตรวจสอบข้อผิดพลาดที่ส่งต่อจาก SkyFunction แม้ว่าเครื่องจะประมวลผลไม่เสร็จก็ตาม
การจัดการเหตุการณ์
สำหรับ SkyFunctions ที่ต้องปล่อยเหตุการณ์ ระบบจะแทรก StoredEventHandler ลงใน SkyKeyComputeState และแทรกลงใน StateMachine ที่ต้องใช้
เหตุการณ์เหล่านั้น ก่อนหน้านี้จำเป็นต้องใช้ StoredEventHandler เนื่องจาก Skyframe จะไม่แสดง
บางเหตุการณ์หากไม่ได้เล่นซ้ำ แต่ต่อมาปัญหานี้ได้รับการแก้ไขแล้ว
ระบบจะเก็บการแทรก StoredEventHandler ไว้เนื่องจากช่วยลดความซับซ้อนของ
การติดตั้งใช้งานเหตุการณ์ที่ปล่อยออกมาจากแฮนเดิลเลอร์ข้อผิดพลาด
Drivers และการเชื่อมต่อกับ 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 จะทิ้งข้อยกเว้นที่ตั้งค่าไว้ แม้ว่าเครื่องจะยังทำงานไม่เสร็จก็ตาม
บทส่งท้าย: การนำการโทรกลับออกในที่สุด
StateMachineเป็นวิธีที่มีประสิทธิภาพสูงแต่ต้องใช้โค้ดสำเร็จรูปจำนวนมากในการดำเนินการ
การคำนวณแบบอะซิงโครนัส การดำเนินการต่อ (โดยเฉพาะในรูปแบบของ Runnables
ที่ส่งไปยัง ListenableFuture) มีอยู่ทั่วไปในบางส่วนของโค้ด Bazel
แต่ไม่พบใน SkyFunction ของการวิเคราะห์ การวิเคราะห์ส่วนใหญ่ขึ้นอยู่กับ CPU และไม่มี API แบบอะซิงโครนัสที่มีประสิทธิภาพสำหรับ I/O ของดิสก์ ในที่สุด การเพิ่มประสิทธิภาพเพื่อลด Callback ก็จะเป็นประโยชน์ เนื่องจากมีการเรียนรู้ที่ซับซ้อนและทำให้ความสามารถในการอ่านลดลง
เธรดเสมือนของ Java เป็นหนึ่งในทางเลือกที่มีแนวโน้มมากที่สุด แทนที่จะต้องเขียนการเรียกกลับ ทุกอย่างจะถูกแทนที่ด้วยการเรียกแบบซิงโครนัสที่บล็อก
ซึ่งเป็นไปได้เนื่องจากการเชื่อมโยงทรัพยากรของเธรดเสมือนนั้นควรมีค่าใช้จ่ายต่ำ ซึ่งต่างจากเธรดแพลตฟอร์ม อย่างไรก็ตาม แม้จะมีเธรดเสมือน การแทนที่การดำเนินการแบบซิงโครนัสอย่างง่ายด้วยการสร้างเธรดและการซิงโครไนซ์
ดั้งเดิมก็มีค่าใช้จ่ายสูงเกินไป เราได้ทำการย้ายข้อมูลจาก StateMachines ไปยัง
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 ที่บันทึกไว้ต้องเป็น ตัวแปรสุดท้ายที่มีผล ดังนั้นการใช้ตัวแปรดังกล่าวจึงอาจยุ่งยาก การซ้อนกันหลายชั้นจะหลีกเลี่ยงได้โดยการส่งคืนการอ้างอิงเมธอดเป็นคอนทินิวเอชันแทนแลมบ์ดา ดังที่แสดงด้านล่าง
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 แบบเมเจอร์หรือแบบเต็มที่มีราคาแพงกว่าเพื่อล้างข้อมูล
ข้อควรระวังเบื้องต้นคือการลดการใช้ตัวแปร StateMachine แต่
ในบางกรณีก็ทำไม่ได้ เช่น หากต้องใช้ค่าในหลายสถานะ
หากเป็นไปได้ ตัวแปรสแต็กภายใน step จะเป็นตัวแปรของรุ่นใหม่
และมีการรวบรวมขยะอย่างมีประสิทธิภาพ
สำหรับตัวแปร StateMachine การแบ่งงานออกเป็นงานย่อยและการทำตาม รูปแบบที่แนะนำสำหรับการส่งต่อค่าระหว่าง StateMachine ก็มีประโยชน์เช่นกัน โปรดสังเกตว่าเมื่อทำตามรูปแบบนี้ เฉพาะ StateMachine ย่อยเท่านั้นที่จะมีการอ้างอิงถึง StateMachine หลัก และในทางกลับกันจะไม่มีการอ้างอิง ซึ่งหมายความว่าเมื่อบุตรหลานทำภารกิจเสร็จและ
อัปเดตผู้ปกครองโดยใช้การเรียกกลับผลลัพธ์ บุตรหลานจะอยู่นอก
ขอบเขตโดยอัตโนมัติและมีสิทธิ์รับ GC
สุดท้ายนี้ ในบางกรณี คุณอาจต้องใช้ตัวแปร StateMachine ในสถานะก่อนหน้า
แต่ไม่จำเป็นต้องใช้ในสถานะต่อๆ ไป การล้างข้อมูลอ้างอิงของออบเจ็กต์ขนาดใหญ่ อาจเป็นประโยชน์เมื่อทราบว่าไม่จำเป็นต้องใช้ออบเจ็กต์เหล่านั้นอีกต่อไป
การตั้งชื่อสถานะ
โดยปกติแล้ว เมื่อตั้งชื่อเมธอด คุณจะตั้งชื่อเมธอดตามลักษณะการทำงาน
ที่เกิดขึ้นภายในเมธอดนั้นได้ แต่ใน StateMachine จะไม่ชัดเจนว่าจะทำอย่างไรเนื่องจากไม่มีสแต็ก ตัวอย่างเช่น สมมติว่าเมธอด foo
เรียกใช้เมธอดรอง bar ใน StateMachine สามารถแปลเป็นลำดับสถานะ foo ตามด้วย bar foo ไม่รวมพฤติกรรม
bar อีกต่อไป ด้วยเหตุนี้ ชื่อเมธอดสำหรับรัฐจึงมักมีขอบเขตที่แคบกว่า
ซึ่งอาจสะท้อนถึงพฤติกรรมในท้องถิ่น
แผนภาพต้นไม้แบบพร้อมกัน
ต่อไปนี้เป็นมุมมองอื่นของแผนภาพในStructured concurrency ซึ่งแสดงโครงสร้างแบบต้นไม้ได้ดีกว่า บล็อกจะประกอบกันเป็นต้นไม้เล็กๆ
-
ซึ่งแตกต่างจากธรรมเนียมของ Skyframe ที่จะรีสตาร์ทจากจุดเริ่มต้นเมื่อไม่มีค่า ↩
-
โปรดทราบว่า
stepได้รับอนุญาตให้ส่งInterruptedExceptionแต่ตัวอย่าง จะละเว้นส่วนนี้ มีวิธีการระดับล่างบางอย่างในโค้ด Bazel ที่ทำให้เกิดข้อยกเว้นนี้ และข้อยกเว้นจะแพร่กระจายไปยังDriverซึ่งจะอธิบายในภายหลัง ที่เรียกใช้StateMachineคุณไม่จำเป็นต้องประกาศว่ามีการขว้างเมื่อไม่จำเป็น ↩ -
งานย่อยที่ทำงานพร้อมกันได้รับแรงบันดาลใจจาก
ConfiguredTargetFunctionซึ่งทำงานแยกกันสำหรับแต่ละการอ้างอิง แทนที่จะจัดการโครงสร้างข้อมูลที่ซับซ้อนซึ่งประมวลผลทรัพยากร Dependency ทั้งหมดพร้อมกัน ซึ่งทำให้เกิดความไม่มีประสิทธิภาพ ทรัพยากร Dependency แต่ละรายการจะมีStateMachineของตัวเองโดยอิสระ ↩ -
ระบบจะจัดกลุ่มการเรียก
tasks.lookUpหลายครั้งภายในขั้นตอนเดียวเข้าด้วยกัน การจัดกลุ่มเพิ่มเติมสามารถสร้างได้โดยการค้นหาที่เกิดขึ้นภายใน งานย่อยที่พร้อมกัน ↩ -
ซึ่งมีแนวคิดคล้ายกับ Structured Concurrency ของ Java jeps/428 ↩
-
การทำเช่นนี้คล้ายกับการสร้างเธรดและเข้าร่วมเธรดเพื่อสร้าง องค์ประกอบตามลำดับ ↩