440 likes | 570 Vues
This work addresses the challenge of deadlocks in concurrent programming, highlighting their prevalence and difficulty in detection. We propose a new deadlock detection tool that combines static and dynamic program analysis techniques. Our method aims to rapidly identify potential deadlocks with high accuracy and no false positives by leveraging imprecise analysis to find abstract states before conducting targeted tests. This approach is designed to scale effectively with large programs, improving developers' ability to diagnose and resolve deadlocks in multi-threaded environments.
E N D
A Randomized Dynamic Program Analysis for Detecting Real Deadlocks KoushikSen CS 265
What is a Deadlock? • An unintended condition in a shared-memory, multi-threaded program in which: • a set of threads blocks forever • because each thread in the set waits to acquire alock being held by another thread in the set • This work: ignore other causes (e.g., wait/notify) • Example // thread t2 sync (l2) { sync (l1) { … } } l1 // thread t1 sync (l1) { sync (l2) { … } } t1 t2 l2
Motivation • Today’s concurrent programs are rife with deadlocks • 6,500/198,000 (~ 3%) of bug reports in Sun’s bug database at http://bugs.sun.com are deadlocks • Deadlocks are difficult to detect • Usually triggered non-deterministically, on specific thread schedules • Fail-stop behavior not guaranteed (some threads may be deadlocked while others continue to run) • Fixing other concurrency bugs like races can introduce new deadlocks • Our past experience with reporting races: developers often ask for deadlock checker
Our Goal • Build a deadlock detection tool that • Scales to very large programs • Finds deadlocks quickly • Has no false positives • Shows a real execution exhibiting a deadlock
Related Work: Program Analysis • Static program analysis (e.g., Engler & Ashcraft SOSP’03; Williams-Thies-Ernst ECOOP’05, Naik et al. ICSE’09) + Examines all possible program behavior • Often reports many false positives • Type systems (e.g., Boyapati-Lee-Rinard OOPSLA’02) - Annotation burden often significant • Model checking (e.g., SPIN, Verisoft, Java Pathfinder) - Does not currently scale beyond few KLOC - Not “directed” • Dynamic program analysis (e.g. GoodlockHavelund et al., Agrawal et al.) + Usually reports lesser false positives - Has false negatives
Related Work: Testing • Testing + Easy to deploy + Scales to large programs + No false positives • Same schedule gets tested many times • No effort to control thread scheduler - Often subtle thread schedules that exhibit deadlocks are not tested
In Summary • Static and dynamic program analyses have false positives • Testing is simple • No false positives • But, may miss subtle thread schedules that result in deadlocks
In Summary • Static and dynamic program analyses have false positives • Testing is simple • No false positives • But, may miss subtle thread schedules that result in deadlocks • Can we leverage program analysis to make testing quickly find real deadlocks?
Our Approach • Active Testing • Phase 1: Use imprecise static or dynamic program analysis to find “abstract” states where a potential deadlock can happen • Phase 2: “Direct” testing (or model checking) based on the “abstract” states obtained from phase 1
Example Thread2 foo(o2,o1,false) void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations s1: f1(); s2: f2(); } s3: synchronized(l1){ s4: synchronized(l2){ } } } Thread1 foo(o1,o2,true)
Testing Thread 1 Thread 2 Thread2 foo(o2,o1,false) Lock(o2) f1() Lock(o1) void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations s1: f1(); s2: f2(); } s3: synchronized(l1){ s4: synchronized(l2){ } } } Unlock(o1) f2() Unlock(o2) Thread1 foo(o1,o2,true)
Testing Thread 1 Thread 2 Thread2 foo(o2,o1,false) Lock(o2) f1() Lock(o1) void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations s1: f1(); s2: f2(); } s3: synchronized(l1){ s4: synchronized(l2){ } } } Unlock(o1) f2() Unlock(o2) Lock(o1) Lock(o2) Unlock(o2) Unlock(o1) Thread1 foo(o1,o2,true)
Testing Thread 1 Thread 2 Thread2 foo(o2,o1,false) Lock(o2) f1() Lock(o1) void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations s1: f1(); s2: f2(); } s3: synchronized(l1){ s4: synchronized(l2){ } } } Unlock(o1) f2() No deadlock detected Unlock(o2) Lock(o1) Lock(o2) Unlock(o2) Unlock(o1) Thread1 foo(o1,o2,true)
Deadlock Directed Testing Thread 1 Thread 2 Thread2 foo(o2,o1,false) Lock(o2) f1() Lock(o1) void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations f1(); f2(); } synchronized(l1){ synchronized(l2){ } } } Paused f2() Thread1 foo(o1,o2,true)
Deadlock Directed Testing Thread 1 Thread 2 Thread2 foo(o2,o1,false) Lock(o2) f1() Lock(o1) void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations f1(); f2(); } synchronized(l1){ synchronized(l2){ } } } Paused f2() Lock(o1) Lock(o2) Paused Thread1 foo(o1,o2,true)
Deadlock Directed Testing Thread 1 Thread 2 Thread2 foo(o2,o1,false) Lock(o2) f1() void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations f1(); f2(); } synchronized(l1){ synchronized(l2){ } } } f2() Lock(o1) Lock(o1) Lock(o2) Paused Paused Thread1 foo(o1,o2,true)
Deadlock Directed Testing Thread 1 Thread 2 Thread2 foo(o2,o1,false) Lock(o2) f1() void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations f1(); f2(); } synchronized(l1){ synchronized(l2){ } } } f2() Deadlock detected ! Lock(o1) Lock(o1) Lock(o2) Paused Paused Thread1 foo(o1,o2,true)
Preempting threads • How do we know where to pause a thread ?
Preempting threads • How do we know where to pause a thread ? • Use existing static or dynamic analyses to find potential deadlock cycles • Note that these analyses may report false deadlock cycles • Use “information” recorded for a deadlock cycle to decide where to pause a thread • We use a modified version of the Goodlock algorithm (iGoodlock) [Havelund et al, Agarwal et al]
iGoodlock • We consider dynamic instances of the following statements • c: Acquire(t,l) • c: Release(t,l) • c: Call(t,m) • c: Release(t,m) • c: o = new (t,o’,T)
iGoodlock • We consider dynamic instances of the following statements • c: Acquire(t,l) • c: Release(t,l) • c: Call(t,m) • c: Release(t,m) • c: o = new (t,o’,T)
Lock Dependency Relation • D is a subset of (T X 2L X L X C*) • (t,L,l,C) is in the lock dependency relation if
Lock Dependency Relation • D is a subset of (T X 2L X L X C*) • (t,L,l,C) is in the lock dependency relation if • thread t acquires lock l while holding the locks in the set L, and C is the sequence of labels of Acquire statements that were executed by t to acquire the locks in L ∪ {l}.
Lock Dependency Chain (t1,L1,l1,C1), (t2,L2,l2,C2), …, (tm,Lm,lm,Cm) is a lock dependency chain if
Lock Dependency Chain (t1,L1,l1,C1), (t2,L2,l2,C2), …, (tm,Lm,lm,Cm) is a lock dependency chain if • for all distinct i, j∈[1, m], ti ≠ tj • for all distinct i, j∈[1, m], li ≠ lj • for all i ∈ [1,m − 1], li ∈ Li+1, • for all distinct i,j ∈ [1,m], Li ∩ Lj = ∅.
Lock Dependency Chain (t1,L1,l1,C1), (t2,L2,l2,C2), …, (tm,Lm,lm,Cm) is a lock dependency chain if • for all distinct i, j∈[1, m], ti ≠ tj • for all distinct i, j∈[1, m], li ≠ lj • for all i ∈ [1,m − 1], li ∈ Li+1, • for all distinct i,j ∈ [1,m], Li ∩ Lj = ∅. • A chain is a deadlock if
Lock Dependency Chain (t1,L1,l1,C1), (t2,L2,l2,C2), …, (tm,Lm,lm,Cm) is a lock dependency chain if • for all distinct i, j∈[1, m], ti ≠ tj • for all distinct i, j∈[1, m], li ≠ lj • for all i ∈ [1,m − 1], li ∈ Li+1, • for all distinct i,j ∈ [1,m], Li ∩ Lj = ∅. • A chain is a deadlock if • lm∈ L1
Compute D on this trace Thread 1 Thread 2 Thread2 foo(o2,o1,false) f1() Acquire(o2) void foo(Object l1, Object l2, boolean flag) { if(flag) { // Long running computations s1: f1(); s2: f2(); } s3: synchronized(l1){ s4: synchronized(l2){ } } } Acquire(o1) Release(o1) f2() Release(o2) Acquire(o1) Acquire(o2) Release(o2) Release(o1) Thread1 foo(o1,o2,true)
Computing lock dependency chains • Compute Dk where D1 = D • for each d ∈D and τ ∈ Di • if d, τ is a lock dependency chain and a deadlock cycle then report a potential deadlock • if d, τ is a lock dependency chain and not a deadlock cycle • add d, τ to Di+1
Preempting threads • T1, o1, T2, o2 are runtime objects • How do we identify them across executions ?
Preempting threads • T1, o1, T2, o2 are runtime objects • How do we identify them across executions ? • Runtime addresses?
Preempting threads • T1, o1, T2, o2 are runtime objects • How do we identify them across executions ? • Runtime addresses? • No, they change across executions !
Preempting threads • T1, o1, T2, o2 are runtime objects • How do we identify them across executions ? • Runtime addresses? • No, they change across executions ! • Allocation sites where they were created ? • Yes, but different objects created at the same allocation site will all be treated as the same object
Preempting threads • T1, o1, T2, o2 are runtime objects • How do we identify them across executions ? • Runtime addresses? • No, they change across executions ! • Allocation sites where they were created ? • Yes, but different objects created at the same allocation site will all be treated as the same object • Can we do better?
Abstractions • Based on k-object sensitivity [Milanova et al] • Based on light-weight execution indexing [Xin et al]
Abstractions • 1 class C { • 2 void foo() { • 3 A a = new A(); • 4 a.bar(); • } • 6 } • 7 class A { • 8 void bar() { • 9 for(int j = 0; j < 5; j++) • 10 Object l = new Object(); • 11 } • 12 } • 13 main() { • 14 C c = new C(); • 15 for(inti = 0; i < 5; i++) • 16 c.foo(); • 17 }
Abstractions • 1 class C { • 2 void foo() { • 3 A a = new A(); • 4 a.bar(); • } • 6 } • 7 class A { • 8 void bar() { • 9 for(int j = 0; j < 5; j++) • 10 Object l = new Object(); • 11 } • 12 } • Abstraction of last • object created (k- • object sensitivity) : [10,3, 14] • Abstraction of last • object created • (execution indexing) : • [(10,5),(4,1),(16,5)] • 13 main() { • 14 C c = new C(); • 15 for(inti = 0; i < 5; i++) • 16 c.foo(); • 17 }
Implementation • Implemented in a prototype tool called DEADLOCKFUZZER for Java • Part of the CALFUZZER framework • Active testing [Sen PLDI 08, Park et al. FSE 08, Joshi et al. CAV 09] of concurrent programs • Instrument Java bytecode to • observe events during execution • control scheduler
Limitations • If DEADLOCKFUZZER does not reproduce a deadlock, we cannot say if the deadlock is a false positive or not • Jigsaw • Deadlocks reported by iGoodlock : 283 • Deadlocks reproduced by DEADLOCKFUZZER : 29 • Deadlocks confirmed as false positives : 18 • Rest : 236
Limitations • If DEADLOCKFUZZER does not reproduce a deadlock, we cannot say if the deadlock is a false positive or not • Jigsaw • Deadlocks reported by iGoodlock : 283 • Deadlocks reproduced by DEADLOCKFUZZER : 29 • Deadlocks confirmed as false positives : 18 • Rest : 236 Cannot say if they are real deadlocks or false warnings
Conclusion • DEADLOCKFUZZER is a practical deadlock detection tool that • Scales to large programs • Finds deadlock quickly • Finds real deadlocks • Complements static and dynamic analyses • Automatically confirms some of the real deadlocks