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 xảy ra rồi nhường quyền kiểm soát lại cho luồng gọi để luồng đó 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, tính khả dụng của các tài nguyên hệ thống có tính cạnh tranh cao hoặc hoạt động đầ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ừng thực hiện công việc thay cho người 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 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 thực hiện thao tác.

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

  • Thực thi nhiều thao tác đồng thời, 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 khuyến khích mạnh mẽ mô hình đồng thời có cấu trúc, một loạt 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 rời 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:

  • Không chặn luồng gọi mà thay vào đó nhường luồng thực thi làm 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ề mức 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ông thức để tạo API đáp ứng các kỳ vọng 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 coi kỳ vọng của nhà phát triển là yêu cầu đối với mọi giao diện 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 theo 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 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ề lệnh gọi lại chỉ được gọi tại chỗ là hàm lọc hoặc hàm ánh xạ 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 đợi các API không đồng bộ là không chặn và trả về nhanh chóng sau khi bắt đầu yêu cầu thao tác. Bạn luôn có thể gọi API không đồng bộ bất cứ lúc nào và việc gọi API không đồng bộ không bao giờ dẫn đến khung hình bị giật hoặc ANR.

Nền tảng hoặ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 và việc yêu cầu nhà phát triển nắm giữ kiến thức chung 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 việc đo lường và bố cục View khi nội dung ứng dụng phải được điền để lấp đầy không gian có sẵn (chẳng hạn như RecyclerView). LifecycleObserver phản hồi phương thức gọi lại trong vòng đời onStart của fragment 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 khung hình ả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 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 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 đượ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 nhiều nhất. Nếu việc đăng ký 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 trình liên kết một chiều
  • Thực hiện lệnh gọi trình 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ó tính cạnh tranh 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ề void và chỉ gửi ngoại lệ cho các đố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 các đối số cho giá trị rỗng và gửi NullPointerException hoặc kiểm tra xem các đối số được cung cấp có nằm trong phạm vi hợp lệ hay không 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 này 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ủ định dạng hợp lệ, chẳng hạn như chỉ gồm 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 lượt kiểm tra này trong chính dịch vụ hệ thống.)

Tất cả cá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 ở:

  • Lỗi thiết bị đầu cuối của thao tác được yêu cầu
  • Ngoại lệ bảo mật đối với việc thiế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 bị tạm ngừng
  • Trình liên kết bị buộc tắt hoặc quy trình từ xa không dùng được

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

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

Các tham chiếu cứng đến lệnh gọi lại do phương thức 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 API không đồng bộ có thể chứa các 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 thập 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 thập 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 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ể tốn nhiều chi phí về mức tiêu thụ năng lượng hoặc cá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 không còn 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 các ứ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ộ, trong đó các lệnh gọi lại bắt nguồn 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 quy trình ứng dụng chuyển sang trạng thái đã lưu vào bộ nhớ đệm, điều này có nghĩa 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ư hoạt động và dịch vụ. Ứng dụng được lưu trong bộ nhớ trong trường hợp ứng dụng đó lại xuất hiện với người dùng, nhưng trong khi đó 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 công việc trong các quy trình ứng dụng được 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 một ứng dụng bị đóng băng, ứng dụng đó sẽ nhận được thời gian CPU bằng 0 và không thể thực hiện bất kỳ công việ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ộ đệm và gửi khi ứng dụng được bỏ đó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ể bị lỗi thời vào thời điểm ứng dụng được bỏ đóng băng và xử lý các giao dịch đó. Bộ đệm là hữu 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 lỗi thời hoặc làm tràn bộ đệm của chúng, đừng gửi các lệnh gọi lại ứng dụng trong khi quy 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 các 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 bỏ đóng băng:

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

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

Cho dù bạ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 hay trạng thái bị đóng băng, 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 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ị buộc 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 để đảm bảo không gửi lệnh gọi lại đến quy trình mục tiêu khi quy trình đó 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ị đóng băng.

Các ứng dụng thường lưu các bản cập nhật mà chúng nhận được bằ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 cân nhắc một API giả định để các ứng dụng theo dõi phần trăm pin còn lại:

interface BatteryListener {
    void onBatteryPercentageChanged(int newPercentage);
}

Hãy cân nhắc 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ị đóng băng. Khi ứng dụng được bỏ đóng bă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 trạng thái lỗi thời khác. Quá trình gửi này phải diễn ra ngay lập tức khi ứng dụng được bỏ đó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 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 bỏ đó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 cân nhắc một API giả định để các ứng dụng được thông báo về các 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ó và các mạng hiện có có trạng thái đã thay đổi – theo thứ tự này.

Đừng thông báo cho ứng dụng về các mạng đã có rồi bị mất 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ộ tài khoản 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 gửi luồng sự kiện không bị gián đoạn 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 tính khả dụng của mạng, thì ứng dụng đó phải ở trạng thái vòng đời giúp ứng dụng khô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 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 gửi trạng thái mới nhất đến 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 gửi các sự kiện không đồng bộ có thể bị trì hoãn, có thể là do người gửi đã tạm dừng gửi trong một khoảng thời gian như 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.

Khuyến khích nhà phát triển không đưa ra giả định về 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 các API tạm ngưng

Các 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:

Hàm tạm ngưng phải hoàn tất mọi công việc được liên kết 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ề của 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ố lệnh 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ố lệnh 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 được liên kết trước khi trả về, vì vậy, chúng không bao giờ được gọi lệnh gọi lại được cung cấp hoặc tham số hàm khác hoặc giữ lại 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ó ghi lại 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 được liên kết trước khi trả về hoặc gửi 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 được 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 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ợ 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 thao tác đang diễn ra bị huỷ, thì hàm phải tiếp tục với 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. suspendCancellableCoroutine và các API tạm ngưng khác do kotlinx.coroutines cung cấp sẽ tự động xử lý việc này. Quá trình triển khai thư viện thường không được 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 trên nền (luồng 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 làm cho hàm chặn tạm ngưng hoàn toàn để chuyển đổi luồng.

Việc gọi hàm tạm ngưng không được dẫn đến việc tạo thêm 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ụ: hàm khởi tạo có thể chấp nhận CoroutineContext được dùng để thực hiện công việc nền cho các phương thức của lớp.

Các hàm tạm ngưng chấp nhận tham số CoroutineContext hoặc Dispatcher không bắt buộc chỉ để chuyển sang trình điều phối đó để 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 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 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ó 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 đồ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ị 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 bối cảnh riêng của chúng, loại bỏ nhu cầu MyClass quản lý CoroutineScope. Việc tuần tự hoá quá trình xử lý các 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 các biến cục bộ của handleRequests thay vì dưới dạng các thuộc tính lớp mà nếu không thì sẽ yêu cầu đồng bộ hoá bổ sung.

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 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ẽ tạo Job con của 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 tất 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ông việc dọn dẹp được thực hiện bằng cách huỷ thao tác.)

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

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

Tên được dùng cho các phương thức tắt sạch các tác vụ đồng thời do đố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 tham số hàm khởi tạo sẽ bị hỏng:

// 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 trình bao bọc không cần thiết và gây hiểu lầm mà trong một số trường hợp sử dụng, có thể chỉ được tạo để truyền dưới dạng 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))

Tham số CoroutineContext mặc định là EmptyCoroutineContext

Khi tham số CoroutineContext không bắt buộc xuất hiện trong 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ừ 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)

    // ...
}