Mobile Benutzer wollen schnelle Antworten

Vor vielen Jahren, in der Anfangszeit von interaktiven Anwendungen waren Benutzer damit zufrieden, dass man ihnen eine Sanduhr anzeigt, wenn ein Programm einen längeren Vorgang ausführen muss. Denken wir dabei etwa an einen Download aus dem Internet. Einige Zeit lang genügten auch noch Pseudo - Fortschrittsbalken1, die keine echte Information über den aktuellen Fortschrift der Anwendung darstellten, sondern einfach nur eine Animation abspielten. Vereinzelt sieht man das sogar noch heute, etwa bei manchen Installationsprogrammen.

Wenn wir heute derartige Apps programmieren, werden diese nicht lange installiert bleiben. Wenn wir Erfolg mit unseren Anwendungen haben wollen, müssen wir präzise visuelle Rückmeldungen über den aktuellen Fortschritt unseres asynchronen Vorganges geben und müssen es dem Benutzer ermöglichen, den Vorgang abzubrechen, wenn das Gefühl entsteht, dass das alles zu lange dauert.

Nun, was ist ein langsamer Vorgang? Denken wir an das Kino. Bei der Wiedergabe eines Filmes wird die physiologische Eigenschaft des Menschen ausgenutzt, dass sich Änderungen des Bildinhaltes, wenn dies im Millisekundenbereich geschieht, vom Seheindruck her mit dem Nachfolgebild vermischen. Kinos haben typischerweise eine Bildwiederholfrequenz von 24 Bildern pro Sekunde. Eine gute Regie schwenkt daher nur sehr langsam oder entsprechend schnell um den Eindruck des “Ruckelns” zu vermeiden.

Das liefert uns also eine Faustregel für die Größenordnung, ab wann wir Multithreading einsetzen müssen: alles, was länger dauert als eine Dreißigstel Sekunde, muss in einem eigenen Thread laufen. Das gilt ganz besonders für Zugriffe auf das Internet und für das Lesen oder Parsen längerer Dateien.

Main- und Background-Thread

Grand Central TerminalZugriffe auf Elemente der Benutzeroberfläche - auf das UserInterface - dürfen nur vom Mainthread aus erfolgen. Das bedeutet mindestens, dass alle Aufrufe von Properties oder Funktionen aller UI* Klassen im Hauptthread erfolgen müssen. Sobald wir einen anderen Thread benutzen, muss dieser, wenn er irgendein Feedback geben will eine Funktion im Mainthread ausführen lassen.

Wir können diese Situation mit einem Rangierbahnhof vergleichen. Der MainThread ist das Hauptgleis, das aus dem Bahnhof herausführt. Wenn Waggons beladen werden, oder Fahrgäste zusteigen, stehen die Waggons auf einem Nebengleis, damit andere Züge weiterhin durchfahren können. Wenn der Zug wieder fahrbereit ist, gliedert er sich wieder auf das Hauptgleis ein. Unser Mainthread entspricht also dem Hauptgleis, und die Background-Threads betrachten wir als Nebengleise. Rechts sehen sie in Analogie zu dem Bild eine Aufnahme des Grand Central Terminals in New York.

Grand Central Dispatch

In den Anfängen des Multithreading musste man sich mit Synchronisationsobjekten herumquälen und hatte mit Dingen wie Semaphor, Mutex, SpinLock und synchronized zu kämpfen. Dining Philosophers

Nicht selten verhungerte dabei die Anwendung ähnlich wie die speisenden Philosphen. Es ist manchmal nicht einfach solche Synchronisationsprobleme aufzuspüren, da sie manchmal nicht reproduzierbar sind.

Durch die Einführung von Closures wurde vieles leichter. Sowohl iOS als auch tvOS und OSX implementieren den Grand Central Dispatch. Das ermöglicht uns eine wunderbar einfache und klare Struktur aller nebenläufigen Vorgänge in unserer Anwendung zu schaffen.

Eine gute Darstellung liefert das GCD Video von der WWDC.

Beispiel: Download von Bildern in einer DispatchQueue

Ein Beispiel, wie wir analog zum oben angeführten Video den Grand Central Dispatch verwenden, um zwei Bilder aus dem Internet zu laden und diese in zwei UIImageViews anzuzeigen kann wie unten dargestellt aussehen2. Nachdem beide Bilder geladen wurden synchronisiert die DispatchGroup die beiden Vorgänge wieder und schreibt anschliessend “back in main thread” in die Debug-Konsole.

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var imageView1: UIImageView!
    @IBOutlet weak var imageView2: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let downloads = [
            "https://spin.atomicobject.com/wp-content/uploads/dining-philosophers.jpg": imageView1,
            "https://raw.githubusercontent.com/wiki/angrave/SystemProgramming/5DiningPhilosophers.png": imageView2
        ]
        
        var currentImageIndex = 0
        let group = DispatchGroup()
        
        downloads.forEach { (url, imageView) in
            currentImageIndex += 1
            let queue = DispatchQueue(label: "imagequeue\(currentImageIndex)")
            group.enter() // tell the group we start something...
            queue.async {
                self.downloadImage(imageurl: url, imageview: imageView!)
                group.leave() // ... now, a long time later we tell the group we are done
            }
        }
        group.notify(queue: DispatchQueue.main) {
            print("back in main thread")
        }
    }
    
    func downloadImage(imageurl: String, imageview: UIImageView){
        if let url = URL(string:imageurl) {
            if let data = try? Data(contentsOf: url) {
                DispatchQueue.main.async {
                    if let image = UIImage(data: data) {
                        print("downloaded image \(url)")
                        imageview.image = image
                    }
                }
            }
        }
    }
 }

Man beachte, wie hier die Zuweisung des images zu imageView.image in den MainThread verlagert wird3.


  1. progressbar

  2. tatsächlich könnte (und sollte) man das UIImage bereits im Background - Thread erstellen, da CGContext hier thread safe ist. Die Faustregel keine UI* Funktionen im Hauptthread verwenden zu vermitteln ist mir hier didaktisch aber wichtiger.

  3. und auch ausgelagert werden muss