github.com/dmmcquay/sia@v1.3.1-0.20180712220038-9f8d535311b9/sync/threadgroup.go (about)

     1  package sync
     2  
     3  import (
     4  	"errors"
     5  	"sync"
     6  )
     7  
     8  // ErrStopped is returned by ThreadGroup methods if Stop has already been
     9  // called.
    10  var ErrStopped = errors.New("ThreadGroup already stopped")
    11  
    12  // A ThreadGroup is a one-time-use object to manage the life cycle of a group
    13  // of threads. It is a sync.WaitGroup that provides functions for coordinating
    14  // actions and shutting down threads. After Stop() is called, the thread group
    15  // is no longer useful.
    16  //
    17  // It is safe to call Add(), Done(), and Stop() concurrently, however it is not
    18  // safe to nest calls to Add(). A simple example of a nested call to add would
    19  // be:
    20  //		tg.Add()
    21  //		tg.Add()
    22  //		tg.Done()
    23  //		tg.Done()
    24  type ThreadGroup struct {
    25  	onStopFns    []func()
    26  	afterStopFns []func()
    27  
    28  	once     sync.Once
    29  	stopChan chan struct{}
    30  	bmu      sync.Mutex // Ensures blocking between calls to 'Add', 'Flush', and 'Stop'
    31  	mu       sync.Mutex // Protects the 'onStopFns' and 'afterStopFns' variable
    32  	wg       sync.WaitGroup
    33  }
    34  
    35  // init creates the stop channel for the thread group.
    36  func (tg *ThreadGroup) init() {
    37  	tg.stopChan = make(chan struct{})
    38  }
    39  
    40  // isStopped will return true if Stop() has been called on the thread group.
    41  func (tg *ThreadGroup) isStopped() bool {
    42  	tg.once.Do(tg.init)
    43  	select {
    44  	case <-tg.stopChan:
    45  		return true
    46  	default:
    47  		return false
    48  	}
    49  }
    50  
    51  // Add increments the thread group counter.
    52  func (tg *ThreadGroup) Add() error {
    53  	tg.bmu.Lock()
    54  	defer tg.bmu.Unlock()
    55  
    56  	if tg.isStopped() {
    57  		return ErrStopped
    58  	}
    59  	tg.wg.Add(1)
    60  	return nil
    61  }
    62  
    63  // AfterStop ensures that a function will be called after Stop() has been
    64  // called and after all running routines have called Done(). The functions will
    65  // be called in reverse order to how they were added, similar to defer. If
    66  // Stop() has already been called, the input function will be called
    67  // immediately.
    68  //
    69  // The primary use of AfterStop is to allow code that opens and closes
    70  // resources to be positioned next to each other. The purpose is similar to
    71  // `defer`, except for resources that outlive the function which creates them.
    72  func (tg *ThreadGroup) AfterStop(fn func()) {
    73  	tg.mu.Lock()
    74  	defer tg.mu.Unlock()
    75  
    76  	if tg.isStopped() {
    77  		fn()
    78  		return
    79  	}
    80  	tg.afterStopFns = append(tg.afterStopFns, fn)
    81  }
    82  
    83  // OnStop ensures that a function will be called after Stop() has been called,
    84  // and before blocking until all running routines have called Done(). It is
    85  // safe to use OnStop to coordinate the closing of long-running threads. The
    86  // OnStop functions will be called in the reverse order in which they were
    87  // added, similar to defer. If Stop() has already been called, the input
    88  // function will be called immediately.
    89  func (tg *ThreadGroup) OnStop(fn func()) {
    90  	tg.mu.Lock()
    91  	defer tg.mu.Unlock()
    92  
    93  	if tg.isStopped() {
    94  		fn()
    95  		return
    96  	}
    97  	tg.onStopFns = append(tg.onStopFns, fn)
    98  }
    99  
   100  // Done decrements the thread group counter.
   101  func (tg *ThreadGroup) Done() {
   102  	tg.wg.Done()
   103  }
   104  
   105  // Flush will block all calls to 'tg.Add' until all current routines have
   106  // called 'tg.Done'. This in effect 'flushes' the module, letting it complete
   107  // any tasks that are open before taking on new ones.
   108  func (tg *ThreadGroup) Flush() error {
   109  	tg.bmu.Lock()
   110  	defer tg.bmu.Unlock()
   111  
   112  	if tg.isStopped() {
   113  		return ErrStopped
   114  	}
   115  	tg.wg.Wait()
   116  	return nil
   117  }
   118  
   119  // Stop will close the stop channel of the thread group, then call all 'OnStop'
   120  // functions in reverse order, then will wait until the thread group counter
   121  // reaches zero, then will call all of the 'AfterStop' functions in reverse
   122  // order. After Stop is called, most actions will return ErrStopped.
   123  func (tg *ThreadGroup) Stop() error {
   124  	// Establish that Stop has been called.
   125  	tg.bmu.Lock()
   126  	defer tg.bmu.Unlock()
   127  
   128  	if tg.isStopped() {
   129  		return ErrStopped
   130  	}
   131  	close(tg.stopChan)
   132  
   133  	tg.mu.Lock()
   134  	for i := len(tg.onStopFns) - 1; i >= 0; i-- {
   135  		tg.onStopFns[i]()
   136  	}
   137  	tg.onStopFns = nil
   138  	tg.mu.Unlock()
   139  
   140  	tg.wg.Wait()
   141  
   142  	// After waiting for all resources to release the thread group, iterate
   143  	// through the stop functions and call them in reverse oreder.
   144  	tg.mu.Lock()
   145  	for i := len(tg.afterStopFns) - 1; i >= 0; i-- {
   146  		tg.afterStopFns[i]()
   147  	}
   148  	tg.afterStopFns = nil
   149  	tg.mu.Unlock()
   150  	return nil
   151  }
   152  
   153  // StopChan provides read-only access to the ThreadGroup's stopChan. Callers
   154  // should select on StopChan in order to interrupt long-running reads (such as
   155  // time.After).
   156  func (tg *ThreadGroup) StopChan() <-chan struct{} {
   157  	tg.once.Do(tg.init)
   158  	return tg.stopChan
   159  }