Modular Verification of Synchronization with Reentrant Locks Tevfik Bultan Fang Yu Department of Computer Science University of California Santa Barbara, CA, USA Aysu Betin Can Informatics Institute Middle East Technical University Ankara, Turkey Concurrent programming with Locks • A common synchronization approach in concurrent programming is to use locks – A thread that wants to access a shared resource first acquires a lock – After the thread is done with accessing the shared resource it releases the lock • Some locks are reentrant – A reentrant lock can be acquired multiple times by the same thread without releasing it – A reentrant lock is released by a thread when the number of acquire and release operations executed by that thread become equal Concurrent Programming in Java • Java programs use lock based synchronization • The default synchronization mechanism in Java uses the synchronized keyword and is based on reentrant locks • The java.util.concurrent package provides specialized locking mechanism such as: ReentrantLock, ReentrantReadWriteLock • Java programmers can write their own synchronization policies using Java synchronization operations: synchronized, wait, notify, notifyAll Correctness • How can we check that a concurrent program that uses locks does not have synchronization errors? Correctness • How can we check that a concurrent program that uses locks does not have synchronization errors? 1. We have to check that the lock (i.e., the synchronization policy) is implemented correctly • We call this Lock Behavior Verification Correctness • How can we check that a concurrent program that uses locks does not have synchronization errors? 1. We have to check that the lock (i.e., the synchronization policy) is implemented correctly • We call this Lock Behavior Verification 2. We need to make sure that each concurrent thread in the program uses the locks correctly • We call this Lock Interface Verification Concurrent Program Model • We assume that a concurrent program consists – A set of threads – Shared variables • Modifiable only via shared operations – Lock variables • Modifiable only via lock operations – Local variables for each thread • Modifiable only by one thread via local operations An Example • Consider a concurrent program that uses a read-write lock to protect access to a shared variable • Shared operations: – read, write • Lock operations: – read_enter, read_exit, write_enter, write_exit Lock Operations • We partition the lock operations to – Acquire (A1, A2, …, An) and Release (R1, R2, … , Rn) operations where the Al and Rl correspond to acquire and release operations for lock l • For the Read-Write lock – A1 = {read_enter} and R1 = {read_exit} – A2 = {write_enter} and R2 = {write_exit} • We can think of the Read-Write lock as consisting of – one read lock (lock with index 1 above) and – one write lock (lock with index 2 above) where both of these locks are implemented using the same lock variables Reentrant Locks • We model reentrant locks as follows: – An acquire operation in Al executed by thread t should not block if • the number of operations from Al that have been executed by thread t is greater than • the number of operations from Rl that have been executed by thread t • We model this behavior by keeping a reentry count for each lock-thread pair – Reentry count for thread t and lock l is incremented by one when thread t executes an operation in Al – Reentry count for thread t and lock l is decremented by one when thread t executes an operation in Rl Lock Interfaces • A lock interface defines the acceptable call sequences for each thread that uses the lock – In other words: A lock interface specifies the correct execution ordering for lock and shared operations for a single thread • For example, before a thread calls the read operation, it must have called the read_enter operation one more time than the read_exit operation – Note that this type of constraints cannot be specified using finite state machines Lock interfaces • We specify lock interfaces as extended finite state machines (EFSMs) • In the lock interface EFSM there is a reentry count for each lock keeping track of the difference between number of acquire and release calls for that lock • Each transition in the EFSM is labeled by a lock operation or a shared operation – and can also have a guarded command updating the reentry counts Example Lock Interface read_enter cr:= cr+1 read read_enter cr:= cr+1 read_exit [cr=1] cr:= cr-1 write_exit [cw=1] cw:= cw-1 write_enter cw:= cw+1 cr and cw are reentry counts for read and write locks, respectively read_exit [cr>1] cr:= cr-1 write_enter cw:= cw+1 read write write_exit [cw>1] cw:= cw+1 Lock Behavior • We assume that locks are implemented using guarded commands • An example Read-Write lock implementation integer nr; boolean busy; Lock variables initial: !busy and nr=0; Lock operations read_enter: [!busy] nr := nr+1; read_exit: nr := nr-1; write_enter: [!busy && nr=0] busy := true; write_exit: busy := false; Lock Behavior Machine • Lock behavior machine consists of – The lock behavior specification, and – One finite state machine for each thread • This finite state machine keeps track of the set of locks held by that thread • Unlike the lock interface machine, these are finite state machines without the reentry counts read_enter read_exit write_exit write_enter Lock Behavior Machine • While checking the lock specification we do not need to keep track of the reentry counts – The values of lock variables change only in transitions that change a reentry count from 0 to 1 or from 1 to 0 – The acquire and release calls that do change a re-entry count from 0 to 1 or from 1 to 0 can be abstracted away as far as lock behavior is concerned read_enter cr=0 cw=0 cr≥1 cw=0 read_exit write_exit write_enter cr=0 cw≥1 Verification Framework • Interface Verification: – Check that each thread in the concurrent program calls the lock operations and shared operations according to the lock interface machine • Behavior Verification: – Check that lock specification is correct • Specify ACTL properties about the lock behavior and check them on the lock behavior machine Verification Framework • We developed several helper classes in Java which allow developers to – Write lock behavior specifications using guarded commands – Write lock interface specifications as finite state machines • The helper classes we provide implement the guarded commands specified by the user using Java synchronization primitives • We also wrote tools that – Extract the lock behavior machine for behavior verification – Isolate each thread based on the lock interface specifications for interface verification Verification Framework Lock Classes Concurrent Program Thread Thread Thread Classes Verification Framework Lock Classes Concurrent Program Thread Thread Thread Classes Lock Behavior Machine Verification Framework Lock Classes Concurrent Program Thread Thread Thread Classes Lock Behavior Machine Behavior Verification Counting Abstraction Action Language Verifier Verification Framework Lock Behavior Machine Lock Classes Counting Abstraction Concurrent Program Lock Interface Machine Thread Thread Thread Classes Behavior Verification Thread Isolation Thread Class Action Language Verifier Verification Framework Lock Behavior Machine Lock Classes Behavior Verification Action Language Verifier Counting Abstraction Concurrent Program Lock Interface Machine Thread Thread Thread Classes Interface Verification Java Path Finder Thread Isolation Thread Class Modular Verification • Modularity 1 – Interface and behavior verification are done separately • Assume guarantee reasoning: We verify the lock behavior assuming that the threads obey the lock interfaces and then we check interface conformance • Modularity 2 – Interface verification is done one thread at a time • Thread-modular interface verification Modular Design / Modular Verification Locks Shared Data Thread n Thread 2 Thread 1 Concurrent Program Modular Design / Modular Verification Thread n Thread 2 Thread 1 Concurrent Program Locks Shared Data Lock Behavior Modular Behavior Verification Modular Design / Modular Verification Thread Modular Interface Verification Thread 1 Thread 2 Thread n Thread n Thread 2 Thread 1 Concurrent Program Interface Machine Interface Machine Interface Machine Locks Shared Data Lock Behavior Modular Behavior Verification Behavior Verification • Analyzing lock behavior by checking ACTL properties about the lock behavior – Verify the lock properties assuming that all the threads adhere to the lock interface • Behavior verification with Action Language Verifier – An infinite state symbolic CTL model checker (uses conservative approximations, widening) – We wrote a translator which translates lock classes written using guarded commands to Action Language – Using counting abstraction we can check the lock behavior with respect to arbitrary number of threads Arbitrary Number of Threads? • What if we wish to check the read-write lock with respect to arbitrary number of threads? • Counting abstraction – Create an integer variable for each thread state – Each variable counts the number of threads in a particular state – Generate updates and guards for these variables based on the specification • Counting abstraction is automated Verification of Read-Write Lock with ALV Integers Booleans Time (seconds) Memory (Mbytes) RW-4 1 5 0.05 6.6 RW-8 1 9 0.09 7 RW-16 1 17 0.21 8 RW-32 1 33 0.56 10.8 RW-64 1 65 1.77 20.6 RW-P 7 1 0.06 9.1 Interface Verification • A thread is correct with respect to a lock interface if all the call sequences generated by the thread can also be generated by the lock interface machine – Checks if all the threads invoke lock methods in the order specified in the interfaces – Checks if the threads access shared data only at the correct interface states • Interface verification with Java PathFinder – Verify Java implementations of threads – Correctness criteria are specified as assertions (if a thread calls a lock operation or shared operation that it should not call, an assertion violation occurs) • Look for assertion violations – Thread-modular verification with thread Isolation Thread Isolation: Part 1 • Interaction among threads • Threads can interact with each other in only two ways: – Executing lock operations – Executing shared data operations • To isolate the threads – Replace locks with lock interface state machines – Replace shared data with shared stubs Thread Isolation: Part 2 • Interaction among a thread and its environment • Modeling thread’s call to its environment with stubs – File I/O, updating GUI components, socket operations, RMI call to another program • Replace with pre-written or generated stubs • Modeling the environment’s influence on threads with drivers – Thread initialization, RMI events, GUI events • Enclose with drivers that generate all possible events that influence controller access • These steps typically lead to unsound, bounded verification A Case Study • Automated Airspace Concept by NASA researchers automates the decision making in air traffic control • The most important challenge is achieving high dependability • Automated Airspace Concept includes a failsafe short term conflict detection component called Tactical Separation Assisted Flight Environment (TSAFE) – It is responsible for detecting conflicts in flight plans of the aircraft within 1 minute from the current time – Dependability of this component is even more important than the dependability of the rest of the system – It should be a smaller, isolated component compared to the rest of the system so that it can be verified TSAFE TSAFE functionality: 1. Display aircraft position 2. Display aircraft planned route 3. Display aircraft future projected route trajectory 4. Show conformance problems TSAFE Architecture User Radar feed <<TCP/IP>> Feed Parser Server Client Flight Database EventThread <<RMI>> Graphical Client Computation 21,057 lines of code with 87 classes Timer Reengineering TSAFE • Found all the synchronization statements in the code (synchronize, wait, notify, notifyAll) • Identified 6 shared objects protected by these synchronization statements • Re-implemented the synchronization using our own lock implementations – Used 2 instances of a reader-writer lock and 3 instances of a mutex lock for synchronization Interface Verification Performance Thread Time (seconds) Memory (MBytes) Server-RMI 177.85 63.15 Server-Event 11.62 22.14 Server-Feed 194.78 52.44 Client-RMI 11.82 26.54 Client-Event 451.54 82.55 Related Work • This work is based on our earlier work [Betin Can and Bultan ASE 2004, ASE 2005] – Earlier work did not consider re-entrant locks • There has been earlier work on analyzing Java’s reentrant locking mechanisms [Abraham-Mumm et al. FOSSACS 2002, Haack et al. APLAS 2008] – We focus on custom lock implementations and their usage • Thread modular verification [Flanagan and Qadeer SPIN 2003] has been proposed before • There has been many work on modular reasoning based on interfaces [e.g. Chakrabarti et al. CAV 2002] • We apply these general concepts to checking reentrant locks Conclusions • Verification of Re-entrant locks can be done modularly – Separate interface verification from behavior verification using interface machines • Interface verification – Makes sure that each thread calls the re-entrant locks according to the lock interface specification – Interface verification can be done thread-modularly • Behavior verification – Makes sure that the lock behavior is correct with respect to the given CTL properties – During behavior verification re-entry counts can be abstracted away – Using counting abstraction lock behavior can be checked with respect to arbitrary number of threads Conclusions • Behavior verification with ALV – Verification of basic lock specifications such as readwrite locks using the infinite state model checking techniques is feasible • Interface verification with JPF – Thread isolation and environment generation are challenging problems that are hard to do fully automatically • Require writing stubs and drivers THE END • Questions? – Please send your questions to: bultan@cs.ucsb.edu