~/home of geeks

Threadsafety 101

· 3050 Wörter · 15 Minute(n) Lesedauer

red cotton yarn, white background, scissor cutting through a strand

Vor einiger Zeit habe ich einen kleinen Vortrag zum Thema Threadsicherheit (Threadsafety) in Form eines Quiz erstellt, dass helfen soll, weitere Achtsamkeit gegenüber potenziellen Konkurrenz-Problemen zu entwickeln.

Threadsicherheit 101
Threadsicherheit 101

Der Vortrag ist in Form von mehreren Regeln organisiert, welche helfen sollen, dass Vorliegen von Konkurrenz-Problemen schneller zu identifizieren. Zu jeder Regel gibt es ein Beispiel, das die Regel illustriert.

Regel 1 - Shared Resource #

Regel 1 - Shared Resource

Sobald eine lokale Variable in einer Klasse verwendet und verändert wird, kann ein Threadsicherheitsproblem auftauchen (Shared Resource, geteilter Zustand / Resource).

Ohne einen geteilten Zustand kann es keine Konkurrenzprobleme geben.

Beispiel:

class ReadMe {
    private String text = "Hello World";
    String getText() {
        return text;
    }
}

 

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?
 
 
 

Ja, denn der Zustand der Variable text wird nie geändert. Im Prinzip ist die Klasse ReadMe ein Immutable-Objekt. Diese sind per se threadsicher.

Regel 1.1 - Immutables #

Regel 1.1 - Immutables

Aus Regel 1 folgt: Unveränderliche Objekte (Immutables) sind per se threadsicher.

class ChangeMe { 
    private String text;
    String getText() {
        return text;
    }
    String setText(String text) {
        this.text = text;
    } 
}

 

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?
 
 
 

Nein, der Zustand der Variable text kann geändert werden. Wie sieht dass dann aus? Hier ein Thread, der die Klasse ChangeMe verwendet:

class Thread {
    private final ChangeMe changeMe;
    private final int id;
    public Thread(ChangeMe changeMe, int id) {
        this.changeMe = changeMe;
        this.id = id;
    }
    
    run() {
        changeMe.setText("Hello Thread" + id);
        System.out.println(changeMe.getText());
    }
}

Die Ausführung dann wie folgt:

ChangeMe changeMe = new ChangeMe();
new Thread(changeMe, 1).run();
new Thread(changeMe, 2).run();

In der Ausführung passiert dann z. B. folgendes:

T1 -> changeMe.setText("Hello Thread1");
T2 -> changeMe.setText("Hello Thread2");
T1 -> System.out.println(changeMe.getText()); // Hello Thread2
T2 -> System.out.println(changeMe.getText()); // Hello Thread2

Threads können jederzeit von der VM angehalten oder wieder ausgeführt werden.

Im oberen Fall überschreibt T2 den von T1 vorgegebenen Zustand. Ausgegeben wird zweimal “Hello Thread2”. Die VM stoppt T1 nach der Zeile

changeMe.setText("Hello Thread" + id);

und übergibt die Kontrolle an T2. Nach der Ausführung von der Zeile

changeMe.setText("Hello Thread" + id);

durch T2 wird wieder auf T1 geswitcht etc.

Die Reihenfolge der Ausführungen wird durch die VM bestimmt und kann auch anders erfolgen. Der Fehler erscheint also nicht immer.

 

Wenn man aber die nicht-threadsichere Klasse nicht teilt, ist es dann threadsicher?

new Thread(new ChangeMe(), 1).run();
new Thread(new ChangeMe(), 2).run();

Threadsicher oder nicht?
Threadsicher oder nicht?
 
 
 

Ja, keine gemeinsamen Zustände (gekapselt durch jeweils eine eigene Instanz pro Thread) = keine Probleme.

Regel 1.2 - Threadlocal #

Regel 1.2 - Threadlocal

Aus Regel 1 folgt: Pro Thread definierte Zustände (Threadlocal), die nur von diesem verändert werden können, sind per se threadsicher.

 
 
 

class ChangeMe2 { 
    private String text;
    String setAndGetText(String text) {
        this.text = text;
        return this.text;
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Nein, der Zustand von text kann weiterhin von mehreren Prozessen gleichzeitig geändert werden. In diesem Beispiel kann die Ausführung zweier Threads nach dem Ausführen von this.text = text; erfolgen und damit falsche Ergebnisse produzieren.

Der Threadwechsel durch die VM kann sogar während eines Increments stattfinden. Increments sind in Java nicht atomar, wird also aus Sicht der VM in mehreren Schritten ausgeführt, die unterbrochen werden können. Referenzzuweisungen sind Atomar, aber nicht threadübergreifend sichtbar und damit nicht threadsicher (kompliziertes Thema).

Nobody expects the Threadwechsel!
Nobody expects the Threadwechsel!

Regel 0 - Atomarität #

Regel 0 - Atomarität

Jedes Statement in Java (inklusive Zuweisungen und Increments) ist nicht threadsicher.

Diese Regel erhält die übergeordnete Position 0, da sie die Grundlage für alle anderen Regeln bildet.

 

class ChangeMe3 {
    String getText(String text) {
        String resultText = text + text;
        return resultText;
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Ja, denn Zustände / Variablen, die innerhalb des Scopes Methode erzeugt und dann verändert werden, sind nur für den aufrufenden Thread gültig.

What happens in vegas stays in vegas
What happens in vegas stays in vegas

Regel 1.2.1 - Method Scoped #

Regel 1.2.1 - Method Scoped

Aus 1.2 folgt: Innerhalb von Methoden definierte Variablen, welche diese Methode nicht verlassen, können nur von dem Thread verändert werden, der sie initialisiert hat, und sind daher Threadsicher.

Regel 1.2.2 - Functional #

Regel 1.2.2 - Functional

** Aus 1.2.1 resultiert: Funktionale Methoden (Methoden, die alle benötigte Information als unveränderliche Parameter erhalten) sind per se threadsicher.**

Das trifft beispielsweise auf alle rein funktionalen Sprachen, wie Common Lisp, Erlang, Haskell, Clojure etc. zu. Man kann auch in Java funktional programmieren.

Viele Stateless Services sind auch funktional, da sie keine Zustände speichern.

 

class Writer {
    private SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy");
    void printDate(Date myDate) { 
        System.out.println(formatter.format(myDate));
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Das hängt davon ab, ob SimpleDateFormat selber threadsicher ist.

Die statische Natur der Klasse lässt vermuten, dass sie threadsicher wäre.

Eine Googlesuche ergibt folgendes Zitat aus der Javadoc:

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

Die Klasse ist also nicht threadsicher.

Regel 2 - Transitivität #

Regel 2 - Transitivität

Regel 1 trifft auch auf alle Objekte in einer Klasse zu, die verwendet werden, wenn wir nicht wissen, ob diese Objekte selber Variablen haben, die sie verändern.

 


class Writer2 {
    private FastDateFormat formatter = FastDateFormat.getInstance("dd.MM.yyyy", TimeZone.getTimeZone("UTC"), Locale.DE);
    void printDate(Date myDate) {
        System.out.println(formatter.format(myDate));
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Das hängt nach Regel 2 davon ab, ob FastDateFormat selber threadsicher ist.

Die Javadoc zu FastDateFormat sagt hierzu:

FastDateFormat is a fast and thread-safe version of SimpleDateFormat.

Also ja, threadsicher.

Regel 2.1 - Know your dependencies #

Regel 2.1 - Know your dependencies

Bei der Nutzung von anderen Klassen als lokale Objekte vergewissern, dass diese threadsicher sind.

   
 

Threadsicherheit herstellen #

In einigen Fällen möchte man eine gemeinsam genutzte Resource benutzen. Beispielsweise einen naiven Zähler für Metriken.

class ImportantService { 
    private long calls = 0L;
    void doImportantStuff(Data myData) {
        incrementCalls();
    }
    private void incrementCalls() { 
        calls++;
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

   
 

Nein, calls kann von zwei Threads parallel ungünstig verändert werden:

calls = 10
T1 -> read calls (10)
T2 -> read calls (10)
T1 -> calls + 1 (11)
T2 -> calls + 1 (11)
T1 -> calls = 11
T2 -> calls = 11

In diesem Verlauf wurden nur 11 Aufrufe gezählt, obwohl es 12 waren. Das Inkrement von T1 wurde von T2 überschrieben, da er den Zustand von T1 nicht kannte und berücksichtigte.

Bedenke: Regel 0 gilt auch für Increments, sie sind nicht atomar (hier als mehrere Schritte dargestellt).

Um die Stelle threadsicher zu machen, verwenden wir das Schlüsselwort synchronized.

You shall not pass!
You shall not pass!

Regel 3.0 - Synchronized #

Regel 3.0 - Synchronized

Mit synchronized markierte Blöcke können nur von einem einzigen Thread betreten werden.

class ImportantService2 { 
    private long calls = 0L;
    void doImportantStuff(Data myData) { 
        incrementCalls();
    }
    private synchronized incrementCalls() { 
        calls++;
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

   
 

Ja, calls kann nur von einem Thread auf einmal gelesen und geändert werden.

 

class ImportantService3 { 
    private long calls = 0L;
    void doImportantStuff(Data myData) { 
        synchronized(this) {
            incrementCalls(); 
        }
    }
    private void incrementCalls() { 
        calls++;
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Ja, auch hier kann calls nur von einem Thread auf einmal gelesen und geändert werden.

 

class ImportantService4 { 
    private long calls = 0L;
    void doImportantStuff(Data myData) { 
        incrementCalls();
    }
    private void incrementCalls() { 
        synchronized(this) {
            calls++;
        }
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Ja, calls kann nur von einem Thread auf einmal gelesen und geändert werden. Dies ist analog zu ImportantService3, lediglich die Synchronisations-Klammer ist verschoben.

Regel 3 - Synchronisationlock #

Eine Synchronisation findet immer auf einem Objekt statt. Das Objekt ist die Sperre, an diesem Objekt wird nachgehalten, ob ein Thread bereits in einem synchronisierten Block ist oder nicht.

Regel 3 - Synchronisationlock

Synchronisation findet immer auf einem Objekt statt (Monitorlock, Intrinsic Lock, Lock, Semaphore)

private synchronized void incrementCalls() { 
    calls++;
}

ist synonym zu

private void incrementCalls() {
    synchornized(this) {
        calls++;
    }
}

und synchronisiert auf this.

Regel 3.1 - Synchronisation auf this #

Regel 3.1 - Synchronisation auf this

synchronized an nicht-statischen Methoden entspricht synchronized (this)

class ImportantService5 {
    private long otherCalls = 0L;
    private long calls = 0L;
    private final Object semaphoreA = new Object();
    private final Object semaphoreB = new Object();
    void doImportantStuff(Data myData) { 
        incrementCalls();
        incrementOtherCalls();
    }
    private void incrementCalls() { 
        synchronized(semaphoreA) {
            calls++;
        }
    }
    private void incrementOtherCalls() { 
        synchronized(semaphoreB) {
            calls++;
            otherCalls++;
        }
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Nein. Während das Hochzählen der Variablen für sich für einzelne Threads gesperrt ist, haben wir zwei unterschiedliche Sperren, die auf die gleiche Resource zugreifen, nämlich calls.

Regel 3.2 - Verschiedene Locks, verschiedene Threads #

Regel 3.2 - Verschiedene Locks, verschiedene Threads

Verschiedene Locks bedeuten je ein verschiedener Thread kann sich in den jeweiligen gesperrten Bereichen befinden. Als Lock kann jedes beliebige Objekt verwendet werden.

class ImportantService6 {
    private static long otherCalls = 0L;
    private static long calls = 0L;
    void doImportantStuff(Data myData) { 
        incrementCalls();
        incrementOtherCalls();
    }
    private synchronized void incrementCalls() { 
        calls++;
    }
    private static synchronized void incrementOtherCalls() { 
        calls++;
        otherCalls++;
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Nein. Wichtig ist hier zu sehen, welche Objekte als Lock benutzt werden.

private synchronized void incrementCalls() -> this (also ein Objekt von ImportantService6)

private static synchronized void incrementOtherCalls() -> Hier gibt es kein this, da es sich um eine statische Methode handelt. Es wird ImportantService6.class als Lock verwendet!

Wir haben es also mit zwei verschiedenen Locks zu tun, die auf die gleiche Resource zugreifen.

Regel 3.3 - Synchronisation bei static #

Regel 3.3 - Synchronisation bei static

synchronized an statischen Methoden entspricht synchronized (MeineKlasse.class)

Threadsicherheit ohne synchronized #

Java bringt einige Klassen mit, die bereits Threadsicher sind. Hierzu gehörten die Atomic-Klassen (java.util.concurrent.atomic)

class ImportantService7 {
    private AtomicLong calls = new AtomicLong(0L);
    void doImportantStuff(Data myData) { 
        incrementCalls();
    }
    private void incrementCalls() { 
        calls.getAndIncrement();
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Ja. Nach Regel 2 (Transitivität) ist die Klasse threadsicher, wenn AtomicLong threadsicher ist. Die Atomic Klassen versichern, dass alle Methoden atomar, also an einem Stück ausgeführt werden und damit threadsicher sind.

Ein häufiger Einsatz für eine gemeinsame Ressource ist eine Map, die als Ablage für Daten und als Cache verwendet werden kann. Im Folgenden wird eine Map als Speicher für Statistiken verwendet. Es werden die Aufrufe pro Parameter gezählt.

class ImportantService8 {
    enum Type{typeA, typeB, typeC}
    private ConcurrentHashMap<Type, Long> calls = new ConcurrentHashMap<>();
    void doImportantStuff(Data data, Type type) { 
        incrementCalls(type);
    }
    private void incrementCalls(Type type) {
        calls.compute(type, { key, value -> value == null ? 1 : value + 1 });
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Ja. Nach Regel 2 (Transitivität) ist die Klasse threadsicher, wenn ConcurrentHashMap#compute threadsicher ist. Die Doku sagt hierzu:

The entire method invocation is performed atomically.

Die ConcurrentHashMap#compute Javadoc versichert, dass die Methode atomar, also an einem Stück ausgeführt wird und damit threadsicher ist.

Im Prinzip wird hier eine Berechnungsfunktion (key, value) -> value == null ? 1 : value + 1 als Parameter übergeben und innerhalb einer (wie auch immer) abgesicherten Methode ausgeführt.

Im Folgenden eine abgewandelte Klasse aus produktivem Code.

class ImportantService9 {
    enum Type{typeA, typeB, typeC}
    private ConcurrentHashMap<Type, Long> cache = new ConcurrentHashMap<>();
    void doImportantStuff(Data data, Type type) {
        initCacheIfNecessary();
        updateCache(typeA, ThreadLocalRandom.current().nextInt(2));
        List<Long> values = findByValueGreaterThanOrEqual(3);
    }
    private void initCacheIfNecessary() { 
        synchronized (cache) {
            if (cache.isEmpty()){
                cache.put(Type.typeA, 0L);
                cache.put(Type.typeB, 1L);
                cache.put(Type.typeC, 2L);
            }
        }
    }
    private void updateCache(Type type, long value) { 
        try {
            cache.put(type, value);
        } catch (IllegalArgumentException e) {
            cache.remove(type);
        }
    }
    private List<Long> findByValueGreaterThanOrEqual(long minValue) { 
        return cache.values().stream().filter(value -> value >= minValue).toList();
    }
}

Ist diese Klasse threadsicher?

Threadsicher oder nicht?
Threadsicher oder nicht?

 
 
 

Die einzige Instanzvariable, die geändert wird, ist cache. Also gilt es diese genauer zu überprüfen. Alle Stellen, die darauf Zugreifen müssen unter die Lupe genommen werden.

private void initCacheIfNecessary() { 
    synchronized (cache) {
        if (cache.isEmpty()){
            cache.putAll(Map.of(Type.typeA, 0L, Type.typeB, 1L, Type.typeC, 2L);
        }
    }
}

Der Code in dem der Cache geprüft und befüllt wird, ist in einem gesperrten Block. Als Lock dient die Map selber. Sieht gut aus.

private void updateCache(Type type, long value) { 
    try {
        cache.put(type, value);
    } catch (IllegalArgumentException e) {
        cache.remove(type);
    }
}

Hier wird etwas in den Cache hinzugefügt oder entfernt. Es gibt keinen synchronized Block. Damit kann ein Thread ein Update durchführen, während die Initialisierung des Caches läuft und damit inkonsistenzen erzeugen. Schauen wir dennoch weiter.

Sichert uns wenigstens ConcurrentHashMap zu, dass die Methoden put, get und remove atomar ausgeführt werden?

However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access.

Retrieval operations (including get) generally do not block, so may overlap with update operations (including put and remove). Retrievals reflect the results of the most recently completed update operations holding upon their onset. ConcurrentHashMap is “concurrent”. A concurrent collection is thread-safe, but not governed by a single exclusion lock. In the particular case of ConcurrentHashMap, it safely permits any number of concurrent reads as well as a tunable number of concurrent writes. “Synchronized” classes can be useful when you need to prevent all access to a collection via a single lock, at the expense of poorer scalability. In other cases in which multiple threads are expected to access a common collection, “concurrent” versions are normally preferable. And unsynchronized collections are preferable when either collections are unshared, or are accessible only when holding other locks.

Es ist also etwas komplexer angelegt, aber nehmen wir an, dass get und put atomar sind.

initCacheIfNecessary benutzt die Map als Lock. Was benutzt cache.get / put als Lock?

Laut Javadoc: Es findet nicht immer ein Locking statt.

Sourcecode: 11 synchronized Blöcke, alle auf einzelne Knoten der Map (ConcurrentHashMap.Node)

Stellt man sich vor, dass die Map als eine Baumstruktur realisiert ist, macht das auch Sinn. Man braucht nicht den kompletten Baum sperren, wenn man etwas ändern will, es reicht einen Teilbaum zu sperren (Segmentation).

Für uns heißt das, dass wir zwei verschiedene Locks haben und damit initCacheIfNecessary und die Codestellen mit cache.get/put von zwei verschiedenen Threads ausgeführt werden können. Das ist nicht threadsicher nach Regel 3.2.

Um es threadsicher zu machen, könnte man jedoch ein synchronized (cache) um das get und das put machen.

Schauen wir noch in die letzte Methode rein, welche cache verwendet:

private List<Long> findByValueGreaterThanOrEqual(long minValue) { 
    return cache.values().stream().filter(value -> value >= minValue).toList();
}

Dies ist wieder ein komplexer Abruf. Die erste Information die uns interessiert: Was passiert beim Aufruf von values()?

Returns a Collection view of the values contained in this map. The collection is backed by the map, so changes to the map are reflected in the collection, and vice- versa.

Das klingt schon nicht optimal, da Änderungen an anderen Stellen unsere Collection ändern könnten. Normalerweise gäbe das eine ConcurrentModificationException.

Most concurrent Collection implementations (including most Queues) also differ from the usual java.util conventions in that their Iterators and Spliterators provide weakly consistent rather than fast-fail traversal:

  • they may proceed concurrently with other operations
  • they will never throw ConcurrentModificationException
  • they are guaranteed to traverse elements as they existed upon construction exactly once, and may (but are not guaranteed to) reflect any modifications subsequent to construction.

Es bleibt also kompliziert.

Eine einfache Lösung hierfür wäre wieder ein synchronized-Block.

private List<Long> findByValueGreaterThanOrEqual(long minValue) {
    final List<NodeDto> values;
    synchronized(cache) {
        values = new ArrayList(cache.values());        
    }
    return values.stream().filter(value -> value >= minValue).toList();
}

Da ein synchronized für die Ausführung des kompletten Blocks alle bis auf einen Thread ausschließt, kann das auf die Performanz gehen, wenn man komplexe oder lange Berechnungen in einem synchronized-Block ausführt. Das umgehen wir hier, in dem wir uns im synchronized-Block nur eine Arbeitskopie der Daten in eine eigene Liste erstellen.

Regel 4 - Make it short #

Regel 4 - Make it short

Synchronisierte Blöcke reduzieren die Performanz und sollten möglichst kurz in der Ausführungszeit sein.

Ende? #

Haben wir damit alles abgedeckt?

Nein. Aussen vor habe ich gelassen:

  • Verwendung Thread Klasse Deadlocks
  • Das Schlüsselwort volatile und
  • CPU Caching
  • Umsetzung von aktivem und passivem Warten via wait und notify
  • Hilfsklassen aus java.concurrent.lock
  • Hilfsklassen aus java.concurrent, wie CyclicBarrier, CountDownLatch, Semaphore, Future etc.
  • Hilfsklassen aus java.lang, wie ThreadLocal und damit verbundene Techniken zur Threadsicherheit
  • Serialisierung und Locks
  • Ungeeignete Lockobjekte
  • Testing mit Nebenläufigkeit
  • Inherent threadsichere Konstruktoren
  • Viele weitere Fehler, die man machen kann

Übersicht aller Regeln #

Regel 0 - Atomarität: Jedes Statement in Java (inklusive Zuweisungen und Increments) ist nicht threadsicher.

Regel 1 - Shared Resource: Sobald eine lokale Variable in einer Klasse verwendet und verändert wird, kann ein Threadsicherheitsproblem auftauchen.

Regel 1.1 - Immutables: Aus Regel 1 folgt: Unveränderliche Objekte sind per se Threadsicher.

Regel 1.2 - Threadlocal: Aus Regel 1 folgt: Pro Thread definierte Zustände, die nur von diesem verändert werden können, sind per se threadsicher.

Regel 1.2.1 - Method Scoped: Aus Regel 1.2 folgt: Innerhalb von Methoden definierte Variablen, welche diese Methode nicht verlassen, können nur von dem Thread verändert werden, der sie initialisiert hat und sind daher Threadsicher.

Regel 1.2.2 - Functional: Aus Regel 1.2.1 resultiert: Funktionale Methoden (Methoden, die alle benötigte Informationan als unveränderliche Paramteter erhalten) sind per se threadsicher.

Regel 2 - Transitivität: Regel 1 trifft auch auf alle Objekte in einer Klasse zu, die verwendet werden, wenn wir nicht wissen, ob diese Objekte selber Variablen haben, die sie verändern.

Regel 2.1 - Know your dependencies: Bei der Nutzung von anderen Klassen als lokale Objekte vergewissern, dass diese threadsicher sind.

Regel 3 - Synchronisationlock: Synchronisation findet immer auf einem Objekt statt (Monitorlock, Intrinsic Lock, Lock, Semaphore).

Regel 3.1 - Synchronisation auf this: synchronized an nicht-statischen Methoden entspricht synchronized (this).

Regel 3.2 - Verschiedene Locks, verschiedene Threads: Verschiedene Locks bedeuten je ein verschiedener Thread kann sich in den jeweiligen gesperrten Bereichen befinden. Als Lock kann jedes beliebige Objekt verwendet werden.

Regel 3.3 - Synchronisation bei static: synchronized an statischen Methoden entspricht synchronized (MeineKlasse.class).

Regel 4 - Make it short: Synchronisierte Blöcke reduzieren die Performanz und sollten möglichst kurz in der Ausführungszeit sein.