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

API không chặn yêu cầu công việc diễn ra, sau đó trả lại quyền kiểm soát cho luồng 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 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, có sẵn tài nguyên hệ thống có mức độ cạnh tranh cao hoặc dữ liệu đầu vào của người dùng trước khi có thể tiếp tục công việc. Các API được thiết kế đặc biệt tốt cung cấp một cách để huỷ thao tác đang diễn ra và ngừng thực hiện công việc thay mặt cho phương thức gọi ban đầu, bảo toàn tình trạng hệ thống và thời lượng pin khi không cần thao tác 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 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 thao tác.

Có hai lý do chính để viế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 gọi cho đến khi thao tác hoàn tất.

Kotlin thúc đẩy mạnh mẽ 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 việc thực thi mã đồng bộ và không đồng bộ 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, thay vào đó, hãy trả về 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 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 về các yêu cầu tối thiểu mà nhà phát triển có thể giữ an toàn khi làm việc với các API không chặn và không đồng bộ, theo sau là một loạt công thức để tạo API đáp ứng các yêu cầu này bằng ngôn ngữ Kotlin hoặc Java, trong nền tảng Android hoặc thư viện Jetpack. Khi không chắc chắn, hãy xem xét các kỳ vọng của nhà phát triển dưới dạng 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 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ó ghi chú khác.

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

Nếu một API chấp nhận lệnh gọi lại không được ghi nhận là chỉ được gọi tại chỗ (tức là chỉ được luồng gọi gọi trước khi lệnh gọi API tự 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 nhận trong các phần sau.

Ví dụ về lệnh gọi lại chỉ được gọi tại chỗ là một hàm lọc hoặc ánh xạ bậc cao hơn gọi một hàm ánh xạ hoặc thuộc tính nhận định trên mỗi 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ộ không chặn và trả về nhanh chóng sau khi khởi tạo yêu cầu cho thao tác. Bạn luôn có thể gọi API không đồng bộ một cách an toàn bất cứ lúc nào và việc gọi API không đồng bộ không bao giờ dẫn đến tình trạng khung hình giật hoặc ANR.

Nhiều thao tác và tín hiệu vòng đời có thể được nền tảng hoặc thư viện kích hoạt theo yêu cầu, và việc kỳ vọng 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 hoạt động đo lường và bố cục View khi nội dung ứng dụng phải được điền sẵn để lấp đầy không gian 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à đây có thể là một đường dẫn mã quan trọng để tạo khung ảnh động không bị giật. Nhà phát triển phải luôn tự tin rằng việc gọi bất kỳ API không đồng bộ nào để phản hồi các loại lệnh gọi lại trong vòng đời này sẽ không gây ra hiện tượng giật khung hình.

Điều này có nghĩa là công việc do API không đồng bộ thực hiện trước khi trả về phải rất nhẹ; tạo bản ghi yêu cầu và lệnh gọi lại liên quan, đồng thời đăng ký yêu cầu đó với công cụ thực thi thực hiện công việc nhiều nhất. Nếu việc đăng ký một thao tác không đồng bộ yêu cầu IPC, thì việc 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 của nhà phát triển này. Thông tin này có thể bao gồm một hoặc nhiều thông tin 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 phải có khoá có mức độ tranh chấp cao
  • Đăng yêu cầu lên luồng worker trong quy trình ứng dụng để thực hiện đăng ký chặn qua IPC

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

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 được 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.

API không đồng bộ có thể kiểm tra đối số 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 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 để đảm bảo tham số nằm trong phạm vi này và gửi IllegalArgumentException nếu tham số nằm ngoài phạm vi hoặc có thể kiểm tra String ngắn để đảm bảo tuân thủ định dạng hợp lệ, chẳng hạn như chỉ 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ả lỗi khác phải được báo cáo cho lệnh gọi lại được 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
  • Trường hợp ngoại lệ về bảo mật khi 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
  • Phần cứng bắt buộc đã bị ngắt kết nối
  • Lỗi mạng
  • Số lần hết thời gian chờ
  • Binder bị lỗi hoặc không có quy trình từ xa

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 một thao tác đang chạy biết 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 hai điều:

Bạn nên giải phóng các tệp tham chiếu cứng đến lệnh gọi lại do phương thức gọi cung cấp

Lệnh gọi lại được cung cấp cho các API không đồng bộ có thể chứa tham chiếu cứng đến các biểu đồ đối tượng lớn và công việc đang diễn ra giữ tham chiếu cứng đến lệnh gọi lại đó có thể ngăn các biểu đồ đối tượng đó bị thu gom rác. Bằng cách giải phóng các tệp 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 khi công việc được phép chạy cho đến khi hoàn tất.

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

Công việc 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 phương thức gọi báo hiệu khi công việc này không còn cần thiết, 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.

Những đ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ế API không đồng bộ, trong đó lệnh gọi lại bắt nguồn từ một quy trình hệ thống và được phân phố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 ứ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 lưu trữ ứng dụng vào bộ nhớ đệm: quy trình của ứ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 hiển thị với người dùng, chẳng hạn như 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 ứng dụng, 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 việc 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 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 công việc 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ị treo. Khi bị treo, ứng dụng sẽ không nhận được thời gian CPU và không thể thực hiện bất kỳ công việc nào. Mọi lệnh gọi đến lệnh gọi lại đã đăng ký của ứng dụng đó đều được lưu vào bộ đệm và phân phối khi ứng dụng được huỷ trạng thái đóng băng.

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

Đang được xem xét:

  • Bạn nên xem xét tạm dừng việc 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 việc gửi lệnh gọi lại ứng dụng trong khi quy trình của ứng dụng bị treo.

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ị treo hoặc được bỏ treo:

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 việc 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 bị treo, 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 lệnh gọi lại đã đăng ký của ứng dụng sau khi ứng dụng thoát khỏi trạng thái tương ứng cho đến khi ứng dụng đã huỷ đăng ký lệnh gọi lại hoặc quy trình ứng dụng bị tắt.

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 để không phân phối lệnh gọi lại cho quy trình mục tiêu khi quy trình đó bị treo.

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ị treo.

Các ứng dụng thường lưu nội dung cập nhật mà chúng nhận được bằng cách sử dụng 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 xem xét một API giả định cho 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 nhiều sự kiện thay đổi trạng thái xảy ra khi một ứng dụng bị treo. Khi ứng dụng được huỷ trạng thái đóng băng, bạn chỉ nên phân phối trạng thái gần đây nhất cho ứng dụng và loại bỏ các thay đổi trạng thái cũ khác. Quá trình phân phối này sẽ diễn ra ngay lập tức khi ứng dụng được huỷ trạng thái đóng bă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 phân phối cho ứ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 huỷ trạng thái đóng bă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 xem xét 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 ghi nhớ tập hợp mạng và trạng thái mà ứng dụng đã xem 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ũ đã 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ề các mạng đã được cung cấp rồi bị mất trong khi các lệnh gọi lại bị tạm dừng. Ứ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 bị đóng băng và tài liệu API không được hứa hẹn sẽ phân phối 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 ứng dụng cần liên tục theo dõi khả năng sử dụng mạng, thì ứng dụng đó phải duy trì trạng thái vòng đời để không bị lưu vào bộ nhớ đệm hoặc bị treo.

Trong quá trình xem xét, bạn nên hợp nhất 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 phân phối 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 khi viết tài liệu dành cho nhà phát triển

Quá trình phân phối sự kiện không đồng bộ có thể bị trì hoãn, do trình gửi tạm dừng quá trình phân phối trong một khoảng thời gian như đã trình bày 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.

Không khuyến khích 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 tính năng đồng thời có cấu trúc của Kotlin sẽ thấy các hành vi sau đây từ mọi API tạm ngưng:

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 gửi

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ề hàm thông thường và lỗi được báo cáo bằng cách gửi ngoại lệ. (Điều này thường có nghĩa là các tham số gọi lại 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 được gọi lệnh gọi lại đã cung cấp hoặc tham số hàm khác hoặc giữ lại tham chiếu đến hàm đó sau khi hàm tạm ngưng trả về.

Các hàm tạm ngưng chấp nhận tham số gọi lại phải giữ lại ngữ cảnh, trừ phi có ghi nhận 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 tất cả công việc liên quan trước khi trả về hoặc gửi và chỉ nên gọi các tham số gọi lại tại chỗ, nên dự kiến 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 liên kết. Nếu mục đích của API là chạy lệnh gọi lại bên ngoài CoroutineContext gọi, thì hành vi này phải được ghi lại rõ ràng.

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

Mọi hàm tạm ngưng được cung cấp phải phối hợp với việc huỷ công việc như được xác định bởi kotlinx.coroutines. Nếu công việc gọi của một thao tác đang diễn ra bị huỷ, hàm sẽ tiếp tục bằng 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 tự động xử lý. Các hoạt động triển khai thư viện thường không nên sử dụng trực tiếp suspendCoroutine, vì theo mặc định, thư viện này không hỗ trợ hành vi huỷ này.

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

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

Việc gọi một hàm tạm ngưng không được tạo ra các luồng bổ sung mà không cho phép nhà phát triển cung cấp luồng hoặc nhóm luồng của riêng họ để thực hiện công việc đó. Ví dụ: một hàm khởi tạo có thể chấp nhận CoroutineContext 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 chỉ chấp nhận 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 nên hiển thị hàm chặn cơ bản và đề xuất rằng các nhà phát triển gọi nên sử dụng lệnh gọi của riêng họ đến withContext để chuyển công việc đến một trình điều phối đã chọn.

Các lớp chạy coroutine

Các lớp khởi chạy coroutine phải có 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 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ị 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 của riêng phương thức đó, không cần phải có MyClass quản lý CoroutineScope. Việc chuyển đổi tuần tự quá trình xử lý yêu cầu 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 biến cục bộ của handleRequests thay vì dưới dạng thuộc tính lớp, nếu không sẽ yêu cầu thêm tính năng đồng bộ hoá.

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 thông tin triển khai phải cung cấp một cách để tắt sạch các tác vụ đồng thời đang diễn ra đó để chúng không rò rỉ công việc đồng thời không được kiểm soát vào phạm vi mẹ. Thông thường, việc này sẽ diễn ra dưới 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ã của người dùng chờ hoàn tất mọi công việc đồng thời đang được đối tượng thực hiện. (Điều này có thể bao gồm cả công việc dọn dẹp được thực hiện 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 dùng cho các phương thức tắt sạch các tác vụ đồng thời do một đối tượng đang trong quá trình thực hiện sở hữu 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 thể bắt đầu thao tác mới sau khi lệnh gọi đến cancel() trả về.

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

Khi các đối tượng bị cấm khởi chạy trực tiếp vào một phạm vi mẹ được cung cấp, tính phù hợp của CoroutineScope dưới dạng tham số 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 tham số hàm khởi tạo, sau đó 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 một 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à trình cảnh báo Empty`CoroutineContext`. Điều này cho phép kết hợp các hành vi API tốt hơn, vì giá trị Empty`CoroutineContext` từ phương thức gọi được xử lý giống như cách 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)

    // ...
}