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.