Xử lý luồng

Mô hình luồng của Binder được thiết kế để tạo điều kiện thuận lợi cho các lệnh gọi hàm cục bộ ngay cả khi các lệnh gọi đó có thể đến một quy trình từ xa. Cụ thể, mọi quy trình lưu trữ một nút phải có một nhóm gồm một hoặc nhiều luồng liên kết để xử lý các giao dịch với các nút được lưu trữ trong quy trình đó.

Giao dịch đồng bộ và không đồng bộ

Trình liên kết hỗ trợ các giao dịch đồng bộ và không đồng bộ. Các phần sau đây giải thích cách thực hiện từng loại giao dịch.

Giao dịch đồng bộ

Các giao dịch đồng bộ sẽ chặn cho đến khi được thực thi trên nút và người gọi đã nhận được câu trả lời cho giao dịch đó. Hình sau đây cho thấy cách thực hiện một giao dịch đồng bộ:

Giao dịch đồng bộ.

Hình 1. Giao dịch đồng bộ.

Để thực thi một giao dịch đồng bộ, trình liên kết sẽ làm như sau:

  1. Các luồng trong nhóm luồng mục tiêu (T2 và T3) gọi vào trình điều khiển hạt nhân để chờ công việc đến.
  2. Nhân nhận được một giao dịch mới và đánh thức một luồng (T2) trong quy trình đích để xử lý giao dịch.
  3. Luồng gọi (T1) chặn và chờ phản hồi.
  4. Quy trình đích thực hiện giao dịch và trả về một câu trả lời.
  5. Luồng trong quy trình đích (T2) gọi lại vào trình điều khiển hạt nhân để chờ công việc mới.

Giao dịch không đồng bộ

Các giao dịch không đồng bộ không chặn để hoàn tất; luồng lệnh gọi sẽ bỏ chặn ngay khi giao dịch được truyền vào nhân. Hình sau đây cho thấy cách thực thi một giao dịch không đồng bộ:

Giao dịch không đồng bộ.

Hình 2. Giao dịch không đồng bộ.

  1. Các luồng trong nhóm luồng mục tiêu (T2 và T3) gọi vào trình điều khiển hạt nhân để chờ công việc đến.
  2. Nhân nhận được một giao dịch mới và đánh thức một luồng (T2) trong quy trình đích để xử lý giao dịch.
  3. Luồng gọi (T1) tiếp tục thực thi.
  4. Quy trình đích thực hiện giao dịch và trả về một câu trả lời.
  5. Luồng trong quy trình đích (T2) gọi lại vào trình điều khiển hạt nhân để chờ công việc mới.

Xác định hàm đồng bộ hoặc không đồng bộ

Các hàm được đánh dấu là oneway trong tệp AIDL là không đồng bộ. Ví dụ:

oneway void someCall();

Nếu một hàm không được đánh dấu là oneway, thì đó là một hàm đồng bộ, ngay cả khi hàm đó trả về void.

Chuyển đổi tuần tự các giao dịch không đồng bộ

Trình liên kết tuần tự hoá các giao dịch không đồng bộ từ bất kỳ nút đơn lẻ nào. Hình sau đây minh hoạ cách liên kết chuyển đổi tuần tự các giao dịch không đồng bộ:

Nối tiếp hoá các giao dịch không đồng bộ.

Hình 3. Nối tiếp hoá các giao dịch không đồng bộ.

  1. Các luồng trong nhóm luồng mục tiêu (B1 và B2) gọi vào trình điều khiển hạt nhân để chờ công việc đến.
  2. Hai giao dịch (T1 và T2) trên cùng một nút (N1) được gửi đến nhân.
  3. Hạt nhân nhận được một giao dịch mới và vì các giao dịch đó đến từ cùng một nút (N1), nên hạt nhân sẽ tuần tự hoá các giao dịch đó.
  4. Một giao dịch khác trên một nút khác (N2) được gửi đến kernel.
  5. Nhân nhận được giao dịch thứ ba và đánh thức một luồng (B2) trong quy trình đích để xử lý giao dịch.
  6. Các quy trình đích thực thi từng giao dịch và trả về một câu trả lời.

Giao dịch lồng nhau

Các giao dịch đồng bộ có thể được lồng vào nhau; một luồng đang xử lý một giao dịch có thể phát hành một giao dịch mới. Giao dịch lồng nhau có thể là một quy trình khác hoặc là quy trình mà bạn nhận được giao dịch hiện tại. Hành vi này mô phỏng các lệnh gọi hàm cục bộ. Ví dụ: giả sử bạn có một hàm chứa các hàm lồng nhau:

def outer_function(x):
    def inner_function(y):
        def inner_inner_function(z):

Nếu đây là các lệnh gọi cục bộ, thì chúng sẽ được thực thi trên cùng một luồng. Cụ thể, nếu phương thức gọi inner_function cũng là quy trình lưu trữ nút triển khai inner_inner_function, thì lệnh gọi đến inner_inner_function sẽ được thực thi trên cùng một luồng.

Hình sau đây cho thấy cách trình liên kết xử lý các giao dịch lồng nhau:

Giao dịch lồng nhau.

Hình 4. Giao dịch lồng nhau.

  1. Luồng A1 yêu cầu chạy foo().
  2. Trong yêu cầu này, luồng B1 chạy bar() mà A chạy trên cùng một luồng A1.

Hình sau đây cho thấy quá trình thực thi luồng nếu nút triển khai bar() nằm trong một quy trình khác:

Giao dịch lồng nhau trong các quy trình khác nhau.

Hình 5. Giao dịch lồng nhau trong các quy trình khác nhau.

  1. Luồng A1 yêu cầu chạy foo().
  2. Trong yêu cầu này, luồng B1 chạy bar(), luồng này chạy trong một luồng C1 khác.

Hình sau đây cho thấy cách luồng sử dụng lại cùng một quy trình ở bất kỳ đâu trong chuỗi giao dịch:

Các giao dịch lồng nhau sử dụng lại một luồng.

Hình 6. Các giao dịch lồng nhau sử dụng lại một luồng.

  1. Quy trình A gọi vào quy trình B.
  2. Quy trình B gọi vào quy trình C.
  3. Sau đó, quy trình C gọi lại vào quy trình A và nhân sẽ sử dụng lại luồng A1 trong quy trình A, là một phần của chuỗi giao dịch.

Đối với các giao dịch không đồng bộ, việc lồng ghép không đóng vai trò gì; ứng dụng không đợi kết quả của một giao dịch không đồng bộ, nên không có việc lồng ghép. Nếu trình xử lý của một giao dịch không đồng bộ gọi vào quy trình đã phát hành giao dịch không đồng bộ đó, thì giao dịch đó có thể được xử lý trên bất kỳ luồng nào còn trống trong quy trình đó.

Tránh bế tắc

Hình ảnh sau đây minh hoạ một tình trạng bế tắc thường gặp:

Tình trạng bế tắc thường gặp.

Hình 7. Tình trạng bế tắc thường gặp.

  1. Quy trình A lấy mutex MA và thực hiện một lệnh gọi liên kết (T1) đến quy trình B. Quy trình này cũng cố gắng lấy mutex MB.
  2. Đồng thời, quy trình B lấy mutex MB và thực hiện một lệnh gọi liên kết (T2) đến quy trình A. Quy trình này cố gắng lấy mutex MA.

Nếu các giao dịch này trùng lặp, mỗi giao dịch có thể lấy một mutex trong quy trình của chúng trong khi chờ quy trình khác giải phóng một mutex, dẫn đến tình trạng bế tắc.

Để tránh tình trạng bế tắc khi sử dụng liên kết, đừng giữ bất kỳ khoá nào khi thực hiện lệnh gọi liên kết.

Khoá các quy tắc sắp xếp và bế tắc

Trong một môi trường thực thi duy nhất, tình trạng bế tắc thường được tránh bằng quy tắc sắp xếp khoá. Tuy nhiên, khi thực hiện các lệnh gọi giữa các quy trình và giữa các cơ sở mã, đặc biệt là khi mã được cập nhật, bạn không thể duy trì và điều phối quy tắc sắp xếp.

Mutex đơn và tình trạng bế tắc

Với các giao dịch lồng nhau, quy trình B có thể gọi trực tiếp trở lại cùng một luồng trong quy trình A đang giữ một mutex. Do đó, do đệ quy không mong muốn, bạn vẫn có thể gặp phải tình trạng bế tắc với một mutex duy nhất.

Lệnh gọi đồng bộ và tình trạng bế tắc

Mặc dù các lệnh gọi liên kết không đồng bộ không chặn quá trình hoàn tất, nhưng bạn cũng nên tránh giữ khoá cho các lệnh gọi không đồng bộ. Nếu giữ một khoá, bạn có thể gặp phải vấn đề về khoá nếu vô tình thay đổi một cuộc gọi một chiều thành cuộc gọi đồng bộ.

Một luồng liên kết và tình trạng bế tắc

Mô hình giao dịch của Binder cho phép nhập lại, vì vậy, ngay cả khi một quy trình có một luồng liên kết duy nhất, bạn vẫn cần khoá. Ví dụ: giả sử bạn đang lặp lại một danh sách trong quy trình A một luồng. Đối với mỗi mục trong danh sách, bạn sẽ thực hiện một giao dịch liên kết đi. Nếu việc triển khai hàm mà bạn đang gọi tạo ra một giao dịch liên kết mới cho một nút được lưu trữ trong quy trình A, thì giao dịch đó sẽ được xử lý trên cùng một luồng đang lặp lại danh sách. Nếu việc triển khai giao dịch đó sửa đổi cùng một danh sách, bạn có thể gặp vấn đề khi tiếp tục lặp lại danh sách sau này.

Định cấu hình kích thước nhóm luồng

Khi một dịch vụ có nhiều ứng dụng, việc thêm nhiều luồng hơn vào nhóm luồng có thể giảm tình trạng tranh chấp và xử lý nhiều lệnh gọi song song hơn. Sau khi xử lý đúng tính đồng thời, bạn có thể thêm nhiều luồng hơn. Một vấn đề có thể xảy ra do việc thêm nhiều luồng mà một số luồng có thể không được dùng trong quá trình xử lý khối lượng công việc không tải.

Các luồng được tạo theo yêu cầu cho đến khi đạt đến số lượng tối đa đã định cấu hình. Sau khi một luồng liên kết được tạo, luồng đó sẽ hoạt động cho đến khi quá trình lưu trữ luồng đó kết thúc.

Thư viện libbinder có mặc định là 15 luồng. Sử dụng setThreadPoolMaxThreadCount để thay đổi giá trị này:

using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);