github.com/mitranim/gg@v0.1.17/conc.go (about) 1 package gg 2 3 import ( 4 "context" 5 "sync" 6 ) 7 8 /* 9 Tiny shortcut for gradually building a list of funcs which are later to be 10 executed concurrently. This type's methods invoke global funcs such as `Conc`. 11 Compare `ConcRaceSlice`. 12 */ 13 type ConcSlice []func() 14 15 // If the given func is non-nil, adds it to the slice for later execution. 16 func (self *ConcSlice) Add(fun func()) { 17 if fun != nil { 18 *self = append(*self, fun) 19 } 20 } 21 22 // Same as calling `Conc` with the given slice of funcs. 23 func (self ConcSlice) Run() { Conc(self...) } 24 25 // Same as calling `ConcCatch` with the given slice of funcs. 26 func (self ConcSlice) RunCatch() []error { return ConcCatch(self...) } 27 28 /* 29 Runs the given funcs sequentially rather than concurrently. Provided for 30 performance debugging. 31 */ 32 func (self ConcSlice) RunSeq() { 33 for _, fun := range self { 34 if fun != nil { 35 fun() 36 } 37 } 38 } 39 40 /* 41 Shortcut for concurrent execution. Runs the given functions via `ConcCatch`. 42 If there is at least one error, panics with the combined error, adding a stack 43 trace pointing to the call site of `Conc`. 44 */ 45 func Conc(val ...func()) { TryAt(Errs(ConcCatch(val...)).Err(), 1) } 46 47 /* 48 Concurrently runs the given functions. Catches their panics, converts them to 49 `error`, and returns the resulting errors. The error slice always has the same 50 length as the number of given functions. Does NOT generate stack traces or 51 modify the resulting errors in any way. The error slice can be converted to 52 `error` via `ErrMul` or `Errs.Err`. The slice is returned as `[]error` rather 53 than `Errs` to avoid accidental incorrect conversion of empty `Errs` to non-nil 54 `error`. 55 */ 56 func ConcCatch(val ...func()) []error { 57 switch len(val) { 58 case 0: 59 return nil 60 61 case 1: 62 return []error{Catch(val[0])} 63 64 default: 65 return concCatch(val) 66 } 67 } 68 69 func concCatch(src []func()) []error { 70 tar := make([]error, len(src)) 71 var gro sync.WaitGroup 72 73 for ind, fun := range src { 74 if fun == nil { 75 continue 76 } 77 gro.Add(1) 78 go concCatchRun(&gro, &tar[ind], fun) 79 } 80 81 gro.Wait() 82 return tar 83 } 84 85 func concCatchRun(gro *sync.WaitGroup, errPtr *error, fun func()) { 86 defer gro.Add(-1) 87 defer RecErr(errPtr) 88 fun() 89 } 90 91 /* 92 Concurrently calls the given function on each element of the given slice. If the 93 function is nil, does nothing. Also see `Conc`. 94 */ 95 func ConcEach[A any](src []A, fun func(A)) { 96 TryAt(Errs(ConcEachCatch(src, fun)).Err(), 1) 97 } 98 99 /* 100 Concurrently calls the given function on each element of the given slice, 101 returning the resulting panics if any. If the function is nil, does nothing and 102 returns nil. Also see `ConcCatch`. 103 */ 104 func ConcEachCatch[A any](src []A, fun func(A)) []error { 105 if fun == nil { 106 return nil 107 } 108 109 switch len(src) { 110 case 0: 111 return nil 112 113 case 1: 114 return []error{Catch10(fun, src[0])} 115 116 default: 117 return concEachCatch(src, fun) 118 } 119 } 120 121 func concEachCatch[A any](src []A, fun func(A)) []error { 122 tar := make([]error, len(src)) 123 var gro sync.WaitGroup 124 125 for ind, val := range src { 126 gro.Add(1) 127 go concCatchEachRun(&gro, &tar[ind], fun, val) 128 } 129 130 gro.Wait() 131 return tar 132 } 133 134 func concCatchEachRun[A any](gro *sync.WaitGroup, errPtr *error, fun func(A), val A) { 135 defer gro.Add(-1) 136 defer RecErr(errPtr) 137 fun(val) 138 } 139 140 // Like `Map` but concurrent. Also see `Conc`. 141 func ConcMap[A, B any](src []A, fun func(A) B) []B { 142 vals, errs := ConcMapCatch(src, fun) 143 TryMul(errs...) 144 return vals 145 } 146 147 /* 148 Like `Map` but concurrent. Returns the resulting values along with the caught 149 panics, if any. Also see `ConcCatch`. 150 */ 151 func ConcMapCatch[A, B any](src []A, fun func(A) B) ([]B, []error) { 152 if fun == nil { 153 return nil, nil 154 } 155 156 switch len(src) { 157 case 0: 158 return nil, nil 159 160 case 1: 161 val, err := Catch11(fun, src[0]) 162 return []B{val}, []error{err} 163 164 default: 165 return concMapCatch(src, fun) 166 } 167 } 168 169 func concMapCatch[A, B any](src []A, fun func(A) B) ([]B, []error) { 170 vals := make([]B, len(src)) 171 errs := make([]error, len(src)) 172 var gro sync.WaitGroup 173 174 for ind, val := range src { 175 gro.Add(1) 176 go concCatchMapRun(&gro, &vals[ind], &errs[ind], fun, val) 177 } 178 179 gro.Wait() 180 return vals, errs 181 } 182 183 func concCatchMapRun[A, B any](gro *sync.WaitGroup, tar *B, errPtr *error, fun func(A) B, val A) { 184 defer gro.Add(-1) 185 defer RecErr(errPtr) 186 *tar = fun(val) 187 } 188 189 // Partial application / thunk of `ConcMap`, suitable for `Conc`. 190 func ConcMapFunc[A, B any](tar *[]B, src []A, fun func(A) B) func() { 191 if IsEmpty(src) || fun == nil { 192 return nil 193 } 194 return func() { *tar = ConcMap(src, fun) } 195 } 196 197 /* 198 Shortcut for constructing `ConcRaceSlice` in a variadic call with parens rather 199 than braces. 200 */ 201 func ConcRace(src ...func(context.Context)) ConcRaceSlice { 202 return ConcRaceSlice(src) 203 } 204 205 /* 206 Tool for concurrent execution. Similar to `ConcSlice`, but with support for 207 context and cancelation. See `ConcRaceSlice.RunCatch` for details. 208 */ 209 type ConcRaceSlice []func(context.Context) 210 211 // If the given func is non-nil, adds it to the slice for later execution. 212 func (self *ConcRaceSlice) Add(fun func(context.Context)) { 213 if fun != nil { 214 *self = append(*self, fun) 215 } 216 } 217 218 /* 219 Shortcut. Runs the functions via `ConcRaceSlice.RunCatch`. If the resulting 220 error is non-nil, panics with that error, idempotently adding a stack trace. 221 */ 222 func (self ConcRaceSlice) Run(ctx context.Context) { 223 TryAt(self.RunCatch(ctx), 1) 224 } 225 226 /* 227 Runs the functions concurrently. Blocks until all functions complete 228 successfully, returning nil. If one of the functions panics, cancels the 229 context passed to each function, and immediately returns the resulting error, 230 without waiting for the other functions to terminate. In this case, the panics 231 in other functions, if any, are caught and ignored. Does NOT generate a stack 232 trace or modify the resulting error in any way. 233 */ 234 func (self ConcRaceSlice) RunCatch(ctx context.Context) (err error) { 235 switch len(self) { 236 case 0: 237 return nil 238 239 case 1: 240 fun := self[0] 241 if fun == nil { 242 return nil 243 } 244 245 defer RecErr(&err) 246 fun(ctx) 247 return 248 249 default: 250 return self.run(ctx) 251 } 252 } 253 254 func (self ConcRaceSlice) run(ctx context.Context) error { 255 var gro sync.WaitGroup 256 var errChan chan error 257 258 for _, fun := range self { 259 if fun == nil { 260 continue 261 } 262 263 if errChan == nil { 264 errChan = make(chan error, 1) 265 266 var cancel func() 267 ctx, cancel = context.WithCancel(ctx) 268 269 // Note: unlike `defer` in some other languages, `defer` in Go is 270 // function-scoped, not block-scoped. This will be executed once 271 // we're done waiting on the error channel. 272 defer cancel() 273 } 274 275 gro.Add(1) 276 go runConcCtx(&gro, errChan, fun, ctx) 277 } 278 279 // Happens when every element is nil and len >= 2. 280 if errChan == nil { 281 return nil 282 } 283 284 go closeConcCtx(&gro, errChan) 285 return <-errChan 286 } 287 288 func runConcCtx(gro *sync.WaitGroup, errChan chan error, fun func(context.Context), ctx context.Context) { 289 defer gro.Add(-1) 290 defer recSend(errChan) 291 fun(ctx) 292 } 293 294 func closeConcCtx(gro *sync.WaitGroup, errChan chan error) { 295 defer close(errChan) 296 gro.Wait() 297 } 298 299 func recSend(errChan chan error) { 300 err := AnyErr(recover()) 301 if err != nil { 302 SendOpt(errChan, err) 303 } 304 }