ภาพรวม
Skyframe StateMachine
คือออบเจ็กต์ฟังก์ชันที่แยกวิเคราะห์แล้วซึ่งอยู่ในกอง รองรับการประเมินที่ยืดหยุ่นและไม่ซ้ำซ้อน1 เมื่อค่าที่จำเป็นไม่พร้อมใช้งานทันที แต่คํานวณแบบไม่พร้อมกัน StateMachine
จะไม่สามารถผูกทรัพยากรเธรดขณะรอได้ แต่ต้องถูกระงับและกลับมาทำงานต่อ ดังนั้นการแยกโครงสร้างจึงแสดงจุดเข้าใหม่อย่างชัดเจนเพื่อให้ข้ามการคํานวณก่อนหน้าได้
StateMachine
ใช้เพื่อแสดงลําดับ การแยกย่อย การทำงานพร้อมกันแบบมีโครงสร้างเชิงตรรกะ และปรับให้เหมาะกับการโต้ตอบกับ Skyframe โดยเฉพาะ StateMachine
สามารถประกอบเป็น StateMachine
ที่ใหญ่ขึ้นและแชร์ StateMachine
ย่อยได้ การทำงานพร้อมกันจะจัดเรียงตามลําดับชั้นเสมอโดยโครงสร้างและเป็นแบบตรรกะล้วนๆ งานย่อยที่ทำงานพร้อมกันทุกรายการจะทํางานในเธรด SkyFunction หลักที่แชร์รายการเดียว
บทนำ
ส่วนนี้ช่วยสร้างแรงบันดาลใจและแนะนำ StateMachine
ในแพ็กเกจ java.com.google.devtools.build.skyframe.state
ข้อมูลเบื้องต้นเกี่ยวกับการรีสตาร์ท Skyframe
Skyframe เป็นเฟรมเวิร์กที่ทำการประเมินแบบคู่ขนานของกราฟทรัพยากร Dependency
โหนดแต่ละโหนดในกราฟจะสอดคล้องกับการประเมิน SkyFunction ที่มี SkyKey ที่ระบุพารามิเตอร์และ SkyValue ที่ระบุผลลัพธ์ รูปแบบการประมวลผลเป็นแบบที่ SkyFunction อาจค้นหา SkyValues ตาม SkyKey ซึ่งจะทริกเกอร์การประเมิน SkyFunction เพิ่มเติมแบบซ้ำซ้อนและขนานกัน แทนที่จะบล็อก ซึ่งจะผูกชุดข้อความ เมื่อ SkyValue ที่ขอยังไม่พร้อมใช้งานเนื่องจากกราฟย่อยของการคำนวณบางส่วนไม่สมบูรณ์ SkyFunction จะเฝ้าดูการตอบกลับ null
getValue
และควรแสดงผล null
แทนที่จะเป็น SkyValue ซึ่งส่งสัญญาณว่าไม่สมบูรณ์เนื่องจากไม่มีอินพุต
Skyframe จะรีสตาร์ท SkyFunctions เมื่อ SkyValue ที่ขอก่อนหน้านี้ทั้งหมดพร้อมใช้งาน
ก่อนการเริ่มใช้ SkyKeyComputeState
วิธีดั้งเดิมในการจัดการกับการรีสตาร์ทคือการคำนวณผลใหม่โดยสมบูรณ์ แม้ว่าวิธีนี้จะซับซ้อนเป็นกำลังสอง แต่ฟังก์ชันที่เขียนในรูปแบบนี้จะเสร็จสมบูรณ์ในที่สุดเพราะเรียกใช้ซ้ำแต่ละครั้ง การค้นหาน้อยลงจะแสดง null
SkyKeyComputeState
ช่วยให้คุณเชื่อมโยงข้อมูลจุดตรวจสอบที่ระบุด้วยตนเองกับ SkyFunction ได้ ซึ่งจะช่วยประหยัดเวลาในการคํานวณใหม่ได้อย่างมาก
StateMachine
คือออบเจ็กต์ที่อยู่ใน SkyKeyComputeState
และช่วยลดการคำนวณซ้ำเกือบทั้งหมดเมื่อ SkyFunction เริ่มทำงานอีกครั้ง (โดยสมมติว่า SkyKeyComputeState
ไม่ได้ออกจากแคช) ด้วยการแสดงฮุกการหยุดชั่วคราวและดำเนินการต่อ
การคํานวณที่มีสถานะภายใน SkyKeyComputeState
จากมุมมองการออกแบบเชิงออบเจ็กต์ การพิจารณาจัดเก็บออบเจ็กต์การประมวลผลภายใน SkyKeyComputeState
แทนค่าข้อมูลล้วนๆ นั้นสมเหตุสมผล
ใน Java คำอธิบายขั้นต่ำที่สุดของออบเจ็กต์ที่แสดงลักษณะการทำงานคืออินเทอร์เฟซแบบฟังก์ชัน และก็เพียงพอแล้ว StateMachine
มีคำจำกัดความแบบซ้ำซ้อนที่น่าสนใจดังต่อไปนี้2
@FunctionalInterface
public interface StateMachine {
StateMachine step(Tasks tasks) throws InterruptedException;
}
อินเทอร์เฟซ Tasks
คล้ายกับ SkyFunction.Environment
แต่ออกแบบมาเพื่อการทำงานแบบไม่พร้อมกันและเพิ่มการรองรับงานย่อยที่ทำงานพร้อมกันตามตรรกะ3
ค่าที่ส่งกลับมาจาก step
คือ StateMachine
อีกแบบหนึ่ง ซึ่งทำให้สามารถกำหนดลำดับขั้นตอนได้ในแบบอุปนัย step
จะแสดงผลเป็น DONE
เมื่อStateMachine
เสร็จสิ้น เช่น
class HelloWorld implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
System.out.println("hello");
return this::step2; // The next step is HelloWorld.step2.
}
private StateMachine step2(Tasks tasks) {
System.out.println("world");
// DONE is special value defined in the `StateMachine` interface signaling
// that the computation is done.
return DONE;
}
}
อธิบาย StateMachine
ที่มีเอาต์พุตต่อไปนี้
hello
world
โปรดทราบว่าการอ้างอิงเมธอด this::step2
ก็เป็น StateMachine
ด้วยเนื่องจาก
step2
เป็นไปตามคําจํากัดความอินเทอร์เฟซฟังก์ชันของ StateMachine
การอ้างอิงเมธอดเป็นวิธีที่พบบ่อยที่สุดในการระบุสถานะถัดไปใน StateMachine
โดยทั่วไปแล้ว การแยกการคํานวณออกเป็น StateMachine
ขั้นตอนแทนที่จะใช้ฟังก์ชันแบบโมโนลิธิกจะให้ฮุกที่จําเป็นในการระงับและดําเนินการต่อการคํานวณ เมื่อ StateMachine.step
แสดงผล จะมีจุดการระงับที่ชัดเจน ความต่อเนื่องที่ระบุโดยค่า StateMachine
ที่แสดงผลเป็นจุดกลับมาทำงานอีกครั้งอย่างชัดเจน จึงหลีกเลี่ยงการคํานวณใหม่ได้เนื่องจากสามารถดําเนินการคํานวณต่อจากจุดที่ค้างไว้ได้
การเรียกกลับ การดําเนินการต่อ และการคำนวณแบบอะซิงโครนัส
ในทางเทคนิคแล้ว StateMachine
จะทำหน้าที่เป็นความต่อเนื่อง ซึ่งจะกำหนดการคำนวณลำดับต่อมา แทนที่จะบล็อก StateMachine
สามารถหยุดชั่วคราวโดยสมัครใจได้โดยกลับจากฟังก์ชัน step
ซึ่งจะโอนการควบคุมกลับไปยังอินสแตนซ์ Driver
จากนั้น Driver
จะเปลี่ยนไปเป็น StateMachine
ที่พร้อมใช้งานหรือมอบการควบคุมกลับไปยัง Skyframe ก็ได้
แต่เดิมนั้น การติดต่อกลับและความต่อเนื่องจะรวมเข้าด้วยกันเป็นแนวคิดเดียว
อย่างไรก็ตาม StateMachine
จะยังคงแยกความแตกต่างระหว่าง 2 รายการนี้
- การติดต่อกลับ - อธิบายตําแหน่งที่จะจัดเก็บผลลัพธ์ของการคํานวณแบบไม่สอดคล้อง
- การดําเนินการต่อ - ระบุสถานะการดําเนินการถัดไป
ต้องมีคอลแบ็กเมื่อเรียกใช้การดำเนินการแบบไม่พร้อมกัน ซึ่งหมายความว่าการดำเนินการจริงจะไม่เกิดขึ้นทันทีที่เรียกใช้เมธอด ดังในกรณีการค้นหา SkyValue การทำซ้ำควรทำอย่างง่ายที่สุด
การดำเนินการต่อคือค่าที่ StateMachine
แสดงผลของ StateMachine
และรวมการดำเนินการที่ซับซ้อนซึ่งจะตามมาเมื่อการคํานวณแบบไม่สอดคล้องกันทั้งหมดเสร็จสมบูรณ์ แนวทางที่มีโครงสร้างนี้ช่วยให้จัดการความซับซ้อนของ callback ได้
งาน
อินเทอร์เฟซ Tasks
มี API สำหรับ StateMachine
เพื่อค้นหา SkyValues ตาม SkyKey และกำหนดเวลางานย่อยพร้อมกัน
interface Tasks {
void enqueue(StateMachine subtask);
void lookUp(SkyKey key, Consumer<SkyValue> sink);
<E extends Exception>
void lookUp(SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);
// lookUp overloads for 2 and 3 exception types exist, but are elided here.
}
การค้นหา SkyValue
StateMachine
มีการใช้โอเวอร์โหลด Tasks.lookUp
เพื่อค้นหา SkyValues ซึ่งคล้ายกับ SkyFunction.Environment.getValue
และ SkyFunction.Environment.getValueOrThrow
และมีความหมายคล้ายกับการจัดการข้อยกเว้น การใช้งานนี้ไม่ได้ทำการค้นหาในทันที แต่จะค้นหาแบบกลุ่ม4เป็นจำนวนมากที่สุดเท่าที่จะเป็นไปได้ก่อนดำเนินการแทน ค่าดังกล่าวอาจไม่พร้อมใช้งานทันที เช่น ต้องรีสตาร์ท Skyframe ดังนั้นผู้เรียกใช้จึงระบุสิ่งที่ต้องทำกับค่าผลลัพธ์โดยใช้ Callback
โปรเซสเซอร์ StateMachine
(Driver
และบริดจ์กับ SkyFrame) รับประกันว่าค่าจะพร้อมใช้งานก่อนที่สถานะถัดไปจะเริ่มขึ้น ตัวอย่างมีดังนี้
class DoesLookup implements StateMachine, Consumer<SkyValue> {
private Value value;
@Override
public StateMachine step(Tasks tasks) {
tasks.lookUp(new Key(), (Consumer<SkyValue>) this);
return this::processValue;
}
// The `lookUp` call in `step` causes this to be called before `processValue`.
@Override // Implementation of Consumer<SkyValue>.
public void accept(SkyValue value) {
this.value = (Value)value;
}
private StateMachine processValue(Tasks tasks) {
System.out.println(value); // Prints the string representation of `value`.
return DONE;
}
}
ในตัวอย่างข้างต้น ขั้นตอนแรกจะค้นหา new Key()
โดยส่ง this
เป็นผู้บริโภค ที่เป็นไปได้เนื่องจาก DoesLookup
จะใช้ Consumer<SkyValue>
ตามสัญญา ก่อนที่สถานะ DoesLookup.processValue
ถัดไปจะเริ่มขึ้น การค้นหา DoesLookup.step
ทั้งหมดจะต้องเสร็จสมบูรณ์ ดังนั้น value
จึงพร้อมใช้งานเมื่อเข้าถึงใน processValue
งานย่อย
Tasks.enqueue
ส่งคําขอดําเนินการงานย่อยที่เกิดขึ้นพร้อมกันตามตรรกะ
งานย่อยยังเป็น StateMachine
ด้วยและทําสิ่งต่างๆ ได้เช่นเดียวกับ StateMachine
ทั่วไป ซึ่งรวมถึงการสร้างงานย่อยเพิ่มเติมแบบซ้ำๆ หรือค้นหา SkyValue
เช่นเดียวกับ lookUp
โปรแกรมควบคุมสถานะเครื่องจักรจะตรวจสอบว่างานย่อยทั้งหมดเสร็จสมบูรณ์แล้วก่อนที่จะไปยังขั้นตอนถัดไป ตัวอย่างมีดังนี้
class Subtasks implements StateMachine {
private int i = 0;
@Override
public StateMachine step(Tasks tasks) {
tasks.enqueue(new Subtask1());
tasks.enqueue(new Subtask2());
// The next step is Subtasks.processResults. It won't be called until both
// Subtask1 and Subtask 2 are complete.
return this::processResults;
}
private StateMachine processResults(Tasks tasks) {
System.out.println(i); // Prints "3".
return DONE; // Subtasks is done.
}
private class Subtask1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
i += 1;
return DONE; // Subtask1 is done.
}
}
private class Subtask2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
i += 2;
return DONE; // Subtask2 is done.
}
}
}
แม้ว่า Subtask1
และ Subtask2
จะทำงานพร้อมกันตามตรรกะ แต่ทุกอย่างจะทํางานในเธรดเดียว การอัปเดต i
แบบ "พร้อมกัน" จึงไม่จำเป็นต้องมีการซิงค์
การเกิดขึ้นพร้อมกันแบบมีโครงสร้าง
เนื่องจากทุกๆ lookUp
และ enqueue
ต้องจับคู่กันก่อนที่จะไปยังสถานะถัดไป นั่นหมายความว่าโดยทั่วไปแล้วการเกิดขึ้นพร้อมกันจะจำกัดไว้เฉพาะโครงสร้างแบบต้นไม้ คุณสามารถสร้างการเกิดขึ้นพร้อมกันตามลําดับชั้น5 ได้ดังที่แสดงในตัวอย่างต่อไปนี้
มองจาก UML แทบจะไม่เห็นเลยว่าโครงสร้างการทำงานพร้อมกันจะสร้างเป็นต้นไม้ มีมุมมองอื่นที่แสดงโครงสร้างต้นไม้ได้ดีกว่า
การเรียกใช้แบบหลายรายการพร้อมกันแบบมีโครงสร้างนั้นเข้าใจได้ง่ายกว่ามาก
การกำหนดรูปแบบโฟลว์การเรียบเรียงและการควบคุม
ส่วนนี้จะแสดงตัวอย่างการคอมโพสิชัน StateMachine
หลายรายการและวิธีแก้ปัญหาเกี่ยวกับโฟลว์การควบคุมบางอย่าง
สถานะตามลำดับ
รูปแบบนี้ถือเป็นรูปแบบการไหลของการควบคุมที่พบบ่อยที่สุดและตรงไปตรงมา ซึ่งตัวอย่างจะแสดงในการคำนวณแบบเก็บสถานะภายใน SkyKeyComputeState
การแตกแขนง
คุณจะดำเนินการสถานะ Branch ใน StateMachine
ได้ด้วยการส่งคืนค่าที่แตกต่างกันโดยใช้ขั้นตอนการควบคุม Java ปกติ ดังที่แสดงในตัวอย่างต่อไปนี้
class Branch implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
// Returns different state machines, depending on condition.
if (shouldUseA()) {
return this::performA;
}
return this::performB;
}
…
}
การที่บางสาขาแสดงผลเป็น DONE
เป็นเรื่องปกติมากสำหรับการทำให้เสร็จสิ้นก่อนกำหนด
องค์ประกอบตามลําดับขั้นสูง
เนื่องจากโครงสร้างการควบคุม StateMachine
ไม่มีการจดจำ บางครั้งการแชร์คำจำกัดความ StateMachine
เป็นงานย่อยจึงอาจไม่สะดวก อนุญาตให้ M1 และ M2 เป็น StateMachine
อินสแตนซ์ที่แชร์ StateMachine
, S โดยมี M1 และ M2 เป็นลำดับ <A, S, B> และ <X, S, Y> ตามลำดับ ปัญหาคือ S ไม่รู้ว่าจะดำเนินการต่อไปยัง B หรือ Y หลังจากที่ดำเนินการเสร็จสิ้น และ StateMachine
ไม่ได้เก็บกองซ้อนการเรียก ส่วนนี้จะอธิบายเทคนิคบางอย่างในการบรรลุเป้าหมายนี้
StateMachine
เป็นองค์ประกอบลำดับเทอร์มินัล
ซึ่งไม่ได้ช่วยแก้ปัญหาแรกที่เกิดขึ้น โดยจะแสดงเฉพาะการเรียงลำดับการคอมโพสิชันเมื่อ StateMachine
ที่แชร์เป็นองค์ประกอบสุดท้ายในลำดับ
// S is the shared state machine.
class S implements StateMachine { … }
class M1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performA();
return new S();
}
}
class M2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performX();
return new S();
}
}
ซึ่งจะใช้ได้แม้ว่า S จะเป็นเครื่องสถานะที่ซับซ้อนก็ตาม
งานย่อยสําหรับการเขียนตามลําดับ
เนื่องจากระบบรับประกันว่างานย่อยที่อยู่ในคิวจะเสร็จสมบูรณ์ก่อนสถานะถัดไป จึงอาจมีการละเมิดกลไกงานย่อย6เล็กน้อยในบางครั้ง
class M1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performA();
// S starts after `step` returns and by contract must complete before `doB`
// begins. It is effectively sequential, inducing the sequence < A, S, B >.
tasks.enqueue(new S());
return this::doB;
}
private StateMachine doB(Tasks tasks) {
performB();
return DONE;
}
}
class M2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performX();
// Similarly, this induces the sequence < X, S, Y>.
tasks.enqueue(new S());
return this::doY;
}
private StateMachine doY(Tasks tasks) {
performY();
return DONE;
}
}
การฉีด runAfter
บางครั้งการละเมิด Tasks.enqueue
เป็นไปไม่ได้เนื่องจากมีงานย่อยอื่นๆ ที่ทำพร้อมกันอยู่หรือมีการเรียกใช้ Tasks.lookUp
ที่ต้องดำเนินการให้เสร็จสิ้นก่อน S จะทำงาน ในกรณีนี้ สามารถใช้การแทรกพารามิเตอร์ runAfter
ลงใน S เพื่อบอก S ว่าต้องทําอะไรต่อไป
class S implements StateMachine {
// Specifies what to run after S completes.
private final StateMachine runAfter;
@Override
public StateMachine step(Tasks tasks) {
… // Performs some computations.
return this::processResults;
}
@Nullable
private StateMachine processResults(Tasks tasks) {
… // Does some additional processing.
// Executes the state machine defined by `runAfter` after S completes.
return runAfter;
}
}
class M1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performA();
// Passes `this::doB` as the `runAfter` parameter of S, resulting in the
// sequence < A, S, B >.
return new S(/* runAfter= */ this::doB);
}
private StateMachine doB(Tasks tasks) {
performB();
return DONE;
}
}
class M2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performX();
// Passes `this::doY` as the `runAfter` parameter of S, resulting in the
// sequence < X, S, Y >.
return new S(/* runAfter= */ this::doY);
}
private StateMachine doY(Tasks tasks) {
performY();
return DONE;
}
}
วิธีการนี้ดูสะอาดตากว่าการละเมิดงานย่อย อย่างไรก็ตาม การใช้รูปแบบนี้มากเกินไป เช่น การทำ StateMachine
ซ้อนกันหลายรายการกับ runAfter
จะเป็นหนทางไปสู่ความยุ่งเหยิงของคอลแบ็ก คุณควรแบ่ง runAfter
ตามลำดับออกเป็นสถานะตามลำดับธรรมดาแทน
return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))
แทนที่ด้วยข้อความต่อไปนี้
private StateMachine step1(Tasks tasks) {
doStep1();
return new S(/* runAfter= */ this::intermediateStep);
}
private StateMachine intermediateStep(Tasks tasks) {
return new T(/* runAfter= */ this::nextStep);
}
ทางเลือกที่ไม่ได้รับอนุญาต: runAfterUnlessError
ในฉบับร่างก่อนหน้านี้ เราได้พิจารณา runAfterUnlessError
ที่จะล้มเลิกข้อผิดพลาดตั้งแต่เนิ่นๆ สาเหตุที่ทำให้เกิดการเปลี่ยนแปลงนี้มาจากข้อเท็จจริงที่ว่าข้อผิดพลาดมักได้รับการตรวจสอบ 2 ครั้ง โดยครั้งหนึ่งจาก StateMachine
ที่มีข้อมูลอ้างอิง runAfter
และอีกครั้งจากเครื่อง runAfter
เอง
หลังจากพิจารณาอย่างถี่ถ้วนแล้ว เราพบว่าความสม่ำเสมอของโค้ดสำคัญกว่าการกรองข้อมูลที่ซ้ำกันออกในการตรวจสอบข้อผิดพลาด การดำเนินการจะเกิดความสับสนหากกลไก runAfter
ไม่ทำงานอย่างสอดคล้องกันกับกลไก tasks.enqueue
ซึ่งต้องมีการตรวจสอบข้อผิดพลาดเสมอ
การมอบสิทธิ์โดยตรง
ทุกครั้งที่มีการเปลี่ยนสถานะอย่างเป็นทางการ ลูป Driver
หลักก็จะเลื่อนไปข้างหน้า
ตามสัญญา สถานะที่เพิ่มขึ้นหมายความว่าการค้นหา SkyValue และงานย่อยทั้งหมดที่อยู่ในคิวก่อนหน้านี้จะได้รับการแก้ไขก่อนที่สถานะถัดไปจะทำงาน บางครั้งตรรกะของ StateMachine
ที่รับมอบสิทธิ์ทําให้เฟสถัดไปไม่จําเป็นหรือทําให้เสียผล ตัวอย่างเช่น หาก step
แรกของผู้รับมอบสิทธิ์ทำการค้นหา SkyKey ที่ทำงานแบบขนานกับการค้นหาสถานะการมอบสิทธิ์ การเลื่อนขั้นระยะการทำงานจะทำให้การค้นหาดังกล่าวทำงานตามลำดับ การมอบสิทธิ์โดยตรงอาจเหมาะสมกว่า ดังที่แสดงในตัวอย่างด้านล่าง
class Parent implements StateMachine {
@Override
public StateMachine step(Tasks tasks ) {
tasks.lookUp(new Key1(), this);
// Directly delegates to `Delegate`.
//
// The (valid) alternative:
// return new Delegate(this::afterDelegation);
// would cause `Delegate.step` to execute after `step` completes which would
// cause lookups of `Key1` and `Key2` to be sequential instead of parallel.
return new Delegate(this::afterDelegation).step(tasks);
}
private StateMachine afterDelegation(Tasks tasks) {
…
}
}
class Delegate implements StateMachine {
private final StateMachine runAfter;
Delegate(StateMachine runAfter) {
this.runAfter = runAfter;
}
@Override
public StateMachine step(Tasks tasks) {
tasks.lookUp(new Key2(), this);
return …;
}
// Rest of implementation.
…
private StateMachine complete(Tasks tasks) {
…
return runAfter;
}
}
โฟลว์ข้อมูล
การสนทนาก่อนหน้านี้มุ่งเน้นที่การจัดการโฟลว์การควบคุม ส่วนนี้จะอธิบายการเผยแพร่ค่าข้อมูล
การใช้การเรียกกลับ Tasks.lookUp
ตัวอย่างการใช้งาน Tasks.lookUp
callback มีอยู่ใน SkyValue
lookups ส่วนนี้จะอธิบายเหตุผลและแนะนําแนวทางการจัดการ 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
ใช้การเรียกกลับโดยตรงจะช่วยประหยัดการจัดสรรหน่วยความจำสําหรับ Lamba
การจัดการข้อผิดพลาดจะให้รายละเอียดเพิ่มเติมเล็กน้อย แต่โดยทั่วไปแล้วจะไม่มีความแตกต่างกันมากนักระหว่างการเผยแพร่ข้อผิดพลาดและค่าปกติ
การใช้ 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>
callback สามารถแชร์ได้โดยไม่เกิดความสับสนเนื่องจากประเภทค่าแตกต่างกัน หากไม่เป็นเช่นนั้น คุณสามารถเปลี่ยนไปใช้การติดตั้งใช้งานแบบ 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
อย่างกระตือรือร้นตามที่การแจ้งข้อผิดพลาดกำหนด
ทางเลือกสำหรับ StateMachine
ระดับบนสุดมีอธิบายอยู่ในDriver
และการสร้างบริดจ์กับ SkyFunctions
การจัดการข้อผิดพลาด
มี 2-3 ตัวอย่างของการจัดการข้อผิดพลาดที่มีอยู่แล้วใน Tasks.lookUp
callback และการเผยแพร่ค่าระหว่าง
StateMachines
ระบบจะไม่แสดงข้อยกเว้นอื่นๆ นอกเหนือจาก InterruptedException
แต่จะใช้การส่งผ่านผ่าน callback เป็นค่าแทน การเรียกกลับดังกล่าวมักมีเอกสิทธิ์หรือความหมายเฉพาะ โดยมีค่าหรือข้อผิดพลาดที่ถูกส่งไปอย่างชัดเจน
ส่วนถัดไปจะอธิบายการโต้ตอบเล็กๆ น้อยๆ แต่สําคัญกับการจัดการข้อผิดพลาดของ Skyframe
การแจ้งข้อผิดพลาด (--nokeep_going)
ในระหว่างการบับเบิลข้อผิดพลาด ระบบอาจรีสตาร์ท SkyFunction แม้ว่า SkyValue ที่ขอจะใช้งานไม่ได้ทั้งหมด ในกรณีเช่นนี้ ระบบจะไม่เข้าสู่สถานะถัดไปเนื่องจากสัญญา Tasks
API อย่างไรก็ตาม StateMachine
ควรยังคงเผยแพร่ข้อยกเว้น
เนื่องจากต้องมีการนำไปใช้ไม่ว่าจะถึงสถานะถัดไปหรือไม่ แคล็กแบ็กการจัดการข้อผิดพลาดจึงต้องดำเนินการนี้ สําหรับ StateMachine
ภายใน
การดำเนินการนี้จะทำได้โดยการเรียกใช้การเรียกกลับของรายการหลัก
ที่ StateMachine
ระดับบนสุดซึ่งเชื่อมต่อกับ SkyFunction จะทำได้โดยเรียกใช้เมธอด setException
ของ ValueOrExceptionProducer
ValueOrExceptionProducer.tryProduceValue
จะแสดงข้อยกเว้นแม้ว่าจะไม่มี SkyValues ก็ตาม
หากใช้ Driver
โดยตรง คุณต้องตรวจหาข้อผิดพลาดที่เผยแพร่จาก SkyFunction แม้ว่าเครื่องจะประมวลผลไม่เสร็จสิ้นก็ตาม
การจัดการเหตุการณ์
สําหรับ SkyFunction ที่ต้องส่งเหตุการณ์ ระบบจะแทรก StoredEventHandler
ลงใน SkyKeyComputeState และแทรกลงใน StateMachine
ที่จําเป็นต้องใช้ ก่อนหน้านี้ต้องใช้ StoredEventHandler
เนื่องจาก Skyframe จะลดเหตุการณ์บางอย่าง เว้นแต่จะมีการเล่นซ้ำ แต่ปัญหานี้ได้รับการแก้ไขในภายหลัง
มีการรักษาการแทรก StoredEventHandler
ไว้เนื่องจากทำให้การใช้งานเหตุการณ์ที่เกิดจากการจัดการข้อผิดพลาด Callback ง่ายขึ้น
Driver
และบริดจ์กับ SkyFunctions
Driver
จะมีหน้าที่จัดการการเรียกใช้ StateMachine
โดยเริ่มจาก StateMachine
รูทที่ระบุ เนื่องจาก StateMachine
สามารถส่ง StateMachine
งานย่อยเข้าคิวซ้ำได้ Driver
รายการเดียวจึงจัดการงานย่อยได้หลายรายการ ภารกิจย่อยเหล่านี้จะสร้างโครงสร้างต้นไม้ ซึ่งเป็นผลมาจากการทำงานพร้อมกันแบบมีโครงสร้าง Driver
จะทำการค้นหา SkyValue แบบเป็นกลุ่มในภารกิจย่อยเพื่อเพิ่มประสิทธิภาพ
มีคลาสจำนวนมากที่สร้างขึ้นรอบๆ Driver
ด้วย API ต่อไปนี้
public final class Driver {
public Driver(StateMachine root);
public boolean drive(SkyFunction.Environment env) throws InterruptedException;
}
Driver
จะรับรูท StateMachine
รายการเดียวเป็นพารามิเตอร์ การเรียกใช้ Driver.drive
จะเรียกใช้ StateMachine
เท่าที่จะทำได้โดยไม่ต้องรีสตาร์ท Skyframe โดยจะแสดงผลเป็น "จริง" เมื่อ StateMachine
เสร็จสมบูรณ์และเป็นเท็จ ซึ่งหมายความว่าจะมีบางค่าเท่านั้นที่ใช้ได้
Driver
จะรักษาสถานะพร้อมกันของ StateMachine
และเหมาะสําหรับการฝังใน SkyKeyComputeState
กำลังสร้างอินสแตนซ์ Driver
โดยตรง
การติดตั้งใช้งาน StateMachine
มักสื่อสารผลลัพธ์ผ่าน Callback คุณสามารถสร้างอินสแตนซ์ Driver
โดยตรงได้ดังที่แสดงในตัวอย่างต่อไปนี้
Driver
ฝังอยู่ในการติดตั้งใช้งาน SkyKeyComputeState
พร้อมกับการใช้งาน ResultSink
ที่เกี่ยวข้องซึ่งจะต้องระบุเพิ่มเติมอีกเล็กน้อย ที่ระดับบนสุด ออบเจ็กต์ State
เป็นตัวรับที่เหมาะสมสำหรับผลลัพธ์ของการคํานวณ เนื่องจากมีการรับประกันว่าจะอยู่ได้นานกว่า Driver
class State implements SkyKeyComputeState, ResultProducer.ResultSink {
// The `Driver` instance, containing the full tree of all `StateMachine`
// states. Responsible for calling `StateMachine.step` implementations when
// asynchronous values are available and performing batched SkyFrame lookups.
//
// Non-null while `result` is being computed.
private Driver resultProducer;
// Variable for storing the result of the `StateMachine`
//
// Will be non-null after the computation completes.
//
private ResultType result;
// Implements `ResultProducer.ResultSink`.
//
// `ResultProducer` propagates its final value through a callback that is
// implemented here.
@Override
public void acceptResult(ResultType result) {
this.result = result;
}
}
โค้ดด้านล่างจะร่าง ResultProducer
class ResultProducer implements StateMachine {
interface ResultSink {
void acceptResult(ResultType value);
}
private final Parameters parameters;
private final ResultSink sink;
… // Other internal state.
ResultProducer(Parameters parameters, ResultSink sink) {
this.parameters = parameters;
this.sink = sink;
}
@Override
public StateMachine step(Tasks tasks) {
… // Implementation.
return this::complete;
}
private StateMachine complete(Tasks tasks) {
sink.acceptResult(getResult());
return DONE;
}
}
จากนั้นโค้ดสําหรับการคํานวณผลลัพธ์แบบเลื่อนเวลาอาจมีลักษณะดังนี้
@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 แบบซิงค์ก็ได้
คลาสนามธรรม ValueOrExceptionProducer
มีเมธอดต่อไปนี้
public abstract class ValueOrExceptionProducer<V, E extends Exception>
implements StateMachine {
@Nullable
public final V tryProduceValue(Environment env)
throws InterruptedException, E {
… // Implementation.
}
protected final void setValue(V value) { … // Implementation. }
protected final void setException(E exception) { … // Implementation. }
}
โดยมีอินสแตนซ์ Driver
แบบฝังและมีความคล้ายคลึงกับคลาส ResultProducer
ในไดรเวอร์การฝังและอินเทอร์เฟซกับ SkyFunction ในลักษณะที่คล้ายกัน การติดตั้งใช้งานจะเรียก setValue
หรือ setException
เมื่อเกิดเหตุการณ์ใดเหตุการณ์หนึ่งแทนการกำหนด ResultSink
เมื่อทั้ง 2 รายการเกิดขึ้น ข้อยกเว้นจะมีลำดับความสำคัญสูงกว่า เมธอด tryProduceValue
จะเชื่อมโยงโค้ด Callback แบบไม่พร้อมกันกับโค้ดแบบซิงโครนัสและยกเว้นเมื่อมีการตั้งค่า
ดังที่กล่าวไว้ก่อนหน้านี้ ในระหว่างการบับเบิลข้อผิดพลาด อาจเกิดข้อผิดพลาดขึ้นได้แม้ว่าเครื่องจะยังไม่เสร็จสิ้นเนื่องจากอินพุตไม่พร้อมใช้งานทั้งหมด tryProduceValue
จึงแสดงข้อยกเว้นที่ตั้งไว้แม้ว่าเครื่องจะยังไม่เสร็จสิ้น
สรุป: การนําการเรียกกลับออกในที่สุด
StateMachine
เป็นวิธีที่มีประสิทธิภาพมากแต่ต้นแบบในการดำเนินการคำนวณแบบไม่พร้อมกัน การดําเนินการต่อ (โดยเฉพาะในรูปแบบของ Runnable
ที่ส่งไปยัง ListenableFuture
) พบได้ทั่วไปในบางส่วนของโค้ด Bazel แต่ไม่ค่อยพบใน SkyFunction การวิเคราะห์ การวิเคราะห์มักผูกกับ CPU และไม่มี API แบบไม่พร้อมกันที่มีประสิทธิภาพสำหรับ I/O ของดิสก์ ท้ายที่สุดแล้ว คุณควรเพิ่มประสิทธิภาพการเรียกกลับ เนื่องจากมีเส้นโค้งการเรียนรู้และขัดขวางการอ่านได้
ทางเลือกหนึ่งที่ดูน่าเชื่อถือที่สุดคือ Java virtual thread ทุกอย่างจะแทนที่ด้วยการเรียกแบบซิงค์ที่บล็อกแทนการเขียนการเรียกกลับ ซึ่งเป็นไปได้เนื่องจากการจองทรัพยากรเธรดเสมือนนั้นควรจะใช้ทรัพยากรน้อย ต่างจากเธรดแพลตฟอร์ม อย่างไรก็ตาม แม้จะใช้เธรดเสมือน แต่การแทนที่การดำเนินการแบบซิงค์อย่างง่ายด้วยการสร้างเธรดและการดำเนินการพื้นฐานแบบซิงค์ก็ยังมีค่าใช้จ่ายสูงเกินไป เราได้ย้ายข้อมูลจากStateMachine
ไปไว้ที่Java virtual threads ซึ่งช้ากว่าหลายเท่าตัว ส่งผลให้เวลาในการตอบสนองของการวิเคราะห์จากต้นทางถึงปลายทางเพิ่มขึ้นเกือบ 3 เท่า เนื่องจากเธรดเสมือนยังคงเป็นฟีเจอร์เวอร์ชันตัวอย่าง จึงเป็นไปได้ว่าคุณจะทำการย้ายข้อมูลนี้ได้ในภายหลังเมื่อประสิทธิภาพดีขึ้น
อีกวิธีหนึ่งที่คุณอาจพิจารณาได้คือรอใช้ coroutine ของ Loom หากมีให้บริการ ข้อดีก็คือคุณสามารถลดค่าใช้จ่ายในการซิงค์ข้อมูลได้โดยใช้การทำงานหลายอย่างพร้อมกัน
หากไม่มีวิธีใดได้ผล การเขียนไบต์โค้ดระดับต่ำใหม่ก็อาจใช้ได้ผลดี หากเพิ่มประสิทธิภาพมากพอ คุณอาจได้รับประสิทธิภาพที่ใกล้เคียงกับโค้ดการเรียกกลับที่เขียนด้วยตนเอง
ภาคผนวก
Callback Hell
ปัญหา Callback Hell เป็นปัญหาที่มีชื่อเสียงในโค้ดแบบอะซิงโครนัสที่ใช้การเรียกกลับ ปัญหานี้เกิดจากความจริงที่ว่าการดำเนินการต่อสำหรับขั้นตอนถัดไปจะฝังอยู่ภายในขั้นตอนก่อนหน้า หากมีขั้นตอนจํานวนมาก การฝังนี้อาจมีความลึกมาก หากใช้ร่วมกับโฟลว์การควบคุม โค้ดจะจัดการไม่ได้
class CallbackHell implements StateMachine {
@Override
public StateMachine step(Tasks task) {
doA();
return (t, l) -> {
doB();
return (t1, l2) -> {
doC();
return DONE;
};
};
}
}
ข้อดีอย่างหนึ่งของการติดตั้งใช้งานที่ฝังอยู่คือสามารถเก็บเฟรมสแต็กของขั้นตอนภายนอกได้ ใน Java ตัวแปร Lambda ที่บันทึกไว้ต้องเป็นแบบสุดท้ายอย่างมีประสิทธิภาพ ดังนั้นการใช้ตัวแปรดังกล่าวจึงอาจยุ่งยาก หลีกเลี่ยงการฝังซ้อนหลายชั้นโดยการแสดงผลข้อมูลอ้างอิงเมธอดเป็นการดำเนินการต่อแทนการใช้ Lambda ดังที่แสดงต่อไปนี้
class CallbackHellAvoided implements StateMachine {
@Override
public StateMachine step(Tasks task) {
doA();
return this::step2;
}
private StateMachine step2(Tasks tasks) {
doB();
return this::step3;
}
private StateMachine step3(Tasks tasks) {
doC();
return DONE;
}
}
ข้อผิดพลาด Callback Hell อาจเกิดขึ้นหากมีการใช้รูปแบบการแทรก runAfter
มากเกินไป แต่สามารถหลีกเลี่ยงได้โดยการแทรกแบบแทรกสลับด้วยขั้นตอนตามลำดับ
ตัวอย่างการค้นหา SkyValue แบบเชน
บ่อยครั้งที่ตรรกะแอปพลิเคชันต้องใช้การค้นหา SkyValue แบบเชนแบบมีความสัมพันธ์ เช่น หาก SkyKey รายการที่ 2 ขึ้นอยู่กับ SkyValue รายการที่ 1 เมื่อพิจารณาอย่างคร่าวๆ การดำเนินการนี้จะส่งผลให้โครงสร้างการเรียกคืนที่ซับซ้อนและซ้อนกันหลายระดับ
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
อยู่ในลําดับการดําเนินการและการติดตั้งใช้งานการเรียกกลับตามหลังทันทีที่ส่งในโค้ด เพื่อปรับปรุงความสามารถในการอ่าน ซึ่งอาจไม่สามารถทำได้เสมอไปเมื่อการไหลของการควบคุมแตกแขนง ความคิดเห็นเพิ่มเติมอาจมีประโยชน์ในกรณีเช่นนี้
ในตัวอย่าง: การค้นหา Chained SkyValue ระบบจะสร้างการอ้างอิงเมธอดขั้นกลางเพื่อให้บรรลุเป้าหมายนี้ ซึ่งจะแลกประสิทธิภาพเพียงเล็กน้อยเพื่อความสามารถในการอ่าน ซึ่งน่าจะคุ้มค่าในกรณีนี้
สมมติฐานรุ่น
ออบเจ็กต์ Java ที่มีอายุการใช้งานปานกลางจะขัดแย้งกับสมมติฐานรุ่นของโปรแกรมรวบรวมขยะ Java ซึ่งออกแบบมาเพื่อจัดการออบเจ็กต์ที่มีอายุการใช้งานสั้นมากหรือออบเจ็กต์ที่มีอายุการใช้งานตลอดไป ตามคำจำกัดความแล้ว ออบเจ็กต์ใน SkyKeyComputeState
ละเมิดสมมติฐานนี้ ออบเจ็กต์ดังกล่าวซึ่งมีต้นไม้ที่สร้างขึ้นจาก StateMachine
ทั้งหมดที่ยังคงทํางานอยู่โดยรูทที่ Driver
จะมีอายุการใช้งานระดับกลางขณะที่หยุดชั่วคราวเพื่อรอการประมวลผลแบบไม่สอดคล้องกันให้เสร็จสมบูรณ์
ดูเหมือนว่า JDK19 จะทำงานได้ดีขึ้น แต่บางครั้งเมื่อใช้ StateMachine
ก็อาจสังเกตได้ว่าเวลา GC เพิ่มขึ้น แม้ว่าจะมีขยะจริงที่สร้างขึ้นลดลงอย่างมาก เนื่องจาก StateMachine
มีช่วงอายุกลางๆ ระบบจึงอาจเลื่อนระดับ StateMachine
ไปเป็นรุ่นเก่า ซึ่งจะทำให้พื้นที่เก็บข้อมูลเต็มเร็วขึ้น จึงจำเป็นต้องใช้ GC หลักหรือ GC แบบเต็มที่มีราคาแพงกว่าในการล้างข้อมูล
ข้อควรระวังเบื้องต้นคือการลดการใช้ตัวแปร StateMachine
แต่บางสถานะก็ทำไม่ได้เสมอไป เช่น หากคุณต้องใช้ค่าในหลายสถานะ หากเป็นไปได้ ตัวแปรสแต็ก step
ในเครื่องเป็นตัวแปรการสร้างอายุน้อยและเป็น GC ที่มีประสิทธิภาพ
สําหรับตัวแปร StateMachine
การแบ่งงานออกเป็นงานย่อยและทําตามรูปแบบที่แนะนําสําหรับการเผยแพร่ค่าระหว่าง StateMachine
นั้นมีประโยชน์เช่นกัน โปรดทราบว่าเมื่อใช้รูปแบบนี้ จะมีเพียง StateMachine
ย่อยเท่านั้นที่อ้างอิงถึง StateMachine
หลัก แต่ StateMachine
หลักจะไม่อ้างอิงถึง StateMachine
ย่อย ซึ่งหมายความว่าเมื่อบุตรหลานดำเนินการเสร็จสิ้นและอัปเดตผู้ปกครองโดยใช้การเรียกกลับผลลัพธ์ บุตรหลานจะออกจากขอบเขตโดยอัตโนมัติและมีสิทธิ์ได้รับ GC
สุดท้าย ในบางกรณี คุณต้องใช้ตัวแปร StateMachine
ในสถานะก่อนหน้า แต่ไม่ต้องใช้ในสถานะถัดไป การเลิกใช้การอ้างอิงวัตถุขนาดใหญ่อาจมีประโยชน์เมื่อทราบว่าไม่จำเป็นต้องใช้อีกต่อไปแล้ว
การตั้งชื่อสถานะ
เมื่อตั้งชื่อเมธอด คุณมักจะตั้งชื่อเมธอดสำหรับลักษณะการทำงานภายในเมธอดนั้นได้ วิธีดำเนินการในStateMachine
นั้นไม่ชัดเจนนักเนื่องจากไม่มีกอง ตัวอย่างเช่น สมมติว่าเมธอด foo
เรียกเมธอดย่อย bar
ใน StateMachine
การดำเนินการนี้อาจแปลเป็นลำดับสถานะ foo
ตามด้วย bar
foo
ไม่ได้รวมพฤติกรรม
bar
แล้ว ด้วยเหตุนี้ ชื่อวิธีการสำหรับรัฐต่างๆ จึงมีแนวโน้มที่จะมีขอบเขตแคบลง และอาจสะท้อนให้เห็นถึงพฤติกรรมในท้องถิ่น
แผนภูมิต้นไม้การเกิดขึ้นพร้อมกัน
ต่อไปนี้เป็นมุมมองอื่นของแผนภาพในการทำงานพร้อมกันแบบมีโครงสร้างซึ่งแสดงโครงสร้างต้นไม้ได้ดีขึ้น บล็อกต่างๆ ประกอบกันเป็นต้นไม้เล็กๆ
-
ต่างจากรูปแบบที่ Skyframe จะรีสตาร์ทตั้งแต่ต้นเมื่อไม่มีค่า ↩
-
โปรดทราบว่า
step
ได้รับอนุญาตให้ส่งInterruptedException
แต่ตัวอย่างจะข้ามรายการนี้ มีเมธอดระดับต่ำ 2-3 รายการในโค้ด Bazel ที่ทำให้เกิดข้อยกเว้นนี้ และจะเผยแพร่ถึงDriver
ซึ่งจะอธิบายในภายหลัง ซึ่งจะเรียกใช้StateMachine
คุณไม่จำเป็นต้องประกาศว่าจะมีการยกเว้นข้อยกเว้นเมื่อไม่จำเป็น ↩ -
งานย่อยที่ทำพร้อมกันได้รับแรงจูงใจจาก
ConfiguredTargetFunction
ซึ่งทำงานอิสระสำหรับทรัพยากร Dependency แต่ละอย่าง แทนที่จะจัดการโครงสร้างข้อมูลที่ซับซ้อนซึ่งประมวลผลทรัพยากร Dependency ทั้งหมดในคราวเดียว ทำให้เกิดความด้อยประสิทธิภาพ ทรัพยากร Dependency แต่ละรายการจะมีStateMachine
เป็นของตนเอง ↩ -
การเรียก
tasks.lookUp
หลายรายการภายในขั้นตอนเดียวจะรวมกันเป็นกลุ่ม คุณสามารถสร้างการแบ่งกลุ่มเพิ่มเติมได้โดยการค้นหาที่เกิดขึ้นภายในงานย่อยที่ทำงานพร้อมกัน ↩ -
แนวคิดนี้คล้ายกับการทำงานพร้อมกันแบบมีโครงสร้างของ Java jeps/428 ↩
-
การดำเนินการนี้จะคล้ายกับการสร้างชุดข้อความและการรวมชุดข้อความเข้าด้วยกันเพื่อสร้างองค์ประกอบตามลำดับ ↩