Richtlinien für asynchrone und nicht blockierende APIs unter Android

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 noch nicht abgeschlossen ist oder auf den Abschluss von E/A- oder IPC-Vorgängen, die Verfügbarkeit von stark umkämpften Systemressourcen oder eine Nutzereingabe gewartet werden muss, bevor die Aufgabe fortgesetzt werden kann. Besonders gut entwickelte APIs bieten eine Möglichkeit, den laufenden Vorgang abzubrechen und die Ausführung von Aufgaben im Namen des ursprünglichen Aufrufers zu beenden, wodurch die Systemintegrität und die Akkulaufzeit geschont werden, wenn der Vorgang nicht mehr benötigt wird.

Asynchrone APIs sind eine Möglichkeit, ein nicht blockierendes Verhalten zu erzielen. Asynchrone APIs akzeptieren eine Form der Fortsetzung oder einen Callback, der benachrichtigt wird, wenn der Vorgang abgeschlossen ist oder wenn andere Ereignisse während des Vorgangs auftreten.

Es gibt zwei Hauptgründe für das Schreiben einer asynchronen API:

  • Mehrere Vorgänge gleichzeitig ausführen, wobei ein N-ter Vorgang initiiert werden muss, bevor der N-1-te Vorgang abgeschlossen ist.
  • Verhindern, dass ein aufrufender Thread blockiert wird, bis ein Vorgang abgeschlossen ist.

Kotlin fördert stark die 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-blockierenden Verhalten entkoppeln. Suspend-Funktionen sind nicht blockierend und synchron.

Suspend-Funktionen:

  • Blockieren nicht den aufrufenden Thread, sondern geben stattdessen ihren Ausführungsthread als Implementierungsdetail ab, während sie auf die Ergebnisse von Vorgängen warten, die an anderer Stelle ausgeführt werden.
  • Werden synchron ausgeführt und erfordern nicht, dass der Aufrufer einer nicht blockierenden API gleichzeitig mit nicht blockierenden Aufgaben fortfährt, die durch den API-Aufruf initiiert wurden.

Auf dieser Seite wird eine Mindestgrundlage für Erwartungen beschrieben, die Entwickler sicher haben können, wenn sie mit nicht blockierenden und asynchronen APIs arbeiten. Anschließend werden eine Reihe von Rezepten für das Erstellen von APIs vorgestellt, die diese Erwartungen in Kotlin oder Java sowie in der Android-Plattform oder Jetpack-Bibliotheken erfüllen. Im Zweifelsfall sollten die Erwartungen der Entwickler als Anforderungen für jede neue API-Oberfläche betrachtet werden.

Erwartungen von Entwicklern an asynchrone APIs

Die folgenden Erwartungen werden aus der Sicht von nicht suspendierenden APIs formuliert, sofern nicht anders angegeben.

APIs, die Callbacks akzeptieren, sind in der Regel asynchron

Wenn eine API einen Callback akzeptiert, der nicht so dokumentiert ist, dass er nur an Ort und Stelle aufgerufen wird (d. h. nur vom aufrufenden Thread, bevor der API-Aufruf selbst zurückkehrt), wird davon ausgegangen, dass die API asynchron ist. Diese API sollte alle anderen Erwartungen erfüllen, 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 eine Mapper- oder Prädikatfunktion für jedes Element in einer Sammlung aufruft.

Asynchrone APIs sollten so schnell wie möglich zurückkehren

Entwickler erwarten, dass asynchrone APIs nicht blockierend sind und schnell zurückkehren, nachdem die Anfrage für den Vorgang initiiert wurde. Es sollte immer sicher sein, eine asynchrone API jederzeit aufzurufen, und der Aufruf einer asynchronen API sollte niemals zu ruckelnden Frames oder ANR führen.

Viele Vorgänge und Lebenszyklussignale können von der Plattform oder den Bibliotheken bei Bedarf ausgelöst werden. Es ist nicht realistisch, von einem Entwickler zu erwarten, dass er alle potenziellen Aufrufstellen für seinen Code kennt. Beispielsweise kann ein Fragment in einer synchronen Transaktion zum FragmentManager hinzugefügt werden, wenn die View gemessen und angeordnet wird, 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 sinnvollerweise einmalige Startvorgänge ausführen. Dies kann auf einem kritischen Codepfad liegen, um einen ruckelfreien Animationsframe zu erzeugen. Ein Entwickler sollte immer darauf vertrauen können, dass der Aufruf einer beliebigen asynchronen API als Reaktion auf diese Art von Lebenszyklus-Callbacks nicht die Ursache für einen ruckelnden Frame ist.

Das bedeutet, dass die Arbeit, die von einer asynchronen API vor der Rückgabe ausgeführt wird, sehr leichtgewichtig sein muss. Es wird höchstens ein Datensatz der Anfrage und des zugehörigen Callbacks erstellt und bei der Ausführungs-Engine registriert, 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 kann Folgendes gehören:

  • Implementieren eines zugrunde liegenden IPC als unidirektionaler Binder-Aufruf
  • Ausführen eines bidirektionalen Binder-Aufrufs an den Systemserver, wobei für den Abschluss der Registrierung keine stark umkämpfte Sperre erforderlich ist
  • Senden der Anfrage an einen Arbeitsthread im App-Prozess, um eine blockierende Registrierung über IPC auszufü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.

Asynchrone APIs können Argumente auf „null“ prüfen und NullPointerException auslösen oder prüfen, ob die angegebenen Argumente in einem gültigen Bereich liegen, und IllegalArgumentException auslösen. Bei einer Funktion, die beispielsweise einen float-Wert im Bereich von 0 bis 1f akzeptiert, kann die Funktion prüfen, ob der Parameter in diesem Bereich liegt, und IllegalArgumentException auslösen, wenn er außerhalb des Bereichs liegt. Ein kurzer String kann auch auf die Einhaltung eines gültigen Formats wie z. B. nur alphanumerische Zeichen geprüft werden. Der Systemserver sollte dem App-Prozess niemals vertrauen. Jeder Systemdienst sollte diese Prüfungen im Systemdienst selbst wiederholen.

Alle anderen Fehler sollten an den bereitgestellten Callback gemeldet werden. Dazu gehören unter anderem:

  • Endgültiger Fehler des angeforderten Vorgangs
  • Sicherheitsausnahmen aufgrund fehlender Autorisierung oder Berechtigungen, die zum Abschließen des Vorgangs erforderlich sind
  • Überschrittenes Kontingent für die Ausführung des Vorgangs
  • Der App-Prozess ist nicht ausreichend im Vordergrund, um den Vorgang auszuführen
  • Erforderliche Hardware wurde getrennt
  • Netzwerkfehler
  • Zeitlimits
  • Binder-Fehler oder nicht verfügbarer Remote-Prozess

Asynchrone APIs sollten einen Mechanismus zum Abbrechen bieten

Asynchrone APIs sollten eine Möglichkeit bieten, einem laufenden Vorgang mitzuteilen, dass der Aufrufer kein Interesse mehr am Ergebnis hat. Dieser Abbruchvorgang sollte zwei Dinge signalisieren:

Feste Verweise auf vom Aufrufer bereitgestellte Callbacks sollten freigegeben werden

Callbacks, die an asynchrone APIs übergeben werden, können feste Verweise auf große Objektgraphen enthalten. Laufende Aufgaben, die einen festen Verweis auf diesen Callback enthalten, können verhindern, dass diese Objektgraphen von der Garbage Collection erfasst werden. Wenn diese Callback-Verweise beim Abbruch freigegeben werden, können diese Objektgraphen viel früher von der Garbage Collection erfasst werden, als wenn die Aufgabe bis zum Abschluss ausgeführt würde.

Die Ausführungs-Engine, die Aufgaben für den Aufrufer ausführt, kann diese Aufgaben beenden

Aufgaben, die durch asynchrone API-Aufrufe initiiert werden, können hohe Kosten in Bezug auf den Energieverbrauch oder andere Systemressourcen verursachen. APIs, mit denen Aufrufer signalisieren können, wenn diese Aufgabe nicht mehr benötigt wird, ermöglichen es, die Aufgabe zu beenden, bevor weitere Systemressourcen verbraucht werden.

Besondere Überlegungen für im Cache gespeicherte oder eingefrorene Apps

Wenn Sie asynchrone APIs entwerfen, bei denen Callbacks aus einem Systemprozess stammen und an Apps gesendet werden, sollten Sie Folgendes berücksichtigen:

  1. Prozesse und App-Lebenszyklus: Der App-Prozess des Empfängers befindet sich möglicherweise im Cache.
  2. Cached Apps Freezer: Der App-Prozess des Empfängers ist möglicherweise eingefroren.

Wenn ein App-Prozess in den Cache-Zustand wechselt, bedeutet das, dass er keine für Nutzer sichtbaren Komponenten wie Aktivitäten und Dienste aktiv hostet. Die App wird im Arbeitsspeicher behalten, falls sie wieder für Nutzer sichtbar wird, sollte aber in der Zwischenzeit keine Aufgaben ausführen. In den meisten Fällen sollten Sie das Senden von App-Callbacks pausieren, wenn die App in den Cache-Zustand wechselt, und fortsetzen, wenn die App den Cache-Zustand verlässt, um keine Aufgaben in im Cache gespeicherten App-Prozessen auszulösen.

Eine im Cache gespeicherte App kann auch eingefroren werden. Wenn eine App eingefroren ist, erhält sie keine CPU-Zeit und kann überhaupt keine Aufgaben ausführen. Alle Aufrufe an die registrierten Callbacks dieser App werden gepuffert und gesendet, wenn die App wieder aktiviert wird.

Gepufferte Transaktionen an App-Callbacks sind möglicherweise veraltet, wenn die App wieder aktiviert wird und sie verarbeitet. Der Puffer ist begrenzt und ein Überlauf würde dazu führen, dass die Empfänger-App abstürzt. Um zu vermeiden, dass Apps mit veralteten Ereignissen überlastet werden oder ihre Puffer überlaufen, sollten Sie keine App-Callbacks senden, während der Prozess 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 pausieren, während der Prozess der App eingefroren ist.

Zustands-Tracking

So verfolgen Sie, wann Apps in den Cache-Zustand wechseln oder ihn verlassen:

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

So verfolgen Sie, wann Apps eingefroren oder wieder aktiviert werden:

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

Strategien zum Fortsetzen des Sendens von App-Callbacks

Unabhängig davon, ob Sie das Senden von App-Callbacks pausieren, wenn die App in den Cache-Zustand oder den eingefrorenen Zustand wechselt, sollten Sie das Senden der registrierten Callbacks der App fortsetzen, sobald die App den jeweiligen Zustand verlässt, bis die App die Registrierung des Callbacks aufhebt 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, um zu verhindern, dass Callbacks an den Zielprozess gesendet werden, 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, oft als Snapshot des letzten Zustands. Beispiel: eine hypothetische API für Apps, um den verbleibenden Akkustand zu überwachen:

interface BatteryListener {
    void onBatteryPercentageChanged(int newPercentage);
}

Stellen Sie sich ein Szenario vor, in dem mehrere Ereignisse zur Zustandsänderung auftreten, während eine App eingefroren ist. Wenn die App wieder aktiviert wird, sollten Sie nur den letzten Zustand an die App senden und andere veraltete Zustandsänderungen verwerfen. Diese Übermittlung sollte sofort erfolgen, wenn die App wieder aktiviert wird, damit die App auf dem neuesten Stand ist. Das lässt sich so erreichen:

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 Wert verfolgen, der an die App gesendet wurde, damit die App nicht über denselben Wert benachrichtigt werden muss, sobald sie wieder aktiviert wird.

Der Zustand kann als komplexere Daten ausgedrückt werden. Beispiel: eine hypothetische API für Apps, um über Netzwerkschnittstellen benachrichtigt zu werden:

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

Wenn Sie Benachrichtigungen an eine App pausieren, sollten Sie sich die Netzwerke und Zustände merken, die die App zuletzt gesehen hat. Nach dem Fortsetzen wird empfohlen, die App über alte Netzwerke, die verloren gegangen sind, über neue Netzwerke, die verfügbar geworden sind, und über vorhandene Netzwerke zu benachrichtigen, deren Zustand sich geändert hat – in dieser Reihenfolge.

Benachrichtigen Sie die App nicht über Netzwerke, die verfügbar gemacht und dann verloren gegangen sind, während Callbacks pausiert wurden. Apps sollten keine vollständige Aufzeichnung der Ereignisse erhalten, die aufgetreten sind, während sie eingefroren waren. Die API-Dokumentation sollte nicht versprechen, Ereignisstreams außerhalb expliziter Lebenszyklus-Zustände ohne Unterbrechung zu liefern. Wenn die App in diesem Beispiel die Netzwerkverfügbarkeit kontinuierlich überwachen muss, muss sie sich in einem Lebenszyklus-Zustand befinden, der verhindert, dass sie im Cache gespeichert oder eingefroren wird.

Bei der Überprüfung sollten Sie Ereignisse zusammenfassen, die nach dem Pausieren und vor dem Fortsetzen von Benachrichtigungen aufgetreten sind, und den letzten Zustand prägnant an die registrierten App-Callbacks senden.

Überlegungen zur Entwicklerdokumentation

Die Übermittlung asynchroner Ereignisse kann verzögert werden, entweder weil der Absender die Übermittlung für einen bestimmten Zeitraum pausiert hat, wie im vorherigen Abschnitt gezeigt, oder weil die Empfänger-App nicht genügend Geräteressourcen erhalten hat, um das Ereignis rechtzeitig zu verarbeiten.

Raten Sie Entwicklern davon ab, Annahmen über die Zeit zwischen dem Zeitpunkt, zu dem ihre App über ein Ereignis benachrichtigt wird, und dem Zeitpunkt, zu dem das Ereignis tatsächlich aufgetreten ist, zu treffen.

Erwartungen von Entwicklern an suspendierende 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 abschließen, bevor sie zurückkehren oder eine Ausnahme auslösen

Ergebnisse nicht blockierender Vorgänge werden als normale Funktionsrückgabewerte zurückgegeben und Fehler werden durch Auslösen von Ausnahmen gemeldet. Das bedeutet oft, dass Callback-Parameter nicht erforderlich sind.

Suspend-Funktionen sollten Callback-Parameter nur an Ort und Stelle aufrufen

Suspend-Funktionen sollten immer alle zugehörigen Aufgaben abschließen, bevor sie zurückkehren. Sie sollten daher niemals einen bereitgestellten Callback oder einen anderen Funktionsparameter aufrufen oder einen Verweis darauf beibehalten, nachdem die Suspend-Funktion zurückgekehrt ist.

Suspend-Funktionen, die Callback-Parameter akzeptieren, sollten den Kontext beibehalten, sofern nicht anders dokumentiert

Wenn eine Funktion in einer Suspend-Funktion aufgerufen wird, wird sie im CoroutineContext des Aufrufers ausgeführt. Da Suspend-Funktionen alle zugehörigen Aufgaben abschließen 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 solchen Callbacks auch im 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 dem Job-Abbruch zusammenarbeiten, wie in kotlinx.coroutines definiert. Wenn der aufrufende Job eines laufenden Vorgangs abgebrochen wird, sollte die Funktion so schnell wie möglich mit einer CancellationException fortgesetzt werden, damit der Aufrufer die Bereinigung durchführen und so schnell wie möglich fortfahren kann. Dies wird automatisch von suspendCancellableCoroutine und anderen suspendierenden APIs von kotlinx.coroutines verarbeitet. Bibliotheksimplementierungen sollten suspendCoroutine im Allgemeinen nicht direkt verwenden, da es dieses Abbruchverhalten standardmäßig nicht unterstützt.

Suspend-Funktionen, die blockierende Aufgaben 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 , eine blockierende Funktion vollständig zu suspendieren, um Threads zu wechseln.

Der Aufruf einer Suspend-Funktion sollte nicht zur Erstellung zusätzlicher Threads führen, ohne dass der Entwickler einen eigenen Thread oder Threadpool für die Ausführung dieser Aufgabe bereitstellen kann. Beispielsweise kann ein Konstruktor ein CoroutineContext akzeptieren, das verwendet wird, um Hintergrundaufgaben für die Methoden der Klasse auszuführen.

Suspend-Funktionen, die einen optionalen CoroutineContext- oder Dispatcher-Parameter akzeptieren würden, nur um zu diesem Dispatcher zu wechseln, um blockierende Aufgaben 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 Aufgabe an einen ausgewählten Dispatcher zu senden.

Klassen, die Koroutinen starten

Klassen, die Koroutinen starten, müssen einen CoroutineScope haben, um diese Startvorgänge auszuführen. Die Einhaltung der Prinzipien der strukturierten Parallelität impliziert die folgenden strukturellen Muster für das Abrufen und Verwalten dieses Bereichs.

Bevor Sie eine Klasse schreiben, die parallele 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()
    }
}

Wenn Sie eine suspend fun verfügbar machen, um parallele Aufgaben auszuführen, kann der Aufrufer den Vorgang im eigenen Kontext aufrufen, sodass MyClass keinen CoroutineScope verwalten muss. Die Serialisierung der Verarbeitung von Anfragen wird einfacher und der Zustand kann oft als lokale Variablen von handleRequests vorhanden sein, anstatt als Klasseneigenschaften, die andernfalls eine zusätzliche Synchronisierung erfordern würden.

Klassen, die Koroutinen verwalten, sollten Methoden zum Schließen und Abbrechen verfügbar machen

Klassen, die Koroutinen 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 auslagern. In der Regel wird dazu ein untergeordneter Job eines bereitgestellten CoroutineContext erstellt:

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

fun cancel() {
    myJob.cancel()
}

Es kann auch eine join()-Methode bereitgestellt werden, damit der Nutzercode auf den Abschluss aller ausstehenden parallelen Aufgaben warten kann, die vom Objekt ausgeführt werden. Dazu kann auch die Bereinigungsarbeit gehören, die durch das Abbrechen eines Vorgangs ausgeführt wird.

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

Benennung von Endvorgängen

Der Name, der für Methoden verwendet wird, die parallele Aufgaben, die sich noch in Bearbeitung befinden und einem Objekt gehören, sauber beenden, sollte den Verhaltensvertrag für das Beenden widerspiegeln:

Verwenden Sie close(), wenn laufende Vorgänge abgeschlossen werden können, aber nach der Rückgabe des Aufrufs von close() keine neuen Vorgänge gestartet werden dürfen.

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
}

Der 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 der Empty`CoroutineContext` Sentinel sein. Dies ermöglicht eine bessere Zusammensetzung von API-Verhaltensweisen, da ein Empty`CoroutineContext` Wert von einem Aufrufer auf dieselbe Weise 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)

    // ...
}