go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/sync/parallel/runner.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package parallel 16 17 import ( 18 "sync" 19 "sync/atomic" 20 ) 21 22 // WorkItem is a single item of work that a Runner will execute. The supplied 23 // function, F, will be executed by a Runner goroutine and the result will 24 // be written to ErrC. 25 // 26 // An optional callback method, After, may be supplied to operate in response 27 // to work completion. 28 type WorkItem struct { 29 // F is the work function to execute. This must be non-nil. 30 F func() error 31 // ErrC is the channel that will receive F's result. If nil or F panics, no 32 // error will be sent. 33 ErrC chan<- error 34 35 // After, if not nil, is a callback method that will be invoked after the 36 // result of F has been passed to ErrC. 37 // 38 // After is called by the same worker goroutine as F, so it will similarly 39 // consume one worker during its execution. 40 // 41 // If F panics, After will still be called, and can be used to recover from 42 // the panic. 43 After func() 44 } 45 46 func (wi *WorkItem) execute() { 47 if wi.After != nil { 48 defer wi.After() 49 } 50 51 err := wi.F() 52 if wi.ErrC != nil { 53 wi.ErrC <- err 54 } 55 } 56 57 // Runner manages parallel function dispatch. 58 // 59 // The zero value of a Runner accepts an unbounded number of tasks and maintains 60 // no sustained goroutines. 61 // 62 // Once started, a Runner must not be copied. 63 // 64 // Once a task has been dispatched to Runner, it will continue accepting tasks 65 // and consuming resources (namely, its dispatch goroutine) until its Close 66 // method is called. 67 type Runner struct { 68 // Sustained is the number of sustained goroutines to use in this Runner. 69 // Sustained goroutines are spawned on demand, but continue running to 70 // dispatch future work until the Runner is closed. 71 // 72 // If Sustained is <= 0, no sustained goroutines will be executed. 73 // 74 // This value will be ignored after the first task has been dispatched. 75 Sustained int 76 77 // Maximum is the maximum number of goroutines to spawn at any given time. 78 // 79 // If Maximum is <= 0, no maximum will be enforced. 80 // 81 // This value will be ignored after the first task has been dispatched. 82 Maximum int 83 84 // initOnce is used to ensure that the Runner is internally initialized 85 // exactly once. 86 initOnce sync.Once 87 // workC is the Runner's work item channel. 88 workC chan WorkItem 89 // dispatchFinishedC is closed when our dispatch loop has completed. This will 90 // happen after workC has closed and all outstanding dispatched work has 91 // finished. 92 dispatchFinishedC chan struct{} 93 } 94 95 // init initializes the starting state of the Runner. It must be called at the 96 // beginning of all exported methods. 97 func (r *Runner) init() { 98 r.initOnce.Do(func() { 99 r.workC = make(chan WorkItem) 100 r.dispatchFinishedC = make(chan struct{}) 101 102 go r.dispatchLoop(r.Sustained, r.Maximum) 103 }) 104 } 105 106 // dispatchLoop is run in a goroutine. It reads tasks from workC and executes 107 // them. 108 func (r *Runner) dispatchLoop(sustained int, maximum int) { 109 defer close(r.dispatchFinishedC) 110 111 if maximum > 0 { 112 spawnC := make(Semaphore, maximum) 113 r.dispatchLoopBody(sustained, spawnC.Lock, spawnC.Unlock) 114 spawnC.TakeAll() 115 return 116 } 117 var wg sync.WaitGroup 118 r.dispatchLoopBody(sustained, func() { wg.Add(1) }, wg.Done) 119 wg.Wait() 120 } 121 122 // dispatchLoopBody starts up to 'sustained' continuous goroutine, plus as many 123 // one-shot goroutines as 'before' allows. 124 func (r *Runner) dispatchLoopBody(sustained int, before, after func()) { 125 numSustained := 0 126 for { 127 before() 128 work, ok := <-r.workC 129 if !ok { 130 after() 131 return 132 } 133 134 if numSustained < sustained { 135 // Spawn a work goroutine to continue working asynchronously. 136 numSustained++ 137 go func() { 138 defer after() 139 work.execute() 140 for work := range r.workC { 141 work.execute() 142 } 143 }() 144 continue 145 } 146 // Still spawn a goroutine. 147 go func() { 148 defer after() 149 work.execute() 150 }() 151 } 152 } 153 154 // Close will instruct the Runner to not accept any more jobs and block until 155 // all current work is finished. 156 // 157 // Close may only be called once; additional calls will panic. 158 // 159 // The Runner's dispatch methods will panic if new work is dispatched after 160 // Close has been called. 161 func (r *Runner) Close() { 162 r.init() 163 164 close(r.workC) 165 <-r.dispatchFinishedC 166 } 167 168 // Run executes a generator function, dispatching each generated task to the 169 // Runner. Run returns immediately with an error channel that can be used to 170 // reap the results of those tasks. 171 // 172 // The returned error channel must be consumed, or it can block additional 173 // functions from being run from gen. A common consumption function is 174 // errors.MultiErrorFromErrors, which will buffer all non-nil errors into an 175 // errors.MultiError. Other functions to consider are Must and Ignore (in this 176 // package). 177 // 178 // Note that there is no association between error channel's error order and 179 // the generated task order. However, the channel will return exactly one error 180 // result for each generated task. 181 // 182 // If the Runner has been closed, this will panic with a reference to the closed 183 // dispatch channel. 184 func (r *Runner) Run(gen func(chan<- func() error)) <-chan error { 185 return r.runThen(gen, nil) 186 } 187 188 // runThen is a thin wrapper around Run that enables an after call function to 189 // be invoked when the generator has finished. 190 func (r *Runner) runThen(gen func(chan<- func() error), then func()) <-chan error { 191 r.init() 192 193 return runImpl(gen, r.workC, then) 194 } 195 196 // RunOne executes a single task in the Runner, returning with a channel that 197 // can be used to reap the result of that task. 198 // 199 // The returned error channel must be consumed, or it can block additional 200 // functions from being run from gen. A common consumption function is 201 // errors.MultiErrorFromErrors, which will buffer all non-nil errors into an 202 // errors.MultiError. Other functions to consider are Must and Ignore (in this 203 // package). 204 // 205 // If the Runner has been closed, this will panic with a reference to the closed 206 // dispatch channel. 207 func (r *Runner) RunOne(f func() error) <-chan error { 208 r.init() 209 210 errC := make(chan error) 211 r.workC <- WorkItem{ 212 F: f, 213 ErrC: errC, 214 After: func() { close(errC) }, 215 } 216 return errC 217 } 218 219 // WorkC returns a channel which WorkItem can be directly written to. 220 func (r *Runner) WorkC() chan<- WorkItem { 221 r.init() 222 return r.workC 223 } 224 225 // runImpl sets up a localized system where a generator generates tasks and 226 // dispatches them to the supplied work channel. 227 // 228 // After all tasks have been written to the work channel, then is called. 229 func runImpl(gen func(chan<- func() error), workC chan<- WorkItem, then func()) <-chan error { 230 errC := make(chan error) 231 taskC := make(chan func() error) 232 233 // Execute our generator method. 234 go func() { 235 defer close(taskC) 236 gen(taskC) 237 }() 238 239 // Read tasks from taskC and dispatch actual work. 240 go func() { 241 if then != nil { 242 defer then() 243 } 244 245 // Use a counter to track the number of active jobs. 246 // 247 // Add one implicit job for the outer task loop. This will ensure that if 248 // we will never hit 0 until all of our tasks have dispatched. 249 count := int32(1) 250 finish := func() { 251 if atomic.AddInt32(&count, -1) == 0 { 252 close(errC) 253 } 254 } 255 defer finish() 256 257 // Dispatch the tasks in the task channel. 258 for task := range taskC { 259 atomic.AddInt32(&count, 1) 260 workC <- WorkItem{ 261 F: task, 262 ErrC: errC, 263 After: finish, 264 } 265 } 266 }() 267 268 return errC 269 }