Nguyên tắc về API không đồng bộ và không chặn của Android

Các API không chặn yêu cầu công việc diễn ra rồi trả lại quyền kiểm soát cho luồng lệnh gọi để luồng này có thể thực hiện công việc khác trước khi hoàn tất thao tác được yêu cầu. Các API này rất hữu ích trong trường hợp công việc được yêu cầu có thể đang diễn ra hoặc có thể yêu cầu chờ hoàn tất I/O hoặc IPC, tính khả dụng của các tài nguyên hệ thống có mức độ tranh chấp cao hoặc dữ liệu đầu vào của người dùng trước khi công việc có thể tiếp tục. Đặc biệt, các API được thiết kế tốt sẽ cung cấp một cách để huỷ thao tác đang diễn ra và ngăn chặn việc thực hiện thay cho phương thức gọi ban đầu, duy trì tình trạng hệ thống và thời lượng pin khi không còn cần thiết nữa.

API không đồng bộ là một cách để đạt được hành vi không chặn. API không đồng bộ chấp nhận một số hình thức tiếp tục hoặc lệnh gọi lại được thông báo khi thao tác hoàn tất hoặc các sự kiện khác trong quá trình hoạt động.

Có 2 động lực chính để viết một API không đồng bộ:

  • Thực thi đồng thời nhiều thao tác, trong đó thao tác thứ N phải được bắt đầu trước khi thao tác thứ N-1 hoàn tất.
  • Tránh chặn luồng lệnh gọi cho đến khi một thao tác hoàn tất.

Kotlin mạnh mẽ thúc đẩy chế độ đồng thời có cấu trúc, một loạt các nguyên tắc và API được xây dựng dựa trên các hàm tạm ngưng giúp tách biệt quá trình thực thi đồng bộ và không đồng bộ của mã khỏi hành vi chặn luồng. Hàm tạm ngưng là không chặnđồng bộ.

Hàm tạm ngưng:

  • Đừng chặn luồng gọi của chúng mà thay vào đó, hãy tạo luồng thực thi của chúng dưới dạng chi tiết triển khai trong khi chờ kết quả của các thao tác thực thi ở nơi khác.
  • Thực thi đồng bộ và không yêu cầu phương thức gọi của một API không chặn tiếp tục thực thi đồng thời với công việc không chặn do lệnh gọi API khởi tạo.

Trang này trình bày chi tiết một đường cơ sở tối thiểu mà nhà phát triển có thể yên tâm khi làm việc với các API không chặn và không đồng bộ, sau đó là một loạt các công thức để tạo API đáp ứng những kỳ vọng này bằng ngôn ngữ Kotlin hoặc Java, trong nền tảng Android hoặc các thư viện Jetpack. Nếu có vấn đề chưa rõ, hãy xem xét kỳ vọng của nhà phát triển như các yêu cầu đối với mọi nền tảng API mới.

Kỳ vọng của nhà phát triển đối với các API không đồng bộ

Các kỳ vọng sau đây được viết từ quan điểm của các API không tạm ngưng, trừ phi có lưu ý khác.

Các API chấp nhận lệnh gọi lại thường là không đồng bộ

Nếu một API chấp nhận một lệnh gọi lại không được ghi lại để chỉ được gọi tại chỗ (tức là chỉ được gọi bởi luồng gọi trước khi chính lệnh gọi API trả về), thì API đó được giả định là không đồng bộ và API đó phải đáp ứng tất cả các kỳ vọng khác được ghi lại trong các phần sau.

Ví dụ về một lệnh gọi lại chỉ được gọi tại chỗ là hàm map hoặc hàm lọc bậc cao gọi một trình ánh xạ hoặc vị từ trên từng mục trong một tập hợp trước khi trả về.

API không đồng bộ phải trả về nhanh nhất có thể

Nhà phát triển mong muốn các API không đồng bộ sẽ không chặn và trả về nhanh chóng sau khi bắt đầu yêu cầu cho hoạt động. Bạn luôn có thể gọi một API không đồng bộ bất cứ lúc nào và việc gọi một API không đồng bộ sẽ không bao giờ dẫn đến khung hình bị giật hoặc lỗi ANR.

Nền tảng hoặc các thư viện có thể kích hoạt nhiều thao tác và tín hiệu vòng đời theo yêu cầu, đồng thời việc yêu cầu nhà phát triển nắm giữ kiến thức toàn cầu về tất cả các vị trí gọi tiềm năng cho mã của họ là không bền vững. Ví dụ: bạn có thể thêm Fragment vào FragmentManager trong một giao dịch đồng bộ để phản hồi phép đo và bố cục View khi nội dung ứng dụng phải được điền để lấp đầy khoảng trống có sẵn (chẳng hạn như RecyclerView). LifecycleObserver phản hồi lệnh gọi lại vòng đời onStart của mảnh này có thể thực hiện các thao tác khởi động một lần một cách hợp lý tại đây và thao tác này có thể nằm trên đường dẫn mã quan trọng để tạo một khung hình động không bị giật. Nhà phát triển luôn phải tự tin rằng việc gọi bất kỳ API không đồng bộ nào để phản hồi những loại lệnh gọi lại vòng đời này sẽ không phải là nguyên nhân gây ra khung hình bị giật.

Điều này ngụ ý rằng công việc do một API không đồng bộ thực hiện trước khi trả về phải rất nhẹ; tạo một bản ghi về yêu cầu và lệnh gọi lại được liên kết, đồng thời đăng ký bản ghi đó với công cụ thực thi thực hiện công việc tối đa. Nếu việc đăng ký một thao tác không đồng bộ yêu cầu IPC, thì quá trình triển khai API phải thực hiện mọi biện pháp cần thiết để đáp ứng kỳ vọng này của nhà phát triển. Điều này có thể bao gồm một hoặc nhiều nội dung sau:

  • Triển khai IPC cơ bản dưới dạng lệnh gọi liên kết một chiều
  • Thực hiện lệnh gọi liên kết hai chiều vào máy chủ hệ thống, trong đó việc hoàn tất quá trình đăng ký không yêu cầu khoá có mức độ tranh chấp cao
  • Đăng yêu cầu lên một luồng worker trong quy trình ứng dụng để thực hiện quá trình đăng ký chặn qua IPC

API không đồng bộ phải trả về giá trị rỗng và chỉ truyền cho các đối số không hợp lệ

Các API không đồng bộ phải báo cáo tất cả kết quả của thao tác được yêu cầu cho lệnh gọi lại đã cung cấp. Điều này cho phép nhà phát triển triển khai một đường dẫn mã duy nhất để xử lý thành công và lỗi.

Các API không đồng bộ có thể kiểm tra các đối số để tìm giá trị rỗng và gửi NullPointerException hoặc kiểm tra để đảm bảo các đối số được cung cấp nằm trong một phạm vi hợp lệ và gửi IllegalArgumentException. Ví dụ: đối với một hàm chấp nhận float trong phạm vi từ 0 đến 1f, hàm có thể kiểm tra xem tham số có nằm trong phạm vi này hay không và gửi IllegalArgumentException nếu tham số nằm ngoài phạm vi, hoặc một String ngắn có thể được kiểm tra để tuân thủ một định dạng hợp lệ, chẳng hạn như chỉ có chữ và số. (Hãy nhớ rằng máy chủ hệ thống không bao giờ được tin tưởng quy trình ứng dụng! Mọi dịch vụ hệ thống đều phải sao chép các bước kiểm tra này trong chính dịch vụ hệ thống.)

Tất cả các lỗi khác sẽ được báo cáo cho lệnh gọi lại đã cung cấp. Điều này bao gồm nhưng không giới hạn ở:

  • Không thực hiện được thao tác đã yêu cầu do lỗi xuất phát từ thiết bị đầu cuối
  • Ngoại lệ bảo mật đối với trường hợp thiếu quyền uỷ quyền hoặc quyền cần thiết để hoàn tất thao tác
  • Vượt quá hạn mức thực hiện thao tác
  • Quy trình ứng dụng không đủ "nền trước" để thực hiện thao tác
  • Đã ngắt kết nối phần cứng bắt buộc
  • Lỗi mạng
  • Số lần bị tạm ngừng
  • Binder bị huỷ hoặc quy trình từ xa không hoạt động

API không đồng bộ phải cung cấp cơ chế huỷ

API không đồng bộ phải cung cấp một cách để cho biết cho một hoạt động đang chạy rằng người gọi không còn quan tâm đến kết quả nữa. Thao tác huỷ này sẽ báo hiệu 2 điều:

Các lệnh gọi lại do người gọi cung cấp phải được phát hành

Các lệnh gọi lại được cung cấp cho các API không đồng bộ có thể chứa các tham chiếu cố định đến các biểu đồ đối tượng lớn và công việc đang diễn ra giữ một tham chiếu cố định đến lệnh gọi lại đó có thể ngăn các biểu đồ đối tượng đó được thu gom rác. Bằng cách phát hành các tham chiếu lệnh gọi lại này khi huỷ, các biểu đồ đối tượng này có thể đủ điều kiện để thu gom rác sớm hơn nhiều so với trường hợp công việc được phép chạy đến khi hoàn tất.

Công cụ thực thi thực hiện công việc cho người gọi có thể dừng công việc đó

Tác vụ do các lệnh gọi API không đồng bộ khởi tạo có thể tiêu tốn nhiều năng lượng hoặc tài nguyên hệ thống khác. Các API cho phép người gọi báo hiệu khi không cần công việc này nữa sẽ cho phép dừng công việc đó trước khi công việc có thể tiêu tốn thêm tài nguyên hệ thống.

Các điểm cần đặc biệt lưu ý đối với ứng dụng được lưu vào bộ nhớ đệm hoặc bị đóng băng

Khi thiết kế các API không đồng bộ mà lệnh gọi lại bắt nguồn từ một quy trình hệ thống và được gửi đến các ứng dụng, hãy cân nhắc những điều sau:

  1. Quy trình và vòng đời của ứng dụng: quy trình ứng dụng nhận có thể ở trạng thái đã lưu vào bộ nhớ đệm.
  2. Trình đóng băng ứng dụng được lưu vào bộ nhớ đệm: quy trình ứng dụng nhận có thể bị đóng băng.

Khi một quy trình ứng dụng chuyển sang trạng thái đã lưu vào bộ nhớ đệm, tức là quy trình đó không chủ động lưu trữ bất kỳ thành phần nào mà người dùng nhìn thấy, chẳng hạn như các hoạt động và dịch vụ. Ứng dụng được lưu giữ trong bộ nhớ trong trường hợp người dùng nhìn thấy lại, nhưng trong thời gian chờ đợi, ứng dụng không được thực hiện công việc. Trong hầu hết các trường hợp, bạn nên tạm dừng gửi các lệnh gọi lại ứng dụng khi ứng dụng đó chuyển sang trạng thái đã lưu vào bộ nhớ đệm và tiếp tục khi ứng dụng thoát khỏi trạng thái đã lưu vào bộ nhớ đệm, để không gây ra hoạt động trong các quy trình ứng dụng đã lưu vào bộ nhớ đệm.

Ứng dụng được lưu vào bộ nhớ đệm cũng có thể bị đóng băng. Khi bị đóng băng, ứng dụng sẽ không nhận được thời gian CPU và hoàn toàn không thể thực hiện bất kỳ thao tác nào. Mọi lệnh gọi đến các lệnh gọi lại đã đăng ký của ứng dụng đó đều được lưu vào bộ nhớ đệm và gửi khi ứng dụng được giải phóng.

Các giao dịch được lưu vào bộ nhớ đệm cho lệnh gọi lại ứng dụng có thể đã cũ vào thời điểm ứng dụng được rã đông và xử lý các giao dịch đó. Bộ nhớ đệm có hạn và nếu tràn sẽ khiến ứng dụng nhận gặp sự cố. Để tránh làm quá tải các ứng dụng bằng các sự kiện cũ hoặc làm tràn bộ nhớ đệm của chúng, đừng gửi các lệnh gọi lại ứng dụng trong khi quá trình của chúng bị đóng băng.

Đang xem xét:

  • Bạn nên cân nhắc tạm dừng gửi lệnh gọi lại ứng dụng trong khi quy trình của ứng dụng được lưu vào bộ nhớ đệm.
  • Bạn PHẢI tạm dừng gửi các lệnh gọi lại ứng dụng trong khi quy trình của ứng dụng bị đóng băng.

Theo dõi trạng thái

Cách theo dõi thời điểm ứng dụng chuyển sang hoặc thoát khỏi trạng thái lưu vào bộ nhớ đệm:

mActivityManager.addOnUidImportanceListener(
    new UidImportanceListener() { ... },
    IMPORTANCE_CACHED);

Cách theo dõi thời điểm ứng dụng bị đóng băng hoặc không bị đóng băng:

IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);

Các chiến lược để tiếp tục gửi lệnh gọi lại ứng dụng

Cho dù bạn tạm dừng gửi lệnh gọi lại ứng dụng khi ứng dụng chuyển sang trạng thái đã lưu vào bộ nhớ đệm hay trạng thái đóng băng, thì khi ứng dụng thoát khỏi trạng thái tương ứng, bạn nên tiếp tục gửi các lệnh gọi lại đã đăng ký của ứng dụng cho đến khi ứng dụng huỷ đăng ký lệnh gọi lại hoặc quy trình ứng dụng kết thúc.

Ví dụ:

IBinder binder = <...>;
bool shouldSendCallbacks = true;
binder.addFrozenStateChangeCallback(executor, (who, state) -> {
    if (state == IBinder.FrozenStateChangeCallback.STATE_FROZEN) {
        shouldSendCallbacks = false;
    } else if (state == IBinder.FrozenStateChangeCallback.STATE_UNFROZEN) {
        shouldSendCallbacks = true;
    }
});

Ngoài ra, bạn có thể sử dụng RemoteCallbackList để đảm bảo không gửi lệnh gọi lại đến quy trình đích khi quy trình này bị đóng băng.

Ví dụ:

RemoteCallbackList<IInterface> rc =
        new RemoteCallbackList.Builder<IInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
                .setExecutor(executor)
                .build();
rc.register(callback);
rc.broadcast((callback) -> callback.foo(bar));

callback.foo() chỉ được gọi nếu quy trình không bị tạm ngưng.

Các ứng dụng thường lưu những nội dung cập nhật mà chúng nhận được bằng cách sử dụng các lệnh gọi lại dưới dạng ảnh chụp nhanh của trạng thái mới nhất. Hãy cân nhắc một API giả định để các ứng dụng theo dõi tỷ lệ phần trăm pin còn lại:

interface BatteryListener {
    void onBatteryPercentageChanged(int newPercentage);
}

Hãy xem xét trường hợp có nhiều sự kiện thay đổi trạng thái xảy ra khi ứng dụng bị đóng băng. Khi ứng dụng được giải phóng, bạn chỉ nên gửi trạng thái gần đây nhất đến ứng dụng và loại bỏ các thay đổi khác về trạng thái cũ. Quá trình phân phối này phải diễn ra ngay lập tức khi ứng dụng được giải phóng để ứng dụng có thể "bắt kịp". Bạn có thể thực hiện việc này như sau:

RemoteCallbackList<IInterface> rc =
        new RemoteCallbackList.Builder<IInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT)
                .setExecutor(executor)
                .build();
rc.register(callback);
rc.broadcast((callback) -> callback.onBatteryPercentageChanged(value));

Trong một số trường hợp, bạn có thể theo dõi giá trị cuối cùng được gửi đến ứng dụng để ứng dụng không cần được thông báo về cùng một giá trị sau khi được rã đông.

Trạng thái có thể được biểu thị dưới dạng dữ liệu phức tạp hơn. Hãy cân nhắc một API giả định để các ứng dụng được thông báo về giao diện mạng:

interface NetworkListener {
    void onAvailable(Network network);
    void onLost(Network network);
    void onChanged(Network network);
}

Khi tạm dừng thông báo cho một ứng dụng, bạn nên nhớ tập hợp các mạng và trạng thái mà ứng dụng đã thấy lần gần đây nhất. Khi tiếp tục, bạn nên thông báo cho ứng dụng về các mạng cũ đã bị mất, các mạng mới đã có sẵn và các mạng hiện có có trạng thái đã thay đổi – theo thứ tự này.

Không thông báo cho ứng dụng về những mạng đã được cung cấp rồi mất kết nối trong khi các lệnh gọi lại bị tạm dừng. Các ứng dụng không được nhận toàn bộ thông tin về các sự kiện đã xảy ra trong khi chúng bị đóng băng và tài liệu API không được hứa hẹn cung cấp luồng sự kiện liên tục bên ngoài các trạng thái vòng đời rõ ràng. Trong ví dụ này, nếu cần liên tục theo dõi tình trạng kết nối mạng, thì ứng dụng phải duy trì trạng thái vòng đời để ngăn ứng dụng bị lưu vào bộ nhớ đệm hoặc bị đóng băng.

Trong quá trình xem xét, bạn nên kết hợp các sự kiện đã xảy ra sau khi tạm dừng và trước khi tiếp tục thông báo, đồng thời cung cấp trạng thái mới nhất cho các lệnh gọi lại ứng dụng đã đăng ký một cách ngắn gọn.

Những điều cần cân nhắc đối với tài liệu dành cho nhà phát triển

Việc phân phối các sự kiện không đồng bộ có thể bị chậm trễ, có thể là do người gửi tạm dừng phân phối trong một khoảng thời gian như minh hoạ trong phần trước hoặc do ứng dụng nhận không nhận đủ tài nguyên thiết bị để xử lý sự kiện một cách kịp thời.

Ngăn nhà phát triển đưa ra giả định về khoảng thời gian giữa thời điểm ứng dụng của họ được thông báo về một sự kiện và thời điểm sự kiện đó thực sự xảy ra.

Kỳ vọng của nhà phát triển đối với việc tạm ngưng API

Những nhà phát triển quen thuộc với mô hình đồng thời có cấu trúc của Kotlin mong đợi các hành vi sau đây từ mọi API tạm ngưng:

Các hàm tạm ngưng phải hoàn tất mọi công việc liên quan trước khi trả về hoặc truyền

Kết quả của các thao tác không chặn được trả về dưới dạng giá trị trả về của hàm thông thường và các lỗi được báo cáo bằng cách gửi các ngoại lệ. (Điều này thường có nghĩa là các tham số gọi lại là không cần thiết.)

Hàm tạm ngưng chỉ được gọi các tham số gọi lại tại chỗ

Hàm tạm ngưng phải luôn hoàn tất mọi công việc liên quan trước khi trả về, vì vậy, hàm này không bao giờ được gọi một lệnh gọi lại đã cung cấp hoặc tham số hàm khác, hoặc giữ lại một tham chiếu đến lệnh gọi lại đó sau khi hàm tạm ngưng đã trả về.

Các hàm tạm ngưng chấp nhận tham số lệnh gọi lại phải giữ nguyên bối cảnh, trừ phi có tài liệu khác

Việc gọi một hàm trong hàm tạm ngưng sẽ khiến hàm đó chạy trong CoroutineContext của phương thức gọi. Vì các hàm tạm ngưng phải hoàn tất mọi công việc liên quan trước khi trả về hoặc truyền, và chỉ được gọi các tham số lệnh gọi lại tại chỗ, nên kỳ vọng mặc định là mọi lệnh gọi lại như vậy cũng chạy trên CoroutineContext gọi bằng trình điều phối được liên kết. Nếu mục đích của API là chạy một lệnh gọi lại bên ngoài CoroutineContext gọi, thì bạn phải ghi lại rõ ràng hành vi này.

Hàm tạm ngưng phải hỗ trợ việc huỷ tác vụ kotlinx.coroutines

Mọi hàm tạm ngưng được cung cấp đều phải phối hợp với việc huỷ công việc theo định nghĩa của kotlinx.coroutines. Nếu lệnh gọi của một thao tác đang diễn ra bị huỷ, thì hàm sẽ tiếp tục với một CancellationException càng sớm càng tốt để phương thức gọi có thể dọn dẹp và tiếp tục càng sớm càng tốt. Việc này được suspendCancellableCoroutine và các API tạm ngưng khác do kotlinx.coroutines cung cấp xử lý tự động. Thông thường, các hoạt động triển khai thư viện không nên sử dụng trực tiếp suspendCoroutine vì theo mặc định, hoạt động này không hỗ trợ hành vi huỷ.

Các hàm tạm ngưng thực hiện công việc chặn trên nền (luồng không phải là luồng chính hoặc luồng giao diện người dùng) phải cung cấp một cách để định cấu hình trình điều phối được dùng

Bạn không nên làm cho một hàm chặn tạm ngưng hoàn toàn để chuyển đổi luồng.

Việc gọi một hàm tạm ngưng không được dẫn đến việc tạo thêm các luồng mà không cho phép nhà phát triển cung cấp luồng hoặc nhóm luồng riêng để thực hiện công việc đó. Ví dụ: một hàm khởi tạo có thể chấp nhận một CoroutineContext được dùng để thực hiện công việc ở chế độ nền cho các phương thức của lớp.

Các hàm tạm ngưng sẽ chỉ chấp nhận một tham số CoroutineContext hoặc Dispatcher không bắt buộc để chuyển sang trình điều phối đó nhằm thực hiện công việc chặn thay vì hiển thị hàm chặn cơ bản và đề xuất rằng các nhà phát triển gọi sử dụng lệnh gọi riêng của họ đến withContext để chuyển công việc đến một trình điều phối đã chọn.

Các lớp khởi chạy coroutine

Các lớp khởi chạy coroutine phải có một CoroutineScope để thực hiện các thao tác khởi chạy đó. Việc tuân thủ các nguyên tắc về mô hình đồng thời có cấu trúc ngụ ý các mẫu cấu trúc sau đây để lấy và quản lý phạm vi đó.

Trước khi viết một lớp khởi chạy các tác vụ đồng thời vào một phạm vi khác, hãy cân nhắc các mẫu thay thế:

class MyClass {
    private val requests = Channel<MyRequest>(Channel.UNLIMITED)

    suspend fun handleRequests() {
        coroutineScope {
            for (request in requests) {
                // Allow requests to be processed concurrently;
                // alternatively, omit the [launch] and outer [coroutineScope]
                // to process requests serially
                launch {
                    processRequest(request)
                }
            }
        }
    }

    fun submitRequest(request: MyRequest) {
        requests.trySend(request).getOrThrow()
    }
}

Việc hiển thị một suspend fun để thực hiện công việc đồng thời cho phép phương thức gọi gọi thao tác trong ngữ cảnh riêng, loại bỏ nhu cầu MyClass quản lý CoroutineScope. Việc xử lý các yêu cầu tuần tự sẽ trở nên đơn giản hơn và trạng thái thường có thể tồn tại dưới dạng các biến cục bộ của handleRequests thay vì dưới dạng các thuộc tính lớp. Nếu không, bạn sẽ phải đồng bộ hoá thêm.

Các lớp quản lý coroutine phải hiển thị các phương thức đóng và huỷ

Các lớp khởi chạy coroutine dưới dạng chi tiết triển khai phải cung cấp một cách để tắt những tác vụ đồng thời đang diễn ra một cách rõ ràng để chúng không rò rỉ công việc đồng thời không kiểm soát vào một phạm vi mẹ. Thông thường, việc này có dạng tạo một Job con của một CoroutineContext được cung cấp:

private val myJob = Job(parent = `CoroutineContext`[Job])
private val myScope = CoroutineScope(`CoroutineContext` + myJob)

fun cancel() {
    myJob.cancel()
}

Bạn cũng có thể cung cấp phương thức join() để cho phép mã người dùng chờ hoàn thành mọi công việc đồng thời đang chờ xử lý do đối tượng thực hiện. (Điều này có thể bao gồm cả việc dọn dẹp bằng cách huỷ một thao tác.)

suspend fun join() {
    myJob.join()
}

Đặt tên cho thao tác trên thiết bị đầu cuối

Tên được dùng cho các phương thức tắt hoàn toàn các tác vụ đồng thời do một đối tượng sở hữu vẫn đang diễn ra phải phản ánh hợp đồng hành vi về cách tắt:

Sử dụng close() khi các thao tác đang diễn ra có thể hoàn tất nhưng không có thao tác mới nào có thể bắt đầu sau khi lệnh gọi đến close() trả về.

Sử dụng cancel() khi các thao tác đang diễn ra có thể bị huỷ trước khi hoàn tất. Không có thao tác mới nào có thể bắt đầu sau khi lệnh gọi đến cancel() trả về.

Hàm khởi tạo lớp chấp nhận CoroutineContext, không phải CoroutineScope

Khi các đối tượng bị cấm khởi chạy trực tiếp vào phạm vi mẹ được cung cấp, tính phù hợp của CoroutineScope dưới dạng một tham số của hàm khởi tạo sẽ bị phá vỡ:

// Don't do this
class MyClass(scope: CoroutineScope) {
    private val myJob = Job(parent = scope.`CoroutineContext`[Job])
    private val myScope = CoroutineScope(scope.`CoroutineContext` + myJob)

    // ... the [scope] constructor parameter is never used again
}

CoroutineScope trở thành một trình bao bọc không cần thiết và gây hiểu lầm. Trong một số trường hợp sử dụng, trình bao bọc này có thể được tạo chỉ để truyền dưới dạng một tham số hàm khởi tạo, chỉ để bị loại bỏ:

// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))

Các tham số CoroutineContext mặc định là EmptyCoroutineContext

Khi tham số CoroutineContext không bắt buộc xuất hiện trong một giao diện API, giá trị mặc định phải là Empty`CoroutineContext` sentinel. Điều này cho phép kết hợp tốt hơn các hành vi API, vì giá trị Empty`CoroutineContext` từ một phương thức gọi được xử lý theo cách tương tự như chấp nhận giá trị mặc định:

class MyOuterClass(
    `CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
    private val innerObject = MyInnerClass(`CoroutineContext`)

    // ...
}

class MyInnerClass(
    `CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
    private val job = Job(parent = `CoroutineContext`[Job])
    private val scope = CoroutineScope(`CoroutineContext` + job)

    // ...
}