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  }