task.go (2943B)
1 // Copyright 2024 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package ssa 6 7 import ( 8 "sync/atomic" 9 ) 10 11 // Each task has two states: it is initially "active", 12 // and transitions to "done". 13 // 14 // tasks form a directed graph. An edge from x to y (with y in x.edges) 15 // indicates that the task x waits on the task y to be done. 16 // Cycles are permitted. 17 // 18 // Calling x.wait() blocks the calling goroutine until task x, 19 // and all the tasks transitively reachable from x are done. 20 // 21 // The nil *task is always considered done. 22 type task struct { 23 done chan unit // close when the task is done. 24 edges map[*task]unit // set of predecessors of this task. 25 transitive atomic.Bool // true once it is known all predecessors are done. 26 } 27 28 func (x *task) isTransitivelyDone() bool { return x == nil || x.transitive.Load() } 29 30 // addEdge creates an edge from x to y, indicating that 31 // x.wait() will not return before y is done. 32 // All calls to x.addEdge(...) should happen before x.markDone(). 33 func (x *task) addEdge(y *task) { 34 if x == y || y.isTransitivelyDone() { 35 return // no work remaining 36 } 37 38 // heuristic done check 39 select { 40 case <-x.done: 41 panic("cannot add an edge to a done task") 42 default: 43 } 44 45 if x.edges == nil { 46 x.edges = make(map[*task]unit) 47 } 48 x.edges[y] = unit{} 49 } 50 51 // markDone changes the task's state to markDone. 52 func (x *task) markDone() { 53 if x != nil { 54 close(x.done) 55 } 56 } 57 58 // wait blocks until x and all the tasks it can reach through edges are done. 59 func (x *task) wait() { 60 if x.isTransitivelyDone() { 61 return // already known to be done. Skip allocations. 62 } 63 64 // Use BFS to wait on u.done to be closed, for all u transitively 65 // reachable from x via edges. 66 // 67 // This work can be repeated by multiple workers doing wait(). 68 // 69 // Note: Tarjan's SCC algorithm is able to mark SCCs as transitively done 70 // as soon as the SCC has been visited. This is theoretically faster, but is 71 // a more complex algorithm. Until we have evidence, we need the more complex 72 // algorithm, the simpler algorithm BFS is implemented. 73 // 74 // In Go 1.23, ssa/TestStdlib reaches <=3 *tasks per wait() in most schedules 75 // On some schedules, there is a cycle building net/http and internal/trace/testtrace 76 // due to slices functions. 77 work := []*task{x} 78 enqueued := map[*task]unit{x: {}} 79 for i := 0; i < len(work); i++ { 80 u := work[i] 81 if u.isTransitivelyDone() { // already transitively done 82 work[i] = nil 83 continue 84 } 85 <-u.done // wait for u to be marked done. 86 87 for v := range u.edges { 88 if _, ok := enqueued[v]; !ok { 89 enqueued[v] = unit{} 90 work = append(work, v) 91 } 92 } 93 } 94 95 // work is transitively closed over dependencies. 96 // u in work is done (or transitively done and skipped). 97 // u is transitively done. 98 for _, u := range work { 99 if u != nil { 100 x.transitive.Store(true) 101 } 102 } 103 }