Nicht blockierende APIs fordern die Ausführung einer Aufgabe an und geben dann die Steuerung an den aufrufenden Thread zurück, damit dieser andere Aufgaben ausführen kann, bevor der angeforderte Vorgang abgeschlossen ist. Diese APIs sind nützlich, wenn die angeforderte Aufgabe möglicherweise noch läuft oder auf den Abschluss von E/A- oder IPC-Vorgängen, die Verfügbarkeit von stark umkämpften Systemressourcen oder Nutzereingaben gewartet werden muss, bevor die Aufgabe fortgesetzt werden kann. Gut konzipierte APIs bieten die Möglichkeit, den laufenden Vorgang abzubrechen und die Ausführung von Aufgaben im Namen des ursprünglichen Aufrufers zu beenden. So werden die Systemleistung und die Akkulaufzeit geschont, wenn der Vorgang nicht mehr benötigt wird.
Asynchrone APIs sind eine Möglichkeit, nicht blockierendes Verhalten zu erreichen. Asynchrone APIs akzeptieren eine Form der Fortsetzung oder des Rückrufs, die benachrichtigt wird, wenn der Vorgang abgeschlossen ist oder andere Ereignisse während des Fortschritts des Vorgangs auftreten.
Es gibt zwei Hauptgründe für die Entwicklung einer asynchronen API:
- Gleichzeitige Ausführung mehrerer Vorgänge, wobei der N-te Vorgang gestartet werden muss, bevor der N-1-te Vorgang abgeschlossen ist.
- Blockieren eines aufrufenden Threads wird vermieden, bis ein Vorgang abgeschlossen ist.
Kotlin unterstützt strukturierte Nebenläufigkeit, eine Reihe von Prinzipien und APIs, die auf suspend-Funktionen basieren und die synchrone und asynchrone Ausführung von Code vom Thread-Blocking-Verhalten entkoppeln. Suspend-Funktionen sind nicht blockierend und synchron.
Funktionen sperren:
- Blockieren Sie den Aufruf-Thread nicht, sondern geben Sie den Ausführungs-Thread als Implementierungsdetail frei, während Sie auf die Ergebnisse von Vorgängen warten, die an anderer Stelle ausgeführt werden.
- Synchron ausführen und nicht erfordern, dass der Aufrufer einer nicht blockierenden API gleichzeitig mit nicht blockierenden Vorgängen, die durch den API-Aufruf initiiert werden, weiter ausgeführt wird.
Auf dieser Seite wird eine Mindestbasislinie für Erwartungen beschrieben, die Entwickler bei der Arbeit mit nicht blockierenden und asynchronen APIs sicher haben können. Anschließend werden eine Reihe von Rezepten für die Entwicklung von APIs vorgestellt, die diese Erwartungen in Kotlin oder Java auf der Android-Plattform oder in Jetpack-Bibliotheken erfüllen. Im Zweifelsfall sollten Sie die Erwartungen der Entwickler als Anforderungen für jede neue API-Oberfläche betrachten.
Erwartungen von Entwicklern an asynchrone APIs
Die folgenden Erwartungen werden, sofern nicht anders angegeben, aus der Sicht von APIs formuliert, die nicht gesperrt werden.
APIs, die Callbacks akzeptieren, sind in der Regel asynchron.
Wenn eine API einen Callback akzeptiert, der nicht dokumentiert ist, um nur in-place aufgerufen zu werden (d. h. nur vom aufrufenden Thread, bevor der API-Aufruf selbst zurückgegeben wird), wird davon ausgegangen, dass die API asynchron ist. Diese API muss allen anderen Erwartungen entsprechen, die in den folgenden Abschnitten dokumentiert sind.
Ein Beispiel für einen Callback, der nur an Ort und Stelle aufgerufen wird, ist eine Map- oder Filterfunktion höherer Ordnung, die vor der Rückgabe einen Mapper oder ein Prädikat für jedes Element in einer Sammlung aufruft.
Asynchrone APIs sollten so schnell wie möglich zurückgegeben werden
Entwickler erwarten, dass asynchrone APIs nicht blockierend sind und schnell zurückgegeben werden, nachdem die Anfrage für den Vorgang initiiert wurde. Es sollte immer sicher sein, eine asynchrone API jederzeit aufzurufen. Der Aufruf einer asynchronen API sollte niemals zu ruckelnden Frames oder ANR-Fehlern führen.
Viele Vorgänge und Lebenszyklussignale können von der Plattform oder den Bibliotheken bei Bedarf ausgelöst werden. Es ist nicht zumutbar, dass Entwickler alle potenziellen Aufrufstellen für ihren Code kennen. Beispiel: Ein Fragment
kann in einer synchronen Transaktion als Reaktion auf die Messung und das Layout von View
zum FragmentManager
hinzugefügt werden, wenn App-Inhalte eingefügt werden müssen, um den verfügbaren Platz zu füllen (z. B. RecyclerView
). Ein LifecycleObserver
, der auf den onStart
-Lebenszyklus-Callback dieses Fragments reagiert, kann hier einmalige Startvorgänge ausführen. Dies kann auf einem kritischen Codepfad für die Erstellung eines ruckelfreien Animationsframes erfolgen. Entwickler sollten sich immer darauf verlassen können, dass der Aufruf einer beliebigen asynchronen API als Reaktion auf diese Art von Lebenszyklus-Callbacks nicht zu einem ruckeligen Frame führt.
Das bedeutet, dass die Arbeit, die von einer asynchronen API vor der Rückgabe ausgeführt wird, sehr gering sein muss. Sie darf höchstens einen Datensatz der Anfrage und des zugehörigen Callbacks erstellen und bei der Ausführungs-Engine registrieren, die die Arbeit ausführt. Wenn für die Registrierung für einen asynchronen Vorgang IPC erforderlich ist, sollte die Implementierung der API alle erforderlichen Maßnahmen ergreifen, um diese Erwartung des Entwicklers zu erfüllen. Dazu gehören möglicherweise:
- Zugrunde liegende IPC als Oneway-Binder-Aufruf implementieren
- Ein bidirektionaler Binder-Aufruf an den Systemserver, bei dem für die Registrierung kein stark umkämpfter Lock erforderlich ist
- Die Anfrage wird an einen Worker-Thread im App-Prozess gesendet, um eine blockierende Registrierung über IPC durchzuführen.
Asynchrone APIs sollten „void“ zurückgeben und nur bei ungültigen Argumenten eine Ausnahme auslösen.
Asynchrone APIs sollten alle Ergebnisse des angeforderten Vorgangs an den bereitgestellten Callback melden. So kann der Entwickler einen einzigen Codepfad für die Erfolgs- und Fehlerbehandlung implementieren.
Bei asynchronen APIs kann geprüft werden, ob Argumente null sind, und NullPointerException
ausgegeben werden. Außerdem kann geprüft werden, ob die angegebenen Argumente in einem gültigen Bereich liegen, und IllegalArgumentException
ausgegeben werden. Bei einer Funktion, die beispielsweise eine float
im Bereich von 0
bis 1f
akzeptiert, kann geprüft werden, ob der Parameter in diesem Bereich liegt. Wenn nicht, wird IllegalArgumentException
ausgegeben. Bei einer kurzen String
kann geprüft werden, ob sie einem gültigen Format entspricht, z. B. ob sie nur alphanumerische Zeichen enthält. Der Systemserver sollte dem App-Prozess niemals vertrauen. Jeder Systemdienst sollte diese Prüfungen im Systemdienst selbst duplizieren.)
Alle anderen Fehler sollten an den bereitgestellten Callback gemeldet werden. Dazu zählt unter anderem Folgendes:
- Endgültiger Fehler beim angeforderten Vorgang
- Sicherheitsausnahmen für fehlende Autorisierung oder Berechtigungen, die zum Ausführen des Vorgangs erforderlich sind
- Kontingent für den Vorgang überschritten
- Der App-Prozess ist nicht ausreichend im Vordergrund, um den Vorgang auszuführen.
- Erforderliche Hardware wurde getrennt
- Netzwerkfehler
- Zeitüberschreitungen
- Binder-Fehler oder nicht verfügbarer Remote-Prozess
Asynchrone APIs sollten einen Mechanismus zum Abbrechen von Vorgängen bieten
Asynchrone APIs sollten eine Möglichkeit bieten, einem laufenden Vorgang mitzuteilen, dass der Aufrufer sich nicht mehr für das Ergebnis interessiert. Durch diesen Abbruchvorgang sollten zwei Dinge signalisiert werden:
Feste Verweise auf vom Anrufer bereitgestellte Callbacks sollten freigegeben werden.
An asynchrone APIs übergebene Callbacks können Hard-Verweise auf große Objektgraphen enthalten. Wenn laufende Prozesse einen Hard-Verweis auf diesen Callback enthalten, kann das dazu führen, dass diese Objektgraphen nicht per Garbage Collection entfernt werden. Durch das Freigeben dieser Callback-Referenzen beim Abbrechen können diese Objektgraphen viel früher als bei einem vollständigen Abschluss der Aufgabe für die Garbage Collection infrage kommen.
Die Ausführungs-Engine, die die Arbeit für den Aufrufer ausführt, kann diese Arbeit beenden.
Arbeit, die durch asynchrone API-Aufrufe initiiert wird, kann einen hohen Stromverbrauch oder andere Systemressourcen verursachen. APIs, mit denen Anrufer signalisieren können, wenn diese Arbeit nicht mehr erforderlich ist, ermöglichen es, die Arbeit zu beenden, bevor weitere Systemressourcen verbraucht werden.
Besondere Überlegungen für Apps im Cache oder eingefrorene Apps
Wenn Sie asynchrone APIs entwerfen, bei denen Callbacks von einem Systemprozess stammen und an Apps gesendet werden, sollten Sie Folgendes beachten:
- Prozesse und App-Lebenszyklus: Der Prozess der Empfänger-App befindet sich möglicherweise im Cache.
- Zwischenspeicher für eingefrorene Apps: Der Prozess der Empfänger-App ist möglicherweise eingefroren.
Wenn ein App-Prozess in den Cache-Status wechselt, bedeutet das, dass er keine für Nutzer sichtbaren Komponenten wie Aktivitäten und Dienste mehr aktiv hostet. Die App wird im Arbeitsspeicher behalten, falls sie wieder für den Nutzer sichtbar wird. In der Zwischenzeit sollte sie jedoch keine Aufgaben ausführen. In den meisten Fällen sollten Sie das Senden von App-Callbacks pausieren, wenn die App in den Cache-Status wechselt, und fortsetzen, wenn die App den Cache-Status verlässt, um keine Arbeit in zwischengespeicherten App-Prozessen zu verursachen.
Eine im Cache gespeicherte App kann auch eingefroren werden. Wenn eine App eingefroren ist, erhält sie keine CPU-Zeit und kann keine Aufgaben ausführen. Alle Aufrufe der registrierten Callbacks dieser App werden gepuffert und zugestellt, wenn die App wieder aktiviert wird.
Gepufferte Transaktionen für App-Rückrufe sind möglicherweise veraltet, wenn die App reaktiviert wird und sie verarbeitet. Der Puffer ist endlich und würde bei einem Überlauf zum Absturz der Empfänger-App führen. Um zu verhindern, dass Apps mit veralteten Ereignissen überlastet werden oder ihre Puffer überlaufen, sollten Sie keine App-Callbacks senden, während der Prozess der App eingefroren ist.
Wird überprüft:
- Sie sollten in Erwägung ziehen, das Senden von App-Callbacks zu pausieren, während der Prozess der App im Cache gespeichert ist.
- Sie MÜSSEN das Senden von App-Callbacks unterbrechen, während der Prozess der App eingefroren ist.
Zustands-Tracking
So erfassen Sie, wann Apps in den Cache-Zustand wechseln oder ihn verlassen:
mActivityManager.addOnUidImportanceListener(
new UidImportanceListener() { ... },
IMPORTANCE_CACHED);
So sehen Sie, wann Apps eingefroren oder aufgetaut werden:
IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);
Strategien zum Fortsetzen des Versendens von App-Callbacks
Unabhängig davon, ob Sie das Senden von App-Callbacks pausieren, wenn die App in den Cache- oder den eingefrorenen Status wechselt, sollten Sie das Senden der registrierten Callbacks der App fortsetzen, sobald die App den jeweiligen Status verlässt, bis die App den Callback abgemeldet hat oder der App-Prozess beendet wird.
Beispiel:
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;
}
});
Alternativ können Sie RemoteCallbackList
verwenden. Dadurch werden keine Callbacks an den Zielprozess gesendet, wenn er eingefroren ist.
Beispiel:
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()
wird nur aufgerufen, wenn der Prozess nicht eingefroren ist.
Apps speichern Updates, die sie über Callbacks erhalten haben, häufig als Snapshot des aktuellen Status. Stellen Sie sich eine hypothetische API für Apps vor, mit der der verbleibende Akkustand in Prozent überwacht werden kann:
interface BatteryListener {
void onBatteryPercentageChanged(int newPercentage);
}
Stellen Sie sich vor, dass mehrere Ereignisse zum Ändern des Status auftreten, wenn eine App eingefroren ist. Wenn die App reaktiviert wird, sollten Sie nur den aktuellen Status an die App senden und andere veraltete Statusänderungen verwerfen. Diese Übermittlung sollte sofort erfolgen, wenn die App reaktiviert wird, damit sie „aufholen“ kann. Das geht so:
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));
In einigen Fällen können Sie den letzten an die App gesendeten Wert erfassen, damit die App nicht über denselben Wert benachrichtigt werden muss, sobald sie wieder aktiv ist.
Der Status kann als komplexere Daten ausgedrückt werden. Angenommen, es gibt eine hypothetische API, über die Apps über Netzwerkschnittstellen benachrichtigt werden:
interface NetworkListener {
void onAvailable(Network network);
void onLost(Network network);
void onChanged(Network network);
}
Wenn Sie Benachrichtigungen für eine App pausieren, sollten Sie sich die Netzwerke und Status merken, die die App zuletzt gesehen hat. Nach dem Fortsetzen wird empfohlen, die App in dieser Reihenfolge über alte Netzwerke, die verloren gegangen sind, über neue Netzwerke, die verfügbar geworden sind, und über vorhandene Netzwerke zu benachrichtigen, deren Status sich geändert hat.
Die App wird nicht über Netzwerke benachrichtigt, die verfügbar waren und dann verloren gegangen sind, während die Callbacks pausiert wurden. Apps sollten keine vollständige Liste der Ereignisse erhalten, die während des Einfrierens aufgetreten sind. Außerdem sollte in der API-Dokumentation nicht versprochen werden, dass Ereignisstreams außerhalb expliziter Lebenszyklusstatus ununterbrochen bereitgestellt werden. Wenn die App in diesem Beispiel die Netzwerkverfügbarkeit kontinuierlich überwachen muss, muss sie in einem Lebenszyklusstatus verbleiben, der verhindert, dass sie im Cache gespeichert oder eingefroren wird.
Während der Überprüfung sollten Sie Ereignisse, die nach dem Pausieren und vor dem Fortsetzen von Benachrichtigungen aufgetreten sind, zusammenfassen und den neuesten Status prägnant an die registrierten App-Callbacks übermitteln.
Überlegungen zur Entwicklerdokumentation
Die Zustellung asynchroner Ereignisse kann sich verzögern, entweder weil der Absender die Zustellung für einen bestimmten Zeitraum pausiert hat (siehe vorheriger Abschnitt) oder weil die Empfänger-App nicht genügend Geräteressourcen erhalten hat, um das Ereignis rechtzeitig zu verarbeiten.
Entwickler sollten nicht davon ausgehen, dass die Benachrichtigung ihrer App über ein Ereignis und das tatsächliche Eintreten des Ereignisses gleichzeitig erfolgen.
Erwartungen von Entwicklern bei der Sperrung von APIs
Entwickler, die mit der strukturierten Nebenläufigkeit von Kotlin vertraut sind, erwarten die folgenden Verhaltensweisen von jeder suspendierenden API:
Suspend-Funktionen sollten alle zugehörigen Aufgaben erledigen, bevor sie zurückkehren oder eine Ausnahme auslösen.
Ergebnisse von nicht blockierenden Vorgängen werden als normale Funktionsrückgabewerte zurückgegeben und Fehler werden durch das Auslösen von Ausnahmen gemeldet. (Das bedeutet oft, dass keine Callback-Parameter erforderlich sind.)
Suspend-Funktionen sollten Callback-Parameter nur direkt aufrufen.
Suspend-Funktionen sollten immer alle zugehörigen Aufgaben erledigen, bevor sie zurückkehren. Sie sollten daher niemals einen bereitgestellten Callback oder einen anderen Funktionsparameter aufrufen oder eine Referenz darauf beibehalten, nachdem die Suspend-Funktion zurückgekehrt ist.
Suspend-Funktionen, die Callback-Parameter akzeptieren, sollten den Kontext beibehalten, sofern nicht anders dokumentiert.
Wenn Sie eine Funktion in einer suspend-Funktion aufrufen, wird sie im CoroutineContext
des Aufrufers ausgeführt. Da suspend-Funktionen alle zugehörigen Aufgaben erledigen sollten, bevor sie zurückkehren oder eine Ausnahme auslösen, und Callback-Parameter nur an Ort und Stelle aufrufen sollten, wird standardmäßig erwartet, dass alle diese Callbacks auch auf dem aufrufenden CoroutineContext
mit dem zugehörigen Dispatcher ausgeführt werden. Wenn der Zweck der API darin besteht, einen Callback außerhalb des aufrufenden CoroutineContext
auszuführen, sollte dieses Verhalten klar dokumentiert werden.
Suspend-Funktionen sollten die Job-Abbruchfunktion von kotlinx.coroutines unterstützen
Jede angebotene Suspend-Funktion sollte mit der Job-Abbruchfunktion gemäß kotlinx.coroutines
zusammenarbeiten. Wenn der Aufrufjob eines laufenden Vorgangs abgebrochen wird, sollte die Funktion so schnell wie möglich mit einem CancellationException
fortgesetzt werden, damit der Aufrufer so schnell wie möglich aufräumen und fortfahren kann. Dies wird automatisch von suspendCancellableCoroutine
und anderen von kotlinx.coroutines
angebotenen APIs zur Kontosperrung übernommen. Bibliotheksimplementierungen sollten suspendCoroutine
im Allgemeinen nicht direkt verwenden, da dieses Verhalten für die Abbrechen-Funktion standardmäßig nicht unterstützt wird.
Suspend-Funktionen, die blockierende Vorgänge in einem Hintergrundthread (nicht im Haupt- oder UI-Thread) ausführen, müssen eine Möglichkeit bieten, den verwendeten Dispatcher zu konfigurieren.
Es wird nicht empfohlen, dass eine blockierende Funktion vollständig angehalten wird, um den Thread zu wechseln.
Das Aufrufen einer suspend-Funktion sollte nicht zur Erstellung zusätzlicher Threads führen, ohne dass der Entwickler einen eigenen Thread oder Thread-Pool für diese Aufgabe bereitstellen kann. Ein Konstruktor kann beispielsweise ein CoroutineContext
akzeptieren, das verwendet wird, um Hintergrundarbeiten für die Methoden der Klasse auszuführen.
Suspend-Funktionen, die einen optionalen CoroutineContext
- oder Dispatcher
-Parameter akzeptieren, nur um zu diesem Dispatcher zu wechseln, um blockierende Vorgänge auszuführen, sollten stattdessen die zugrunde liegende blockierende Funktion verfügbar machen und empfehlen, dass aufrufende Entwickler ihren eigenen Aufruf von withContext verwenden, um die Arbeit an einen ausgewählten Dispatcher zu leiten.
Klassen, die Coroutinen starten
Klassen, die Coroutinen starten, müssen ein CoroutineScope
haben, um diese Startvorgänge auszuführen. Wenn Sie die Prinzipien der strukturierten Nebenläufigkeit beachten, ergeben sich die folgenden strukturellen Muster für das Abrufen und Verwalten dieses Bereichs.
Bevor Sie eine Klasse schreiben, die gleichzeitige Aufgaben in einem anderen Bereich startet, sollten Sie alternative Muster in Betracht ziehen:
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()
}
}
Durch die Bereitstellung einer suspend fun
für die gleichzeitige Ausführung von Aufgaben kann der Aufrufer den Vorgang im eigenen Kontext aufrufen. Dadurch ist es nicht mehr erforderlich, dass MyClass
eine CoroutineScope
verwaltet. Die Verarbeitung von Anfragen wird einfacher und der Status kann oft als lokale Variablen von handleRequests
anstelle von Klassenattributen gespeichert werden, die ansonsten eine zusätzliche Synchronisierung erfordern würden.
Klassen, die Coroutinen verwalten, sollten die Methoden „close“ und „cancel“ bereitstellen.
Klassen, die Coroutinen als Implementierungsdetails starten, müssen eine Möglichkeit bieten, diese laufenden parallelen Aufgaben sauber zu beenden, damit sie keine unkontrollierte parallele Arbeit in einen übergeordneten Bereich übertragen. In der Regel wird dazu ein untergeordnetes Job
eines bereitgestellten CoroutineContext
erstellt:
private val myJob = Job(parent = `CoroutineContext`[Job])
private val myScope = CoroutineScope(`CoroutineContext` + myJob)
fun cancel() {
myJob.cancel()
}
Möglicherweise wird auch eine join()
-Methode bereitgestellt, damit der Nutzercode auf den Abschluss aller ausstehenden parallelen Aufgaben warten kann, die vom Objekt ausgeführt werden.
Dazu kann auch das Bereinigen durch Abbrechen eines Vorgangs gehören.
suspend fun join() {
myJob.join()
}
Benennung von Terminalvorgängen
Der Name für Methoden, die gleichzeitig ausgeführte Aufgaben, die einem Objekt gehören und noch in Bearbeitung sind, sauber beenden, sollte den Verhaltensvertrag für das Herunterfahren widerspiegeln:
Verwenden Sie close()
, wenn laufende Vorgänge abgeschlossen werden können, aber keine neuen Vorgänge gestartet werden dürfen, nachdem der Aufruf von close()
zurückgegeben wurde.
Verwenden Sie cancel()
, wenn laufende Vorgänge vor dem Abschluss abgebrochen werden können.
Nach der Rückgabe des Aufrufs von cancel()
dürfen keine neuen Vorgänge gestartet werden.
Klassenkonstruktoren akzeptieren CoroutineContext, nicht CoroutineScope
Wenn Objekte nicht direkt in einem bereitgestellten übergeordneten Bereich gestartet werden dürfen, ist CoroutineScope
als Konstruktorparameter nicht geeignet:
// 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
}
Die CoroutineScope
wird zu einem unnötigen und irreführenden Wrapper, der in einigen Anwendungsfällen nur erstellt wird, um als Konstruktorparameter übergeben zu werden, nur um dann verworfen zu werden:
// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))
CoroutineContext-Parameter haben standardmäßig EmptyCoroutineContext
Wenn ein optionaler CoroutineContext
-Parameter in einer API-Oberfläche angezeigt wird, muss der Standardwert das Sentinel Empty`CoroutineContext`
sein. So lassen sich API-Verhaltensweisen besser zusammensetzen, da ein Empty`CoroutineContext`
-Wert von einem Aufrufer genauso behandelt wird wie die Annahme des Standardwerts:
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)
// ...
}