ภาพรวม
Skyframe StateMachine
คือออบเจ็กต์ฟังก์ชันที่แยกส่วนซึ่งอยู่ในฮีป โดยรองรับการประเมินที่ยืดหยุ่นและไม่มีการซ้ำซ้อน1 เมื่อค่าที่จำเป็นไม่พร้อมใช้งานในทันที แต่จะมีการคำนวณแบบไม่พร้อมกัน StateMachine
ไม่สามารถผูกทรัพยากรเธรดขณะรอได้ แต่ต้อง
ถูกระงับและกลับมาทำงานต่อแทน การแยกส่วนจึงเผยให้เห็นจุดกลับเข้าที่ชัดเจน
เพื่อให้ข้ามการคำนวณก่อนหน้าได้
StateMachine
ใช้เพื่อแสดงลำดับ การแยกสาขา ความพร้อมกันเชิงตรรกะที่มีโครงสร้าง และปรับแต่งมาเพื่อการโต้ตอบกับ Skyframe โดยเฉพาะ
StateMachine
สามารถประกอบเป็น StateMachine
ที่ใหญ่ขึ้นและแชร์ StateMachine
ย่อยได้ การทำงานพร้อมกันจะเป็นแบบลำดับชั้นเสมอโดยโครงสร้างและ
เป็นแบบตรรกะล้วนๆ งานย่อยที่ทำงานพร้อมกันทั้งหมดจะทำงานในเธรด SkyFunction ของงานหลักที่แชร์รายการเดียว
บทนำ
ส่วนนี้จะอธิบายและแนะนำ StateMachine
s ที่พบในแพ็กเกจ
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 จะรวมกันเป็นแนวคิดเดียว
อย่างไรก็ตาม StateMachine
s ยังคงแยกความแตกต่างระหว่างทั้ง 2 อย่างนี้
- Callback - อธิบายตำแหน่งที่จะจัดเก็บผลลัพธ์ของการคำนวณแบบอะซิงโครนัส
- การดำเนินการต่อ - ระบุสถานะการดำเนินการถัดไป
ต้องใช้การเรียกกลับเมื่อเรียกใช้การดำเนินการแบบไม่พร้อมกัน ซึ่งหมายความว่าการดำเนินการจริงจะไม่เกิดขึ้นทันทีเมื่อเรียกใช้เมธอด เช่น ในกรณีของการค้นหา SkyValue ควรทำให้การเรียกกลับเรียบง่ายที่สุด
Continuations คือStateMachine
ค่าที่ส่งคืนของ StateMachine
s และ
ห่อหุ้มการดำเนินการที่ซับซ้อนซึ่งจะเกิดขึ้นเมื่อการคำนวณแบบอะซิงโครนัสทั้งหมด
ได้รับการแก้ไข แนวทางที่มีโครงสร้างนี้ช่วยให้จัดการความซับซ้อนของ
การเรียกกลับได้
งาน
Tasks
อินเทอร์เฟซมี API สำหรับ StateMachine
s เพื่อค้นหา 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
โปรเซสเซอร์ (Driver
s และการเชื่อมต่อกับ
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
s และทำทุกอย่างที่ StateMachine
s ปกติทำได้ รวมถึงการสร้างงานย่อยเพิ่มเติมแบบเรียกซ้ำหรือการค้นหา 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 หลังจากที่ดำเนินการเสร็จสมบูรณ์แล้ว และ StateMachine
s ไม่ได้เก็บสแต็กการเรียกใช้ ส่วนนี้จะทบทวนเทคนิคบางอย่างในการบรรลุเป้าหมายนี้
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
จะทิ้งข้อยกเว้นที่ตั้งค่าไว้ แม้ว่าเครื่องจะยังทำงานไม่เสร็จก็ตาม
บทส่งท้าย: การนำการโทรกลับออกในที่สุด
StateMachine
s เป็นวิธีที่มีประสิทธิภาพสูงแต่ต้องใช้โค้ดสำเร็จรูปจำนวนมากในการดำเนินการ
การคำนวณแบบอะซิงโครนัส การดำเนินการต่อ (โดยเฉพาะในรูปแบบของ Runnable
s
ที่ส่งไปยัง ListenableFuture
) มีอยู่ทั่วไปในบางส่วนของโค้ด Bazel
แต่ไม่พบใน SkyFunctions ของการวิเคราะห์ การวิเคราะห์ส่วนใหญ่ขึ้นอยู่กับ CPU และไม่มี API แบบอะซิงโครนัสที่มีประสิทธิภาพสำหรับ I/O ของดิสก์ ในที่สุด การเพิ่มประสิทธิภาพเพื่อลดการเรียกกลับก็จะเป็นประโยชน์ เนื่องจากมีการเรียนรู้ที่ซับซ้อนและทำให้ความสามารถในการอ่านลดลง
เธรดเสมือนของ Java เป็นหนึ่งในทางเลือกที่มีแนวโน้มมากที่สุด แทนที่จะต้องเขียนการเรียกกลับ ทุกอย่างจะถูกแทนที่ด้วยการเรียกแบบซิงโครนัสที่บล็อก
การดำเนินการนี้เป็นไปได้เนื่องจากการเชื่อมโยงทรัพยากรของเธรดเสมือนควรมีค่าใช้จ่ายต่ำ ซึ่งแตกต่างจากเธรดของแพลตฟอร์ม อย่างไรก็ตาม แม้จะมีเธรดเสมือน การแทนที่การดำเนินการแบบซิงโครนัสอย่างง่ายด้วยการสร้างเธรดและการซิงโครไนซ์
ดั้งเดิมก็มีค่าใช้จ่ายสูงเกินไป เราได้ทำการย้ายข้อมูลจาก StateMachine
s ไปยังเธรดเสมือนของ 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 ที่แสดงโครงสร้างแบบต้นไม้ได้ดีกว่า บล็อกจะประกอบกันเป็นต้นไม้เล็กๆ
-
ซึ่งแตกต่างจากธรรมเนียมของ Skyframe ที่จะรีสตาร์ทจากจุดเริ่มต้นเมื่อไม่มีค่า ↩
-
โปรดทราบว่า
step
ได้รับอนุญาตให้ส่งInterruptedException
แต่ตัวอย่าง จะละเว้นส่วนนี้ มีเมธอดระดับล่างบางอย่างในโค้ด Bazel ที่ทำให้เกิดข้อยกเว้นนี้ และข้อยกเว้นจะส่งต่อไปยังDriver
ซึ่งจะอธิบายในภายหลัง ที่เรียกใช้StateMachine
ไม่จำเป็นต้องประกาศว่ามีการขว้างเมื่อไม่จำเป็น ↩ -
งานย่อยที่ทำงานพร้อมกันมีแรงจูงใจมาจาก
ConfiguredTargetFunction
ซึ่ง ทำงานแยกกันสำหรับแต่ละการอ้างอิง แทนที่จะจัดการ โครงสร้างข้อมูลที่ซับซ้อนซึ่งประมวลผลการอ้างอิงทั้งหมดพร้อมกัน ซึ่งทำให้เกิดความไม่มีประสิทธิภาพ การอ้างอิงแต่ละรายการจะมีStateMachine
ของตัวเองโดยอิสระ ↩ -
ระบบจะจัดกลุ่มการเรียก
tasks.lookUp
หลายครั้งภายในขั้นตอนเดียวเข้าด้วยกัน การจัดกลุ่มเพิ่มเติมสามารถสร้างได้โดยการค้นหาที่เกิดขึ้นภายใน งานย่อยที่พร้อมกัน ↩ -
ซึ่งมีแนวคิดคล้ายกับ Structured Concurrency ของ Java jeps/428 ↩
-
การทำเช่นนี้คล้ายกับการสร้างเธรดและเข้าร่วมเธรดเพื่อสร้าง องค์ประกอบตามลำดับ ↩