Programmiersprache: Swift 5.5 krempelt die Nebenläufigkeit der Sprache um

Das aktuelle Release der Open-Source-Sprache bringt zahlreiche Neuerungen für nebenläufige Programmierung vom Async/Await-Pattern bis zum Actor-Modell.

In Pocket speichern vorlesen Druckansicht 11 Kommentare lesen

(Bild: Paweł Kuźniar (unter der GFDL))

Lesezeit: 5 Min.
Von
  • Rainald Menge-Sonnentag
Inhaltsverzeichnis

Apples quelloffene Programmiersprache Swift ist in Version 5.5 erschienen und bringt zahlreiche Neuerungen für nebenläufige Programmierung mit. Neben dem Async/Await-Pattern wartet das Release mit Structured Concurrency, asynchronen Sequences und dem Actor-Modell auf.

Die vor fünf Monaten veröffentlichte Version 5.4 hatte bereits erste Vorbereitungen für das neue Concurrency-Modell an Bord, die nun in zahlreichen Proposals in Swift einfließen. Das Proposal SE-0296 führt das Async/Await-Pattern ein, um Koroutinen analog zu Programmiersprachen wie C#, Kotlin, C++20 oder JavaScript umzusetzen.

Das Schlüsselwort async deklariert eine Funktion als asynchron, und der Aufruf erfolgt über das Keyword await. Die Funktion kann ihre Ausführung jederzeit unterbrechen, um beispielsweise bei Netzwerktransaktionen den Ablauf des Programms nicht zu blockieren, während sie auf eingehende Daten wartet.

Um die Umsetzung kümmert sich der Kompiler, sodass Entwicklerinnen und Entwickler asynchronen Code ohne zusätzliche Prüfroutinen analog zu synchronem schreiben können, wie in folgendem Beispiel aus dem Proposal:

func loadWebResource(_ path: String) async throws -> Resource
func 
 decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image

func processImageData() async throws -> Image {
  let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  let imageTmp      = 
    try await decodeImage(dataResource, imageResource)
  let imageResult   = try await dewarpAndCleanupImage(imageTmp)
  return imageResult
}

Die eigentliche Umsetzung für nebenläufige Programme findet sich in einer weiteren Neuerung von Swift 5.5: SE-0304 führt Structured Concurrency ein. Es ermöglicht die Aufteilung von sequenziellen Prozessen in jeweils parallele Strukturen. Als anschauliches Beispiel bringt das Proposal das Vorbereiten des Abendessens auf den Tisch, bei dem sich einige Schritte parallelisieren lassen, während andere aufeinander warten müssen. So kann jemand das Fleisch marinieren, während jemand anderes das Gemüse schneidet. Spätestens beim Servieren müssen aber alle Aufgaben zusammenlaufen.

Die Grundlage für die parallele Verarbeitung bilden Tasks. Laut dem Proposal verhält sich in Swift 5.5 ein Task zu einer asynchronen Funktion wie ein Thread zu einer synchronen Funktion. Jeder Task hat einen von drei Zuständen: Er läuft (running), ist angehalten (suspended) oder fertig (completed). Die TaskPriority legt die Priorität des jeweiligen Tasks als high, medium, low oder background fest. Alternativ lassen sich die Apple-spezifischen Stufen userInitiated für high und utility für low nutzen.

Eine TaskGroup kann mehrere Aufgaben in Child Tasks zusammenfassen. Die Gruppe beendet erst dann ihre Arbeit und kehrt zurück, wenn alle Kinder ihre Ausführung abgeschlossen haben. Die Kinder erben die Prioriät ihrer Eltern. Abgesehen von der regulären Ausführung kann es zum Abbruch kommen: Der Aufruf von cancel() bricht den Task manuell ab, und ein unerwarteter Fehler in einem Eltern-Task führt zum Abbruch nicht beendeter Kinder-Tasks.

Zusätzliche Methoden ermöglichen das Steuern eines Tasks: Mit sleep() pausiert er eine angegebene Zeit, und suspend() hält ihn explizit an, um anderen Tasks Zeit für die Ausführung zu geben.

Zusätzlich führt das Proposal SE-0298 das AsyncSequence-Protokoll ein, das im Wesentlichen dem synchronen Sequence-Protokoll entspricht, aber auf Werte ausgelegt ist, die erst mit der Zeit eintreffen. Aufgrund der asynchronen Verarbeitung muss unter anderem die next()-Funktion innerhalb der AsyncSequence als async definiert sein. Das Iterieren über die Sequenz ist beispielsweise mit for try await n in MySequence() möglich.

Ein weiteres Proposal bildet die Brücke zu nebenläufigen Anwendungen in Objective-C: SE-0297 definiert das Übersetzen von Completion-Handlers aus Objective-C in async-Methoden in Swift. Umgekehrt lassen sich in letzterer Programmiersprache asynchrone Methoden über @objc als Aufrufe aus Objective-C bereitstellen und dabei als Completion-Handler-Methoden übergeben.

Das Proposal SE-0306 führt schließlich das Actor-Modell in Swift ein. Der Vorschlag dazu kam kurz nach dem Release von Swift 5.3 auf. Es stellt ein Konzept dar, um typische Fallen der Parallelprogrammierung wie Data Races zu vermeiden. Die Aktoren verhalten sich in dem Modell ähnlich wie Klassen, schützen aber die veränderlichen Werte vor externem Zugriff. Nachrichten ersetzen die direkten Funktionsaufrufe, während der direkte Zugriff nur aus self erlaubt ist. Für die Isolierung der Aktoren sorgt der Compiler.

Der Aktor kümmert sich um die Verarbeitung, sobald dies ohne potenzielle Race Conditions möglich ist. Jeder Actor hat seine eigene Mailbox oder Message Queue, die alle Nachrichten aufnimmt, bis er sie bearbeiten kann. Die Aktoren haben jeweils einen eigenen Posteingang und verarbeiten die Nachrichten sequenziell. Um Race Conditions zu verhindern, findet keine parallele Verarbeitung von Actor-isoliertem Code statt.

Tücken der Parallelprogrammierung: Race Conditions und Data Races

In der nebenläufigen Programmierung sind Race Conditions ein typisches Problem des nichtdeterministischen Verhaltens: Wenn zwei parallel laufende Threads beispielsweise Berechnungen auf einer globalen Variable ausführen, kann es zu Fehlern kommen, die jedoch nicht immer auftreten. Für den simplen Fall, dass beide Threads den Wert zunächst auslesen, dann mit 2 multiplizieren und schließlich in dieselbe Variable zurückschreiben, ergibt sich im korrekten Fall für den Ausgangswert 1 ein Ergebnis von 4.

Voraussetzung dafür ist, dass zunächst ein Thread die Variable mit dem Wert 1 ausliest, mit 2 multipliziert und anschließend das Ergebnis 2 schreibt, bevor der zweite Thread diese 2 ausliest, erneut mit 2 multipliziert und anschließend eine 4 schreibt. Falls jedoch der zweite Thread den Wert ausliest, wenn der erste bereits gelesen, aber das Ergebnis noch nicht geschrieben hat, rechnen beide mit dem Ausgangswert 1 und schreiben im Anschluss die 2 als falsches Endergebnis in die Variable zurück.

Data Races beschreiben, dass ein Speicherzugriff in einem Thread potenziell in für den korrekten Programmablauf gefährlichen oder störenden Weise von einer Schreiboperation auf denselben Speicherbereich in einem anderen Thread beeinflusst wird. Sie gelten in manchen Definitionen als Teilbereich der Race Conditions, während andere sie als eigenständiges Problem betrachten.

Als zusätzliche Erweiterung führt SE-0316 Global Actors ein, bei denen die Aktoren direkt aus dem Main Thread des Programms zur Verfügung stehen.

Swift 5.5 bringt einige weitere Neuerungen, die zum Teil das neue Concurrency-Modell erweitern wie Task Local Values (SE-0311), async let-Bindings (SE-0317), Effectful Read-only Properties (SE-0310) oder AsyncStream und AsyncThrowingStream (SE-0314).

Jenseits der Nebenläufigkeit sind die Erweiterung der in Swift 5.1 eingeführten Property Wrappers auf Parameter von Funktionen und Closures (SE-0293) und die Codable Synthesis for Enums with Associated Values (SE-0295) nennenswert. Außerdem erlaubt der Paketmanager von Swift neuerdings das Erstellen einer kuratierten Liste von Paketen und zugehörigen Metadaten als Package Collection (SE-0291).

Die vollständige Liste der Neuerungen lässt sich dem Swift-Blog entnehmen. Binaries der Open-Source-Programmiersprache finden sich auf der Download-Seite. Eine weitere Seite verweist auf die einzelnen GitHub-Repositories mit dem Sourcecode.

(rme)