Hướng dẫn về Skyframe StateMachines

Báo cáo vấn đề Xem nguồn Hằng đêm · 7,3 · 7.2 · 7.1 · 7 · 6,5

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.

Tạm ngưng rồi tiếp tục

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ưngtiế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ạithà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.getValueSkyFunction.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ù Subtask1Subtask2 đồ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 lookUpenqueue đề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ụ:

Tính năng đồng thời có cấu trúc

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 không có cấu trúc

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 M1M2 là thực thể StateMachine có chung StateMachine, S, với M1M2 là dãy số <A, S, B><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ạiNhâ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ỏ.

Mô hình đồng thời có cấu trúc 3D


  1. 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. 

  2. Lưu ý rằng step được phép gửi InterruptedException, 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 đến Driver, sẽ được mô tả sau, chạy StateMachine. Bạn không nên khai báo về việc gửi dữ liệu khi không cần thiết.

  3. 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.

  4. 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.

  5. 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.

  6. 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ự.