github.com/haraldrudell/parl@v0.4.176/moderator-core.go (about) 1 /* 2 © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package parl 7 8 import ( 9 "fmt" 10 "math" 11 "sync" 12 "sync/atomic" 13 ) 14 15 const ( 16 // default is to allow 20 threads at a time 17 defaultParallelism = 20 18 ) 19 20 // ModeratorCore invokes functions at a limited level of parallelism 21 // - ModeratorCore is a ticketing system 22 // - ModeratorCore does not have a cancel feature 23 // - during low contention atomic performance 24 // - during high-contention lock performance 25 // 26 // Usage: 27 // 28 // m := NewModeratorCore(20, ctx) 29 // defer m.Ticket()() // waiting here for a ticket 30 // // got a ticket! 31 // … 32 // return or panic // ticket automatically returned 33 // m.String() → waiting: 2(20) 34 type ModeratorCore struct { 35 // parallelism is the maximum number of outstanding tickets 36 parallelism uint64 37 // number of issued tickets 38 // - if less than parallelism: 39 // - — moderator is in atomic mode, ie. 40 // - — tickets obtained by atomic access only 41 // - if equal to parallelism: 42 // - — moderator is in lock mode. ie. 43 // - — tickets are transfered orderly using queue, 44 // waiting and transferBehindLock 45 active atomic.Uint64 46 // lock used when moderator in lock mode 47 // - treads use the cond with waiting and transferBehindLock 48 // - orderly first-come-first-served 49 queue sync.Cond 50 // number of threads waiting for a ticket 51 // - behind lock 52 // - atomic so Status can read 53 waiting atomic.Uint64 54 // transferBehindLock facilitates locked ticket transfer 55 // - behind lock 56 // - atomic so it can be inspected 57 transferBehindLock atomic.Uint64 58 } 59 60 // moderatorCore is a parl-private version of ModeratorCore 61 type moderatorCore struct { 62 *ModeratorCore 63 } 64 65 // NewModerator creates a new Moderator used to limit parallelism 66 func NewModeratorCore(parallelism uint64) (m *ModeratorCore) { 67 if parallelism < 1 { 68 parallelism = defaultParallelism 69 } 70 return &ModeratorCore{ 71 parallelism: parallelism, 72 queue: *sync.NewCond(&sync.Mutex{}), 73 } 74 } 75 76 // Ticket returns a ticket possibly blocking until one is available 77 // - Ticket returns the function for returning the ticket 78 // 79 // Usage: 80 // 81 // defer moderator.Ticket()() 82 func (m *ModeratorCore) Ticket() (returnTicket func()) { 83 returnTicket = m.returnTicket 84 85 // try available ticket at atomic performance 86 for { 87 if tickets := m.active.Load(); tickets == m.parallelism { 88 break // it’s lock mode 89 } else if m.active.CompareAndSwap(tickets, tickets+1) { 90 return // got atomic ticket return 91 } 92 } 93 94 // enter lock mode 95 m.queue.L.Lock() 96 defer m.queue.L.Unlock() 97 defer m.lastWaitCheck() 98 99 // critial section: ticket loop 100 var isWaiting bool 101 for { 102 103 // attempt atomic ticket 104 for { 105 if tickets := m.active.Load(); tickets == m.parallelism { 106 break // still lock mode 107 } else if m.active.CompareAndSwap(tickets, tickets+1) { 108 return // got atomic ticket return 109 } 110 } 111 112 // attempt transfer-behind-lock ticket 113 if m.transferBehindLock.Load() > 0 { 114 m.transferBehindLock.Add(math.MaxUint64) 115 return // ticket transfer successful return 116 } 117 118 // wait for ticket to become available 119 if !isWaiting { 120 isWaiting = true 121 m.waiting.Add(1) 122 defer m.waiting.Add(math.MaxUint64) 123 } 124 // blocks here 125 m.queue.Wait() 126 } 127 } 128 129 // lastWaitCheck prevents tickets from getting stuck as transfers 130 // - invoked while holding lock 131 // - this can happen if 1 thread is waiting and multiple threads transfer tickets 132 func (m *ModeratorCore) lastWaitCheck() { 133 if m.waiting.Load() > 0 { 134 return // more threads are waiting 135 } 136 var transfers = m.transferBehindLock.Load() 137 if transfers == 0 { 138 return // no extra transfers available return 139 } 140 141 // put extra transfers in atomic tickets 142 m.transferBehindLock.Store(0) 143 m.active.Add(math.MaxUint64 - transfers + 1) 144 } 145 146 // returnTicket returns a ticket obtained by Ticket 147 func (m *ModeratorCore) returnTicket() { 148 149 // attempt ticket-return atomically 150 for { 151 if tickets := m.active.Load(); tickets == m.parallelism { 152 break // lock mode: use transfer-ticket 153 } else if m.active.CompareAndSwap(tickets, tickets-1) { 154 return // ticket returned atomically return 155 } 156 } 157 158 // return ticket using transfer behind lock 159 m.queue.L.Lock() 160 defer m.queue.L.Unlock() 161 162 // if no thread waiting, return atomically 163 if m.waiting.Load() == 0 { 164 m.active.Add(math.MaxUint64) 165 return // atomic transfer complete return 166 } 167 168 // if thread waiting, do ticket transfer 169 m.transferBehindLock.Add(1) 170 m.queue.Signal() // signal while holding lock 171 } 172 173 // Status: values may lack integrity 174 func (m *ModeratorCore) Status() (parallelism, active, waiting uint64) { 175 parallelism = m.parallelism 176 active = m.active.Load() 177 waiting = m.waiting.Load() 178 return 179 } 180 181 // when tickets available: “available: 2(10)” 182 // - 10 - 2 = 8 threads operating 183 // - when threads waiting “waiting 1(10)” 184 // - 10 threads operating, 1 thread waiting 185 func (m *ModeratorCore) String() (s string) { 186 var parallelism, active, waiting = m.Status() 187 if active < parallelism { 188 s = fmt.Sprintf("available: %d(%d)", parallelism-active, parallelism) 189 } else { 190 s = fmt.Sprintf("waiting: %d(%d)", waiting, parallelism) 191 } 192 return 193 }