package async import ( "sync/atomic" "github.com/pkg/errors" ) //---------------------------------------- // Task // val: the value returned after task execution. // err: the error returned during task completion. // abort: tells Parallel to return, whether or not all tasks have completed. type Task func(i int) (val interface{}, abort bool, err error) type TaskResult struct { Value interface{} Error error } type TaskResultCh <-chan TaskResult type taskResultOK struct { TaskResult OK bool } type TaskResultSet struct { chz []TaskResultCh results []taskResultOK } func newTaskResultSet(chz []TaskResultCh) *TaskResultSet { return &TaskResultSet{ chz: chz, results: make([]taskResultOK, len(chz)), } } func (trs *TaskResultSet) Channels() []TaskResultCh { return trs.chz } func (trs *TaskResultSet) LatestResult(index int) (TaskResult, bool) { if len(trs.results) <= index { return TaskResult{}, false } resultOK := trs.results[index] return resultOK.TaskResult, resultOK.OK } // NOTE: Not concurrency safe. // Writes results to trs.results without waiting for all tasks to complete. func (trs *TaskResultSet) Reap() *TaskResultSet { for i := 0; i < len(trs.results); i++ { var trch = trs.chz[i] select { case result, ok := <-trch: if ok { // Write result. trs.results[i] = taskResultOK{ TaskResult: result, OK: true, } } // else { // We already wrote it. // } default: // Do nothing. } } return trs } // NOTE: Not concurrency safe. // Like Reap() but waits until all tasks have returned or panic'd. func (trs *TaskResultSet) Wait() *TaskResultSet { for i := 0; i < len(trs.results); i++ { var trch = trs.chz[i] result, ok := <-trch if ok { // Write result. trs.results[i] = taskResultOK{ TaskResult: result, OK: true, } } // else { // We already wrote it. // } } return trs } // Returns the firstmost (by task index) error as // discovered by all previous Reap() calls. func (trs *TaskResultSet) FirstValue() interface{} { for _, result := range trs.results { if result.Value != nil { return result.Value } } return nil } // Returns the firstmost (by task index) error as // discovered by all previous Reap() calls. func (trs *TaskResultSet) FirstError() error { for _, result := range trs.results { if result.Error != nil { return result.Error } } return nil } //---------------------------------------- // Parallel // Run tasks in parallel, with ability to abort early. // Returns ok=false iff any of the tasks returned abort=true. // NOTE: Do not implement quit features here. Instead, provide convenient // concurrent quit-like primitives, passed implicitly via Task closures. (e.g. // it's not Parallel's concern how you quit/abort your tasks). func Parallel(tasks ...Task) (trs *TaskResultSet, ok bool) { var taskResultChz = make([]TaskResultCh, len(tasks)) // To return. var taskDoneCh = make(chan bool, len(tasks)) // A "wait group" channel, early abort if any true received. var numPanics = new(int32) // Keep track of panics to set ok=false later. // We will set it to false iff any tasks panic'd or returned abort. ok = true // Start all tasks in parallel in separate goroutines. // When the task is complete, it will appear in the // respective taskResultCh (associated by task index). for i, task := range tasks { var taskResultCh = make(chan TaskResult, 1) // Capacity for 1 result. taskResultChz[i] = taskResultCh go func(i int, task Task, taskResultCh chan TaskResult) { // Recovery defer func() { if pnk := recover(); pnk != nil { atomic.AddInt32(numPanics, 1) // Send panic to taskResultCh. taskResultCh <- TaskResult{nil, errors.Errorf("panic in task %v", pnk)} // Closing taskResultCh lets trs.Wait() work. close(taskResultCh) // Decrement waitgroup. taskDoneCh <- false } }() // Run the task. var val, abort, err = task(i) // Send val/err to taskResultCh. // NOTE: Below this line, nothing must panic/ taskResultCh <- TaskResult{val, err} // Closing taskResultCh lets trs.Wait() work. close(taskResultCh) // Decrement waitgroup. taskDoneCh <- abort }(i, task, taskResultCh) } // Wait until all tasks are done, or until abort. // DONE_LOOP: for i := 0; i < len(tasks); i++ { abort := <-taskDoneCh if abort { ok = false break } } // Ok is also false if there were any panics. // We must do this check here (after DONE_LOOP). ok = ok && (atomic.LoadInt32(numPanics) == 0) return newTaskResultSet(taskResultChz).Reap(), ok }