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