| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package concurrency |
| |
| import ( |
| "errors" |
| "fmt" |
| "reflect" |
| ) |
| |
| // state records a state of the exploration of the space of all |
| // possible program states a concurrent test can encounter. The states |
| // of the exploration are organized using a tree data structure |
| // referred to as the "execution tree". Nodes of the execution tree |
| // represent different abstract program states and edges represent the |
| // different scheduling decisions that can be made at those states. A |
| // path from the root to a leaf thus uniquely represents an execution |
| // as a serialization of the scheduling decisions along the execution. |
| type state struct { |
| // depth identifies the depth of this node in the execution tree. |
| depth int |
| // parent records the pointer to the parent abstract state. |
| parent *state |
| // children maps threads to the abstract states that correspond to |
| // executing the next transitions of that thread. |
| children map[TID]*state |
| // explored records whether the subtree rooted at this state has |
| // been fully explored. |
| explored bool |
| // seeded records whether the state has been used to seed an |
| // exploration strategy. |
| seeded bool |
| // visited records whether the state has been visited |
| visited bool |
| // tid records the thread identifier of the thread whose transition |
| // lead to this state. |
| tid TID |
| // kind records the type of the transition that lead to this state. |
| kind transitionKind |
| // clock records the logical time of this state. |
| clock clock |
| // enabled records whether the transition that leads to this state |
| // is enabled. |
| enabled bool |
| // readSet records the identifiers of the abstract resources read by |
| // the transition that lead to this state. |
| readSet resourceSet |
| // writeSet records the identifiers of the abstract resources |
| // written by the transition that lead to this state. |
| writeSet resourceSet |
| } |
| |
| // newState is the state factory. |
| func newState(parent *state, tid TID) *state { |
| depth := 0 |
| if parent != nil { |
| depth = parent.depth + 1 |
| } |
| return &state{ |
| depth: depth, |
| children: make(map[TID]*state), |
| parent: parent, |
| tid: tid, |
| readSet: newResourceSet(), |
| writeSet: newResourceSet(), |
| clock: newClock(), |
| } |
| } |
| |
| // addBranch adds a branch described through the given sequence of |
| // choices to the execution tree rooted at this state. |
| func (s *state) addBranch(branch []*choice, seeds *stack) error { |
| if len(branch) == 0 { |
| s.explored = true |
| return nil |
| } |
| choice := branch[0] |
| if len(s.children) != 0 { |
| // Check for the absence of divergence. |
| if err := s.checkDivergence(choice.transitions); err != nil { |
| return err |
| } |
| } else { |
| // Add new children. |
| for _, transition := range choice.transitions { |
| child := newState(s, transition.tid) |
| child.clock = transition.clock |
| child.enabled = transition.enabled |
| child.kind = transition.kind |
| child.readSet = transition.readSet |
| child.writeSet = transition.writeSet |
| s.children[child.tid] = child |
| } |
| } |
| next, found := s.children[choice.next] |
| if !found { |
| return errors.New(fmt.Sprintf("invalid choice (no transition fo thread %d).", choice.next)) |
| } |
| next.visited = true |
| branch = branch[1:] |
| if err := next.addBranch(branch, seeds); err != nil { |
| return err |
| } |
| s.collectSeeds(choice.next, seeds) |
| s.compactTree() |
| return nil |
| } |
| |
| // approach identifies and enforces exploration of transition(s) |
| // originating from this state that near execution of the transition |
| // leading to the given state. |
| func (s *state) approach(other *state, seeds *stack) { |
| candidates := make([]TID, 0) |
| for tid, child := range s.children { |
| if child.enabled && child.happensBefore(other) { |
| candidates = append(candidates, tid) |
| } |
| } |
| if len(candidates) == 0 { |
| for _, child := range s.children { |
| if child.enabled && !child.seeded && !child.visited { |
| seeds.Push(child) |
| child.seeded = true |
| } |
| } |
| } else { |
| for _, tid := range candidates { |
| child := s.children[tid] |
| if child.seeded || child.visited { |
| return |
| } |
| } |
| tid := candidates[0] |
| child := s.children[tid] |
| seeds.Push(child) |
| child.seeded = true |
| } |
| } |
| |
| // checkDivergence checks whether the given transition matches the |
| // transition identified by the given thread identifier. |
| func (s *state) checkDivergence(transitions map[TID]*transition) error { |
| if len(s.children) != len(transitions) { |
| return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", len(s.children), len(transitions))) |
| } |
| for tid, t := range transitions { |
| child, found := s.children[tid] |
| if !found { |
| return errors.New(fmt.Sprintf("divergence encountered (no transition for thread %d)", tid)) |
| } |
| if child.enabled != t.enabled { |
| return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", child.enabled, t.enabled)) |
| } |
| if !child.clock.equals(t.clock) { |
| return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", child.clock, t.clock)) |
| } |
| if !reflect.DeepEqual(child.readSet, t.readSet) { |
| return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", child.readSet, t.readSet)) |
| } |
| if !reflect.DeepEqual(child.writeSet, t.writeSet) { |
| return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", child.writeSet, t.writeSet)) |
| } |
| } |
| return nil |
| } |
| |
| // collectSeeds uses dynamic partial order reduction |
| // (http://users.soe.ucsc.edu/~cormac/papers/popl05.pdf) to identify |
| // which alternative scheduling choices should be explored. |
| func (s *state) collectSeeds(next TID, seeds *stack) { |
| for tid, child := range s.children { |
| if tid != next && !s.mayInterfereWith(child) && !s.happensBefore(child) { |
| continue |
| } |
| for handle := s; handle.parent != nil; handle = handle.parent { |
| if handle.mayInterfereWith(child) && !handle.happensBefore(child) { |
| handle.parent.approach(child, seeds) |
| } |
| } |
| } |
| } |
| |
| // compactTree updates the exploration status of this state and |
| // deallocates its children map if all of its children are explored. |
| func (s *state) compactTree() { |
| for _, child := range s.children { |
| if (child.seeded || child.visited) && !child.explored { |
| return |
| } |
| } |
| s.children = nil |
| s.explored = true |
| } |
| |
| // generateStrategy generates a sequence of scheduling decisions that |
| // will steer an execution towards the state of the execution tree. |
| func (s *state) generateStrategy() []TID { |
| if s.parent == nil { |
| return make([]TID, 0) |
| } |
| return append(s.parent.generateStrategy(), s.tid) |
| } |
| |
| // happensBefore checks if the logical time of the transition that |
| // leads to this state happened before (in the sense of Lamport's |
| // happens-before relation) the logical time of the transition that |
| // leads to the given state. |
| func (s *state) happensBefore(other *state) bool { |
| return s.clock.happensBefore(other.clock) |
| } |
| |
| // mayInterfereWith checks if the execution of the transition that |
| // leads to this state may interfere with the execution of the |
| // transition that leads to the given state. |
| func (s *state) mayInterfereWith(other *state) bool { |
| if (s.kind == tMutexLock && other.kind == tMutexUnlock) || |
| (s.kind == tMutexUnlock && other.kind == tMutexLock) { |
| return false |
| } |
| if (s.kind == tRWMutexLock && other.kind == tRWMutexUnlock) || |
| (s.kind == tRWMutexUnlock && other.kind == tRWMutexLock) { |
| return false |
| } |
| if (s.kind == tRWMutexRLock && other.kind == tRWMutexUnlock) || |
| (s.kind == tRWMutexRUnlock && other.kind == tRWMutexLock) { |
| return false |
| } |
| for k, _ := range s.readSet { |
| if _, found := other.writeSet[k]; found { |
| return true |
| } |
| } |
| for k, _ := range s.writeSet { |
| if _, found := other.readSet[k]; found { |
| return true |
| } |
| } |
| for k, _ := range s.writeSet { |
| if _, found := other.writeSet[k]; found { |
| return true |
| } |
| } |
| return false |
| } |