Tổng quan
Skyframe StateMachine
là một đối tượng hàm được huỷ cấu trúc nằm trên vùng nhớ khối xếp. Công cụ này hỗ trợ tính linh hoạt và đánh giá mà không cần dự phòng1 khi các giá trị bắt buộc không có sẵn ngay nhưng được tính không đồng bộ. StateMachine
không thể liên kết tài nguyên chuỗi trong khi chờ, mà phải tạm ngưng và tiếp tục. Do đó, cấu trúc giải phóng sẽ hiển thị các điểm truy cập lại rõ ràng để có thể bỏ qua các phép tính trước.
Bạn có thể sử dụng StateMachine
để thể hiện trình tự, phân nhánh, đồng thời logic có cấu trúc và được điều chỉnh riêng cho hoạt động tương tác với Skyframe. StateMachine
có thể được kết hợp thành các StateMachine
lớn hơn và dùng chung các StateMachine
phụ. Tính năng đồng thời luôn có tính phân cấp theo cấu trúc và hoàn toàn hợp lý. Mọi việc phụ cần làm đồng thời sẽ chạy trong một chuỗi chính được chia sẻ của Skyky.
Giới thiệu
Phần này vận động và giới thiệu ngắn gọn về StateMachine
, có trong gói java.com.google.devtools.build.skyframe.state
.
Giới thiệu ngắn gọn về việc khởi động lại Skyframe
Skyframe là khung thực hiện đánh giá song song các biểu đồ phần phụ thuộc.
Mỗi nút trong biểu đồ tương ứng với kết quả đánh giá một SkyFunction bằng một SkyKey chỉ định các tham số và SkyValue chỉ định kết quả của nó. Mô hình tính toán sao cho SkyFunction có thể tra cứu SkyValues bằng SkyKey,
kích hoạt đánh giá đệ quy, song song các SkyFunctions bổ sung. Thay vì chặn, điều này sẽ liên kết một luồng khi một SkyValue được yêu cầu chưa sẵn sàng vì một số đồ thị con của phép tính chưa hoàn tất, thì SkyFunction yêu cầu sẽ quan sát phản hồi null
getValue
và sẽ trả về null
thay vì SkyValue, báo hiệu rằng nó chưa hoàn thành do thiếu dữ liệu đầu vào.
Skyframe khởi động lại SkyFunctions khi tất cả SkyValues được yêu cầu trước đó
có sẵn.
Trước khi giới thiệu SkyKeyComputeState
, cách truyền thống để xử lý
khởi động lại là chạy lại toàn bộ quá trình tính toán. Mặc dù điều này có độ phức tạp bậc hai, nhưng các hàm được viết theo cách này cuối cùng sẽ hoàn tất vì mỗi lần chạy lại, một số lần tra cứu sẽ trả về null
. Với SkyKeyComputeState
, bạn có thể liên kết dữ liệu điểm kiểm tra chỉ định bằng tay với SkyFunction, giúp lưu lại quá trình tính toán lại đáng kể.
StateMachine
là các đối tượng tồn tại bên trong SkyKeyComputeState
và loại bỏ gần như toàn bộ quá trình tính toán lại khi SkyFunction khởi động lại (giả sử rằng SkyKeyComputeState
không thoát khỏi bộ nhớ đệm) bằng cách hiển thị các hook tạm ngưng và tiếp tục thực thi.
Các phép tính có trạng thái bên trong SkyKeyComputeState
Từ quan điểm thiết kế hướng đối tượng, bạn nên xem xét việc lưu trữ đối tượng tính toán bên trong SkyKeyComputeState
thay vì giá trị dữ liệu thuần túy.
Trong Java, mô tả tối thiểu về một hành vi mang đối tượng là giao diện chức năng và hóa ra là đủ. StateMachine
có
định nghĩa đệ quy, một cách tò mò2 sau đây.
@FunctionalInterface
public interface StateMachine {
StateMachine step(Tasks tasks) throws InterruptedException;
}
Giao diện Tasks
tương tự như SkyFunction.Environment
nhưng được thiết kế cho chế độ không đồng bộ và hỗ trợ thêm cho các tác vụ phụ đồng thời về mặt logic3.
Giá trị trả về của step
là một StateMachine
khác, cho phép xác định trình tự các bước, theo quy nạp. step
trả về DONE
khi StateMachine
hoàn tất. Ví dụ:
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;
}
}
mô tả StateMachine
với kết quả sau.
hello
world
Lưu ý tham chiếu phương thức this::step2
cũng là StateMachine
do
step2
đáp ứng định nghĩa giao diện chức năng của StateMachine
. Tham chiếu phương thức là cách phổ biến nhất để chỉ định trạng thái tiếp theo trong StateMachine
.
Theo trực giác, việc chia nhỏ phép tính thành các bước StateMachine
thay vì một hàm nguyên khối sẽ cung cấp các hook cần thiết để tạm ngưng và tiếp tục tính toán. Khi StateMachine.step
trả về, sẽ có một điểm tạm ngưng rõ ràng. Giá trị tiếp tục được chỉ định bởi giá trị StateMachine
được trả về là một điểm tiếp tục rõ ràng. Do đó, bạn có thể tránh việc tính toán lại vì có thể tiếp tục tính toán chính xác nơi đã dừng lại.
Lệnh gọi lại, hàm tiếp tục và phép tính không đồng bộ
Về mặt kỹ thuật, StateMachine
đóng vai trò là giá trị tiếp tục, giúp xác định việc tính toán tiếp theo sẽ được thực thi. Thay vì chặn, StateMachine
có thể tự nguyện tạm ngưng bằng cách quay lại từ hàm step
. Hàm này sẽ chuyển quyền kiểm soát về lại một thực thể Driver
. Sau đó, Driver
có thể
chuyển sang StateMachine
sẵn sàng hoặc từ bỏ quyền kiểm soát để quay lại Skyframe.
Theo truyền thống, lệnh gọi lại và phần tiếp tục được kết hợp thành một khái niệm.
Tuy nhiên, StateMachine
vẫn duy trì sự khác biệt giữa hai loại mã này.
- Lệnh gọi lại – mô tả nơi lưu trữ kết quả của phép tính không đồng bộ.
- Tiếp tục – chỉ định trạng thái thực thi tiếp theo.
Bạn phải gọi lại khi gọi một thao tác không đồng bộ, có nghĩa là thao tác thực tế sẽ không diễn ra ngay khi gọi phương thức, như trong trường hợp tra cứu SkyValue. Cuộc gọi lại nên được giữ đơn giản nhất có thể.
Tiếp tục là các giá trị trả về StateMachine
của StateMachine
và đóng gói cách thực thi phức tạp sau khi tất cả các phép tính không đồng bộ được phân giải. Phương pháp tiếp cận có cấu trúc này giúp duy trì tính phức tạp của lệnh gọi lại có thể quản lý.
Tasks
Giao diện Tasks
cung cấp cho StateMachine
một API để tra cứu SkyValues bằng SkyKey và lên lịch các nhiệm vụ phụ đồng thời.
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.
}
Tra cứu SkyValue
StateMachine
sử dụng các phương thức nạp chồng Tasks.lookUp
để tìm SkyValues. Các lớp này tương tự như SkyFunction.Environment.getValue
và SkyFunction.Environment.getValueOrThrow
, đồng thời có các ngữ nghĩa xử lý ngoại lệ tương tự. Cách triển khai không thực hiện ngay hoạt động tra cứu mà thay vào đó, sẽ xử lý hàng loạt4 lượt tìm kiếm nhiều nhất có thể. Giá trị này có thể không có sẵn ngay lập tức, ví dụ: cần khởi động lại Skyframe, vì vậy, phương thức gọi sẽ chỉ định việc cần làm với giá trị thu được.
Bộ xử lý StateMachine
(Driver
và cầu nối sang SkyFrame) đảm bảo rằng giá trị đó có sẵn trước khi trạng thái tiếp theo bắt đầu. Sau đây là một ví dụ.
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;
}
}
Trong ví dụ trên, bước đầu tiên là tra cứu new Key()
, chuyển this
làm thực thể tiêu thụ. Điều này có thể xảy ra vì DoesLookup
triển khai Consumer<SkyValue>
.
Theo hợp đồng, trước khi trạng thái DoesLookup.processValue
tiếp theo bắt đầu, tất cả các lượt
tra cứu của DoesLookup.step
đã hoàn tất. Do đó, bạn có thể sử dụng value
khi truy cập processValue
.
Việc phụ cần làm
Tasks.enqueue
yêu cầu thực thi các tác vụ phụ đồng thời về mặt logic.
Việc phụ cần làm cũng là StateMachine
và có thể làm bất cứ việc gì StateMachine
thông thường có thể làm, bao gồm cả việc tạo thêm việc phụ cần làm hoặc tra cứu SkyValues.
Giống như lookUp
, trình điều khiển máy trạng thái đảm bảo rằng tất cả các tác vụ phụ đã hoàn tất trước khi chuyển sang bước tiếp theo. Sau đây là một ví dụ.
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.
}
}
}
Mặc dù Subtask1
và Subtask2
đồng thời về mặt logic, nhưng mọi thứ chạy trong một luồng duy nhất nên bản cập nhật "đồng thời" của i
không cần đồng bộ hoá.
Mô hình đồng thời có cấu trúc
Vì mọi lookUp
và enqueue
đều phải phân giải trước khi chuyển sang trạng thái tiếp theo, điều này có nghĩa là cơ chế xử lý đồng thời vốn chỉ giới hạn ở cấu trúc cây. Bạn có thể tạo mô hình đồng thời5 phân cấp như trong ví dụ sau.
Từ UML, cấu trúc đồng thời tạo thành một cây rất khó để nói rõ. Có một chế độ xem thay thế hiển thị rõ hơn cấu trúc cây.
Tính năng đồng thời có cấu trúc dễ hiểu hơn nhiều.
Thành phần và các mẫu luồng điều khiển
Phần này trình bày các ví dụ về cách soạn nhiều StateMachine
và các giải pháp cho một số vấn đề về luồng điều khiển.
Trạng thái tuần tự
Đây là mẫu luồng điều khiển phổ biến và đơn giản nhất. Ví dụ về vấn đề này được trình bày trong bài viết Các phép tính trạng thái bên trong SkyKeyComputeState
.
Phân nhánh
Bạn có thể đạt được các trạng thái phân nhánh trong StateMachine
bằng cách trả về các giá trị khác nhau thông qua luồng điều khiển Java thông thường, như trong ví dụ sau.
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;
}
…
}
Một số nhánh nhất định thường trả về DONE
để hoàn tất sớm.
Bố cục tuần tự nâng cao
Vì cấu trúc điều khiển StateMachine
không cần bộ nhớ, nên việc chia sẻ định nghĩa StateMachine
làm nhiệm vụ con đôi khi có thể gây khó khăn. Gọi M1 và M2 là các thực thể StateMachine
dùng chung StateMachine
, S, với M1 và M2 lần lượt là các trình tự <A, S, B> và <X, S, Y>. Vấn đề là S không biết nên tiếp tục với B hay Y sau khi hoàn tất và StateMachine
không hoàn toàn giữ lại ngăn xếp lệnh gọi. Phần này sẽ xem xét một số kỹ thuật để đạt được điều này.
StateMachine
làm phần tử trình tự đầu cuối
Cách này không giải quyết được sự cố ban đầu gây ra. Nó chỉ thể hiện cấu trúc tuần tự khi StateMachine
dùng chung là thiết bị đầu cuối trong trình tự.
// 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();
}
}
Tính năng này hoạt động ngay cả khi S là một máy trạng thái phức tạp.
Tác vụ phụ cho cấu trúc tuần tự
Vì các tác vụ phụ trong hàng đợi được đảm bảo hoàn tất trước trạng thái tiếp theo, nên đôi khi có thể lạm dụng một chút6 cơ chế tác vụ phụ.
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
lần tiêm
Đôi khi, việc lạm dụng Tasks.enqueue
là không thể vì có các tác vụ phụ khác hoặc lệnh gọi Tasks.lookUp
phải được hoàn thành trước khi thực thi S. Trong trường hợp này, việc chèn tham số runAfter
vào S có thể được dùng để thông báo cho S việc cần làm tiếp theo.
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;
}
}
Phương pháp này rõ ràng hơn là lợi dụng việc phụ cần làm. Tuy nhiên, việc áp dụng tuỳ chọn này cũng rất tự do, ví dụ: bằng cách lồng nhiều StateMachine
với runAfter
, là con đường dẫn đến Callback Hell. Thay vào đó, bạn nên chia nhỏ các runAfter
tuần tự với các trạng thái tuần tự thông thường.
return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))
có thể được thay thế bằng nội dung sau.
private StateMachine step1(Tasks tasks) {
doStep1();
return new S(/* runAfter= */ this::intermediateStep);
}
private StateMachine intermediateStep(Tasks tasks) {
return new T(/* runAfter= */ this::nextStep);
}
Lựa chọn thay thế cho Cấm: runAfterUnlessError
Trong một bản nháp trước đó, chúng tôi đã xem xét một runAfterUnlessError
có thể huỷ bỏ
lỗi sớm. Nguyên nhân là do các lỗi thường bị kiểm tra hai lần, một lần bởi StateMachine
có tham chiếu runAfter
và một lần bởi chính máy runAfter
.
Sau một số lần cân nhắc, chúng tôi quyết định rằng tính đồng nhất của mã quan trọng hơn việc sao chép kiểm tra lỗi. Sẽ rất khó hiểu nếu cơ chế runAfter
không hoạt động nhất quán với cơ chế tasks.enqueue
. Cơ chế này luôn yêu cầu kiểm tra lỗi.
Ủy quyền trực tiếp
Mỗi khi có một quá trình chuyển đổi trạng thái chính thức, vòng lặp Driver
chính sẽ diễn ra.
Theo hợp đồng, trạng thái tiến có nghĩa là tất cả hoạt động tra cứu và công việc phụ trên SkyValue đã được đưa vào hàng đợi trước đó sẽ phân giải trước khi trạng thái tiếp theo thực thi. Đôi khi, logic của một thực thể đại diện StateMachine
khiến cho một giai đoạn tiến trình không cần thiết hoặc
phản tác dụng. Ví dụ: nếu step
đầu tiên của đại biểu thực hiện
tra cứu SkyKey có thể song song với việc tra cứu trạng thái ủy quyền, thì việc chuyển tiếp giai đoạn sẽ làm cho các lượt tìm kiếm đó tuần tự. Có thể hợp lý hơn khi thực hiện ủy quyền trực tiếp, như được hiển thị trong ví dụ bên dưới.
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;
}
}
Luồng dữ liệu
Trọng tâm của cuộc thảo luận trước đó là quản lý luồng kiểm soát. Phần này mô tả sự phổ biến của các giá trị dữ liệu.
Triển khai các lệnh gọi lại Tasks.lookUp
Dưới đây là ví dụ về cách triển khai lệnh gọi lại Tasks.lookUp
trong lệnh kiểm tra
SkyValue. Phần này đưa ra lý do và đề xuất cách tiếp cận để xử lý nhiều SkyValues.
Tasks.lookUp
lệnh gọi lại
Phương thức Tasks.lookUp
nhận một lệnh gọi lại sink
làm tham số.
void lookUp(SkyKey key, Consumer<SkyValue> sink);
Bạn có thể sử dụng hàm lambda Java để triển khai phương pháp này:
tasks.lookUp(key, value -> myValue = (MyValueClass)value);
với myValue
là biến thành viên của phiên bản StateMachine
đang thực hiện
tra cứu. Tuy nhiên, lambda yêu cầu phân bổ bộ nhớ bổ sung so với việc triển khai giao diện Consumer<SkyValue>
trong quá trình triển khai StateMachine
. Hàm lambda vẫn hữu ích khi có nhiều thông tin tra cứu không rõ ràng.
Cũng có lỗi xử lý quá tải Tasks.lookUp
, tương tự như 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);
}
Dưới đây là một ví dụ về cách triển khai.
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.
…
}
}
Giống như việc tra cứu mà không xử lý lỗi, việc để lớp StateMachine
trực tiếp
triển khai lệnh gọi lại sẽ lưu mức phân bổ bộ nhớ cho lamba.
Xử lý lỗi cung cấp thông tin chi tiết hơn một chút, nhưng về cơ bản, không có nhiều khác biệt giữa việc truyền lỗi và giá trị bình thường.
Sử dụng nhiều SkyValue
Bạn thường phải tra cứu nhiều giá trị SkyValue. Một cách tiếp cận hiệu quả trong phần lớn thời gian là bật loại SkyValue. Sau đây là ví dụ đã được đơn giản hoá từ mã sản xuất nguyên mẫu.
@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);
}
Bạn có thể chia sẻ quá trình triển khai lệnh gọi lại Consumer<SkyValue>
một cách rõ ràng vì các loại giá trị là khác nhau. Nếu không phải như vậy, việc quay lại sử dụng phương thức triển khai dựa trên lambda hoặc đầy đủ các thực thể lớp bên trong sẽ triển khai các lệnh gọi lại phù hợp là khả thi.
Tuyên truyền các giá trị giữa StateMachine
Cho đến nay, tài liệu này chỉ giải thích cách sắp xếp công việc phụ, nhưng các nhiệm vụ phụ cũng cần báo cáo lại một giá trị cho phương thức gọi. Vì các việc phụ cần làm là không đồng bộ về mặt logic, nên kết quả của các việc phụ cần làm đó sẽ được thông báo lại cho phương thức gọi bằng cách sử dụng lệnh gọi lại. Để công việc này hoạt động, nhiệm vụ con xác định giao diện bồn lưu trữ dữ liệu được đưa vào thông qua hàm khởi tạo.
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;
}
}
Sau đó, phương thức gọi StateMachine
sẽ có dạng như sau.
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;
}
}
Ví dụ trên minh họa một vài điều. Caller
phải truyền lại kết quả và xác định Caller.ResultSink
của riêng mình. Caller
triển khai các lệnh gọi lại BarProducer.ResultSink
. Khi tiếp tục, processResult
sẽ kiểm tra xem
value
có rỗng hay không để xác định xem đã xảy ra lỗi hay chưa. Đây là dạng hành vi phổ biến sau khi chấp nhận kết quả đầu ra của việc phụ cần làm hoặc tra cứu SkyValue.
Xin lưu ý rằng việc triển khai acceptBarError
một cách háo hức chuyển tiếp kết quả đến Caller.ResultSink
, theo yêu cầu bằng cách Lỗi sủi bọt.
Các lựa chọn thay thế cho StateMachine
cấp cao nhất được mô tả trong Driver
và bắc cầu lên SkyFunctions.
Xử lý lỗi
Đã có một vài ví dụ về cách xử lý lỗi trong lệnh gọi lại
Tasks.lookUp
và Truyền giá trị giữa
StateMachines
. Ngoại lệ, ngoại trừ InterruptedException
, sẽ không được gửi mà thay vào đó sẽ được chuyển qua các lệnh gọi lại dưới dạng giá trị. Những lệnh gọi lại như vậy thường có ngữ nghĩa độc quyền hoặc ngữ nghĩa, trong đó chỉ có đúng một giá trị hoặc lỗi được truyền.
Phần tiếp theo mô tả một tương tác tinh tế nhưng quan trọng với việc xử lý lỗi Skyframe.
Lỗi sủi bọt (--nokeep_going)
Trong quá trình tạo lỗi, một SkyFunction có thể được khởi động lại ngay cả khi không phải tất cả SkyValues yêu cầu đều có sẵn. Trong những trường hợp như vậy, trạng thái tiếp theo sẽ không bao giờ
đạt đến do hợp đồng API Tasks
. Tuy nhiên, StateMachine
vẫn sẽ áp dụng ngoại lệ.
Vì việc truyền giống phải diễn ra bất kể trạng thái tiếp theo có đạt được hay không, nên lệnh gọi lại xử lý lỗi phải thực hiện tác vụ này. Đối với StateMachine
nội bộ, bạn có thể thực hiện việc này bằng cách gọi lệnh gọi lại của thành phần mẹ.
Ở StateMachine
cấp cao nhất, giao diện với SkyFunction, bạn có thể thực hiện việc này bằng cách gọi phương thức setException
của ValueOrExceptionProducer
.
Sau đó, ValueOrExceptionProducer.tryProduceValue
sẽ gửi ngoại lệ, ngay cả khi thiếu SkyValues.
Nếu đang sử dụng Driver
trực tiếp, bạn cần kiểm tra các lỗi được truyền từ SkyFunction, ngay cả khi máy chưa xử lý xong.
Xử lý sự kiện
Đối với SkyFunctions cần phát các sự kiện, StoredEventHandler
được chèn
vào SkyKeyComputeState và tiếp tục được chèn vào StateMachine
yêu cầu
các sự kiện đó. Trước đây, cần có StoredEventHandler
do Skyframe bỏ qua một số sự kiện nhất định trừ phi các sự kiện đó được phát lại nhưng sau đó đã được khắc phục.
Việc chèn StoredEventHandler
được duy trì vì tính năng này giúp đơn giản hoá việc triển khai các sự kiện được tạo ra từ các lệnh gọi lại xử lý lỗi.
Driver
và bắc cầu lên SkyFunction
Driver
chịu trách nhiệm quản lý việc thực thi các StateMachine
,
bắt đầu bằng StateMachine
gốc được chỉ định. Vì StateMachine
có thể thêm các nhiệm vụ con vào StateMachine
một cách đệ quy, nên Driver
duy nhất có thể quản lý nhiều nhiệm vụ con. Những việc phụ cần làm này sẽ tạo ra một cấu trúc cây, do có tính năng đồng thời có cấu trúc. Driver
phân loại các lượt tìm kiếm SkyValue qua các tác vụ phụ để cải thiện hiệu quả.
Có một số lớp được xây dựng xung quanh Driver
, với API sau đây.
public final class Driver {
public Driver(StateMachine root);
public boolean drive(SkyFunction.Environment env) throws InterruptedException;
}
Driver
lấy một StateMachine
gốc làm tham số. Việc gọi
Driver.drive
sẽ thực thi StateMachine
trong phạm vi có thể mà không cần
khởi động lại Skyframe. Phương thức này trả về giá trị true khi StateMachine
hoàn tất và false nếu không cho thấy rằng không phải tất cả giá trị đều có sẵn.
Driver
duy trì trạng thái đồng thời của StateMachine
và rất phù hợp để nhúng trong SkyKeyComputeState
.
Tạo trực tiếp Driver
Việc triển khai StateMachine
thường truyền đạt kết quả của chúng thông qua
lệnh gọi lại. Bạn có thể khởi tạo trực tiếp Driver
như trong ví dụ sau.
Driver
được nhúng trong quá trình triển khai SkyKeyComputeState
cùng với
hoạt động triển khai của ResultSink
tương ứng để được xác định thêm một chút. Ở cấp cao nhất, đối tượng State
là một trình nhận thích hợp cho kết quả tính toán vì đối tượng này được đảm bảo sẽ tồn tại lâu hơn 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;
}
}
Mã bên dưới phác thảo 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;
}
}
Sau đó, mã để tính toán từng phần giúp cho kết quả có dạng như sau.
@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;
}
Đang nhúng Driver
Nếu StateMachine
tạo ra một giá trị và không đưa ra ngoại lệ, thì việc nhúng
Driver
là một cách triển khai khả thi khác, như trong ví dụ sau.
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 có thể có mã giống như sau (trong đó State
là
loại cụ thể của hàm 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;
}
Việc nhúng Driver
trong quá trình triển khai StateMachine
phù hợp hơn với kiểu mã hoá đồng bộ của Skyframe.
StateMachines có thể tạo ra các ngoại lệ
Nếu không, sẽ có các lớp ValueOrExceptionProducer
và ValueOrException2Producer
có thể nhúng SkyKeyComputeState
có API đồng bộ để khớp với mã SkyFunction đồng bộ.
Lớp trừu tượng ValueOrExceptionProducer
bao gồm các phương thức sau.
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. }
}
Lớp này bao gồm một thực thể Driver
được nhúng và gần giống với lớp ResultProducer
trong Trình điều khiển nhúng và giao diện với SkyFunction theo cách tương tự. Thay vì xác định ResultSink
,
các phương thức triển khai sẽ gọi setValue
hoặc setException
khi một trong hai hoạt động đó xảy ra.
Khi cả hai điều này xảy ra, ngoại lệ sẽ được ưu tiên. Phương thức tryProduceValue
sẽ kết nối mã gọi lại không đồng bộ với mã đồng bộ và gửi
một ngoại lệ khi đã đặt.
Như đã lưu ý trước đó, trong quá trình tạo bong bóng lỗi, có thể xảy ra lỗi ngay cả khi máy chưa được thực hiện vì không phải tất cả dữ liệu đầu vào đều có sẵn. Để đáp ứng yêu cầu này, tryProduceValue
sẽ gửi bất kỳ trường hợp ngoại lệ nào đã đặt, ngay cả trước khi máy chạy xong.
Phần kết: Cuối cùng là xoá lệnh gọi lại
StateMachine
là một cách có hiệu quả cao nhưng là nguyên mẫu để thực hiện phép tính không đồng bộ. Các thành phần tiếp nối (đặc biệt là dưới dạng Runnable
được truyền đến ListenableFuture
) rất phổ biến trong một số phần của mã Bazel, nhưng không phổ biến trong các hàm phân tích SkyFunction. Công cụ phân tích chủ yếu bị ràng buộc bởi CPU và không có API không đồng bộ hiệu quả cho I/O trên ổ đĩa. Cuối cùng, bạn nên
tối ưu hoá các hàm gọi lại vì chúng có đường cong học tập và trở ngại
khả năng đọc được.
Một trong những phương án thay thế hứa hẹn nhất là luồng ảo Java. Thay vì phải viết lệnh gọi lại, mọi thứ sẽ được thay thế bằng các lệnh gọi đồng bộ, chặn. Điều này có thể xảy ra vì việc liên kết tài nguyên chuỗi ảo, không giống như luồng nền tảng, được cho là rẻ. Tuy nhiên, ngay cả với các luồng ảo, việc thay thế các hoạt động đồng bộ đơn giản bằng các nguyên tắc tạo và đồng bộ hoá luồng cũng tốn kém. Chúng tôi đã di chuyển các luồng ảo từ StateMachine
sang Java. Các lệnh này chậm hơn 3 lần, dẫn đến độ trễ phân tích hai đầu tăng gần gấp 3 lần. Vì các luồng ảo vẫn là một tính năng xem trước, nên có thể hoạt động di chuyển này sẽ được thực hiện vào một ngày sau đó khi hiệu suất được cải thiện.
Một phương pháp khác cần cân nhắc là chờ coroutine Loom nếu chúng có sẵn. Ưu điểm ở đây là bạn có thể giảm mức hao tổn đồng bộ hoá bằng cách sử dụng tính năng đa nhiệm phối hợp.
Nếu vẫn không thành công, việc viết lại mã byte cấp thấp cũng có thể là một phương án thay thế khả thi. Nếu có đủ tính năng tối ưu hoá, bạn có thể đạt được hiệu suất bằng cách sử dụng mã lệnh gọi lại được viết thủ công.
Phụ lục
Địa chỉ gọi lại
Địa chỉ gọi lại là một vấn đề khét tiếng trong mã không đồng bộ sử dụng lệnh gọi lại. Nó xuất phát từ thực tế là phần tiếp theo của bước tiếp theo được lồng trong bước trước đó. Nếu có nhiều bước, việc lồng ghép này có thể cực kỳ sâu sắc. Nếu cùng với luồng điều khiển, mã sẽ không thể quản lý được.
class CallbackHell implements StateMachine {
@Override
public StateMachine step(Tasks task) {
doA();
return (t, l) -> {
doB();
return (t1, l2) -> {
doC();
return DONE;
};
};
}
}
Một trong những lợi thế của việc triển khai lồng nhau là có thể giữ nguyên khung ngăn xếp của bước bên ngoài. Trong Java, các biến lambda được thu thập phải là dữ liệu có hiệu quả cuối cùng để việc sử dụng các biến đó có thể cồng kềnh. Bạn cần tránh lồng ghép sâu bằng cách tham chiếu đến phương thức trả về dưới dạng các giá trị tiếp theo thay vì lambda như dưới đây.
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;
}
}
Địa chỉ gọi lại cũng có thể xảy ra nếu bạn sử dụng mẫu runAfter
quá nhiều, nhưng có thể tránh được điều này bằng cách chèn các thành phần xen kẽ với các bước tuần tự.
Ví dụ: Tra cứu SkyValue theo chuỗi
Thông thường, logic ứng dụng yêu cầu các chuỗi tìm kiếm SkyValue phụ thuộc, chẳng hạn như nếu SkyKey thứ hai phụ thuộc vào SkyValue đầu tiên. Suy nghĩ về vấn đề này một cách thô sơ, điều này sẽ dẫn đến một cấu trúc gọi lại phức tạp, được lồng ghép sâu.
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;
}
Tuy nhiên, vì các giá trị tiếp tục được chỉ định làm tham chiếu phương thức, nên mã sẽ tuân thủ quy trình chuyển đổi trạng thái: step2
tuân theo step1
. Xin lưu ý rằng ở đây, một lambda được dùng để gán value2
. Điều này giúp thứ tự của mã khớp với thứ tự tính toán từ trên xuống dưới.
Mẹo Khác
Mức độ dễ đọc: Thứ tự thực thi
Để cải thiện khả năng đọc, hãy cố gắng duy trì việc triển khai StateMachine.step
theo thứ tự thực thi và triển khai lệnh gọi lại ngay sau khi vị trí được truyền vào mã. Điều này không phải lúc nào cũng có thể xảy ra khi luồng điều khiển phân nhánh. Nhận xét bổ sung có thể hữu ích trong các trường hợp như vậy.
Trong Ví dụ: Tra cứu SkyValue theo chuỗi, hệ thống sẽ tạo một tệp tham chiếu phương thức trung gian để đạt được điều này. Việc này đánh đổi một số hiệu suất nhỏ để dễ đọc, có thể đáng giá ở đây.
Giả thuyết thế hệ
Đối tượng Java tồn tại ở mức trung bình phá vỡ giả thuyết thế hệ của trình thu gom rác Java, được thiết kế để xử lý các đối tượng tồn tại trong thời gian rất ngắn hoặc các đối tượng tồn tại mãi mãi. Theo định nghĩa, các đối tượng trong SkyKeyComputeState
vi phạm giả thuyết này. Các đối tượng như vậy, chứa cây xây dựng của tất cả các StateMachine
vẫn đang chạy, đã bị can thiệp vào hệ thống tại Driver
có vòng đời trung gian khi chúng tạm ngưng, chờ các quá trình tính toán không đồng bộ hoàn tất.
JDK19 có vẻ ít xấu hơn nhưng khi sử dụng StateMachine
, đôi khi, bạn có thể thấy thời gian hiển thị tăng lên, ngay cả khi lượng rác thực tế đã tạo giảm đáng kể. Vì StateMachine
có tuổi thọ trung bình
nên chúng có thể được chuyển lên thế hệ cũ, khiến mã này lấp đầy nhanh hơn, do đó,
cần phải có các GC lớn hoặc đầy đủ hơn để dọn dẹp.
Biện pháp phòng ngừa ban đầu là giảm thiểu việc sử dụng các biến StateMachine
, nhưng không phải lúc nào cũng có thể thực hiện được, chẳng hạn như khi cần một giá trị trên nhiều trạng thái. Nếu có thể, biến step
ngăn xếp cục bộ là các biến thế hệ trẻ và GC'd một cách hiệu quả.
Đối với các biến StateMachine
, việc chia nhỏ nội dung thành các tác vụ phụ và tuân theo mẫu được đề xuất để Truyền giá trị giữa các StateMachine
cũng rất hữu ích. Lưu ý khi tuân thủ mẫu, chỉ các StateMachine
con mới có tham chiếu đến StateMachine
mẹ chứ không phải ngược lại. Điều này có nghĩa là khi thành phần con hoàn tất và cập nhật thành phần mẹ bằng lệnh gọi lại kết quả, thành phần con cái sẽ tự nhiên nằm ngoài phạm vi và đủ điều kiện sử dụng GC.
Cuối cùng, trong một số trường hợp, bạn cần có một biến StateMachine
ở các trạng thái trước đó nhưng không cần thiết ở các trạng thái sau đó. Việc xoá các tệp tham chiếu của đối tượng lớn sau khi biết rằng các đối tượng đó không còn cần thiết có thể có lợi.
Trạng thái đặt tên
Khi đặt tên phương thức, thường có thể đặt tên cho một phương thức cho hành vi xảy ra trong phương thức đó. Sẽ không rõ ràng hơn về cách thực hiện việc này trong StateMachine
vì không có ngăn xếp. Ví dụ: giả sử phương thức foo
gọi một phương thức phụ bar
. Trong StateMachine
, lệnh này có thể được chuyển thành trình tự trạng thái foo
, theo sau là bar
. foo
không còn chứa hành vi bar
. Do đó, tên phương thức cho các trạng thái có xu hướng hẹp hơn về phạm vi,
có thể phản ánh hành vi cục bộ.
Sơ đồ cây đồng thời
Sau đây là khung hiển thị thay thế của sơ đồ trong Mô hình đồng thời có cấu trúc, mô tả rõ hơn cấu trúc cây. Các khối này tạo thành một cây nhỏ.
-
Ngược lại với quy ước khởi động lại ngay từ đầu khi Skyframe không có giá trị. ↩
-
Xin lưu ý rằng
step
được phép gửiInterruptedException
, nhưng các ví dụ sẽ bỏ qua trường hợp này. Có một vài phương thức thấp trong mã Bazel, áp dụng trường hợp ngoại lệ này và truyền lên đếnDriver
, như được mô tả sau đây, chạyStateMachine
. Bạn có thể không khai báo việc gửi nội dung khi không cần thiết.↩ -
Các việc phụ cần làm đồng thời được thúc đẩy bởi
ConfiguredTargetFunction
thực hiện độc lập hoạt động cho từng phần phụ thuộc. Thay vì thao túng các cấu trúc dữ liệu phức tạp xử lý tất cả các phần phụ thuộc cùng một lúc, các phần phụ thuộc lại hoạt động không hiệu quả nhưng mỗi phần phụ thuộc lại cóStateMachine
riêng, độc lập.↩ -
Nhiều lệnh gọi
tasks.lookUp
trong một bước được nhóm lại với nhau. Bạn có thể tạo các lô bổ sung bằng cách tra cứu xảy ra trong các tác vụ phụ đồng thời.↩ -
Về mặt lý thuyết, khái niệm này tương tự như tính năng đồng thời có cấu trúc jeps/428 của Java.↩
-
Cách làm này cũng tương tự như việc tạo một luồng và tham gia luồng đó để đạt được cấu trúc tuần tự. ↩