V tomto článku se podíváme na data race. Data
race je synchronizační chyba, která se objevuje ve
vícevláknových programech. Řekneme si, kdy tato chyba
nastává, ukážeme si pár příkladů a představíme si
nástroj, kterým lze data race detekovat.
Pokud dvě vlákna přistupují ke sdílené proměnné,
alespoň jedno vlákno zapisuje a mezi přístupy není
žádné uspořádání pomocí relace happens-before, nastává data race.
Chybějící uspořádání může způsobit velmi překvapivé
chování programu. Např. pokud a, b mají hodnotu 0 a vlákno 1 vykoná
a = 1;
x = b;
System.out.println("x = " + x);
a vlákno 2 vykoná
b = 2;
y = a;
System.out.println("y = " + y);
je zřejmé, že na výstupu můžeme dostat (v
libovolném pořadí) x = 0 a y = (vlákno 1 provedlo přiřazení dříve než
1
vlákno 2), x = 2 a y = 0 (vlákno 2 provedlo přiřazení dříve než vlákno 1)
nebo x = 2 a y = 1
(vlákno 1 provedlo a = 1 a pak vlákno
2 provedlo b = 2). Překvapivě ovšem
můžeme dostat také x = 0 a y =, protože chybějící uspořádání při přístupu
0
k proměnné a může způsobit, že vlákno
2 nevidí změnu, kterou provedlo vlákno 1 (a
analogicky vlákno 1 nemusí vidět změnu, kterou
provedlo vlákno 2 na proměnné b).
Máme-li v programu data race, jde
obvykle o chybu, která je obtížně detekovatelná,
protože se může projevit jen někdy (např. pouze na
některých architekturách). Podívejme se na příklad. V
následujícím kódu chybí uspořádání při přístupu k
proměnné x.
public class Increment implements Runnable {
int x;
@Override
public void run() {
x++;
}
}
public class Test1 {
public static void main(String[] args) {
Runnable r = new Increment();
new Thread(r).start();
new Thread(r).start();
}
}
Chceme-li se chybě data race vyhnout, je
třeba zajistit, aby mezi každými dvěma přístupy ke
sdílené proměnné byla relace happens-before. Tuto relaci lze v programu zavést několika
způsoby. Např. pokud vlákno t1 spustí
vlákno t2, pak vše, co se vykonalo v
t1 před zavoláním t2.start(), je v relaci happens-before s tím, co proběhne v t2. Jiný způsob, jak můžeme relaci zavést,
je použití klíčového slova synchronized nebo tříd z balíku java.util.concurrent. Podívejme se na
příklady. V následujícím kódu je uspořádání při
přístupu k proměnné x definováno
pomocí metody start() třídy java.lang.Thread.
public class Test2 {
public static void main(String[] args) {
Increment p = new Increment();
p.x = 1;
new Thread(p).start();
}
}
V dalším příkladu je uspořádání definováno pomocí
monitoru. Všimněte si, že i když je tento program
správně synchronizovaný, není určeno, v jakém pořadí
vlákna vykonají synchronizované sekce.
public class Decrement implements Runnable {
int x;
@Override
public void run() {
synchronized (this) {
x--;
}
}
}
public class Test3 {
public static void main(String[] args) {
Runnable r = new Decrement();
new Thread(r).start();
new Thread(r).start();
}
}
Dále se podíváme na možnosti detekce této chyby.
Algoritmy pro detekci lze rozdělit na statické a
dynamické. Statické algoritmy hledají chyby zkoumáním
zdrojového nebo bajtového kódu. Dynamické algoritmy
sbírají data za běhu programu a tato data
vyhodnocují. Mohou sledovat buď množiny zámků
(monitorů) nebo relaci happens-before.
Algoritmy, které sledují množiny zámků, jsou založeny
na předpokladu, že ve správně synchronizovaném
programu je přístup ke sdílené proměnné strážen
nějakým zámkem (monitorem). Detekce chybějící
synchronizace probíhá takto: při každém přístupu ke
sdílené proměnné zjistíme aktuální množinu zámků a
průnik této množiny s množinami zámků z předchozích
přístupů. Pokud je průnik prázdný, ohlásíme data
race. Výhodou tohoto přístupu je snadná
implementace, nevýhodou je poměrně velké množství
falešných hlášení. Algoritmy, které sledující relaci
happens-before, monitorují akce, jež tuto
relaci vytvářejí. Např. vstup do synchronizované
sekce, volání metody start() nebo
návrat z metody join() na objektu typu
java.lang.Thread nebo metody lock() a unlock() na objektu
typu java.util.concurrent.locks.Lock.
Pokud detekují dva přístupy bez relace happens-before, ohlásí data race.
Tyto algoritmy dávají přesnější výsledky než
algoritmy založené na množinách zámků, jsou však
náročnější na implementaci.
K detekci data race v programu lze
použít projekt JaDaRD
(Java Data-Race Detector). JaDaRD je tzv. JVM agent,
což je dynamická knihovna, kterou JVM přilinkuje při
spuštění. Za běhu programu JaDaRD monitoruje přístupy
ke sdíleným proměnným a sleduje zámky používané při
těchto přístupech. Umí sledovat také metody start() a join() na objektech
java.lang.Thread (přepínač -watchThreads) a metody lock() a unlock() na objektech java.util.concurrent.locks.Lock (přepínač
-watchConcurrent). Agenta spustíme
pomocí argumentů na příkazové řádce. Pro detekci data race v balíku simple
můžeme použít např.
java -agentpath:jadard.dll=-stackTrace-trie-watchThreads-loggerLevel=WARN-package=simple simple.Test1
Argument -stackTrace způsobí výpis
obsahu zásobníku při nalezeném data race a
-trie zapíná efektivní ukládání
informací do TRIE. Více na wiki projektu.
