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 }