github.com/haraldrudell/parl@v0.4.176/awaitable-slice.go (about) 1 /* 2 © 2024–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package parl 7 8 import ( 9 "sync" 10 "sync/atomic" 11 12 "github.com/haraldrudell/parl/pslices" 13 ) 14 15 const ( 16 // default allocation size for new slices if Size is < 1 17 defaultSize = 10 18 // scavenging: max size for slice preallocation 19 maxForPrealloc = 100 20 ) 21 22 // AwaitableSlice is a queue as thread-safe awaitable unbound slice of element value T or slices of value T 23 // - [AwaitableSlice.Send] [AwaitableSlice.Get] allows efficient 24 // transfer of single values 25 // - [AwaitableSlice.SendSlice] [AwaitableSlice.GetSlice] allows efficient 26 // transfer of slices where 27 // a sender relinquish slice ownership by invoking SendSlice and 28 // a receiving thread gains slice ownership by invoking GetSlice 29 // - [AwaitableSlice.DataWaitCh] returns a channel that closes once data is available 30 // making the queue awaitable 31 // - [AwaitableSlice.EndCh] returns a channel that closes on slice empty, 32 // providing close-like behavior 33 // - [AwaitableSlice.SetSize] allows for setting initial slice capacity 34 // - AwaitableSlice benefits: 35 // - — is trouble-free data-sink: non-blocking-unbound-send non-deadlocking panic-free error-free 36 // - — is initialization-free awaitable thread-less thread-safe 37 // - — features channel-based wait usable with Go select and default 38 // - — is unbound configurable low-allocation 39 // - — features contention-separation between Send SendSlice and Get GetSlice 40 // - — offers high-throughput multiple-value operations SendSlice GetSlice 41 // - — avoids temporary large-slice memory leaks by using size 42 // - — avoids temporary memory leaks by zero-out of unused slice elements 43 // - — although the slice can transfer values almost allocation free or 44 // multiple values at a time, 45 // the wait mechanic requires pointer allocation 10 ns, 46 // channel make 21 ns, channel close 9 ns as well as 47 // CAS operations 8/21 ns 48 // - compared to Go channel: 49 // - — AwaitableSlice is unbound, non-blocking-send error and panic free 50 // - — happens-before with each received value or value and close detection 51 // similar to unbuffered channel guarantees 52 // - — closable by any thread, observable close while also transmitting data 53 // - — AwaitableSlice uses closing channel mechanic for wait and close. 54 // Data synchronization is [sync.Mutex] and the queue is slice. 55 // All is shielded by atomic performance 56 // - — for high parallelism, AwaitableSlice has predominately atomic performance while 57 // channel has 100× deteriorating unshielded lock performance go1.22.3 58 // - AwaitableSlice deprecates: 59 // - — [NBChan] fully-featured unbound channel 60 // - — [NBRareChan] low-usage unbound channel 61 // 62 // Usage: 63 // 64 // var valueQueue parl.AwaitableSlice[*Value] 65 // go func(valueSink parl.ValueSink) { 66 // defer valueSink.EmptyCh() 67 // … 68 // valueSink.Send(value) 69 // … 70 // }(&valueQueue) 71 // endCh := valueQueue.EmptyCh(parl.CloseAwait) 72 // for { 73 // select { 74 // case <-valueQueue.DataWaitCh(): 75 // for value := valueQueue.Init(); valueQueue.Condition(&value); { 76 // doSomething(value) 77 // } 78 // case <-endCh: 79 // break 80 // } 81 // // the slice closed 82 // … 83 // // to reduce blocking, at most 100 at a time 84 // for i, hasValue := 0, true; i < 100 && hasValue; i++ { 85 // var value *Value 86 // if value, hasValue = valueQueue.Get(); hasValue { 87 // doSomething(value) 88 type AwaitableSlice[T any] struct { 89 // allocation size for new slices, effective if > 0 90 // - 10 or larger value from SetSize 91 size Atomic64[int] 92 // maxRetainSize is the longest slice that will be reused 93 // - avoids temporary memory leaks 94 maxRetainSize Atomic64[int] 95 // queueLock makes queue and slices thread-safe 96 // - queueLock also makes Send SendSlice critical sections 97 queueLock sync.Mutex 98 // queue is a locally made slice for individual values 99 // - behind queueLock 100 // - not a slice-away slie 101 queue []T 102 // slices contains slices of values transferred by SendSlice and 103 // possible subsequent locally made slices of values 104 // - slice-away slice, behind queueLock 105 slices, slices0 [][]T 106 // isLocalSlice is true if the last slice of slices is locally made 107 // - only valid when slices non-empty 108 // - behind queueLock 109 isLocalSlice bool 110 // indicates at all times whether the queue is empty 111 // - allows for updateDataWait to be invoked without any locks held 112 // - written behind queueLock 113 hasData atomic.Bool 114 // a pre-allocated slice for queue 115 // - behind queueLock 116 // - allocated by Get Get1 GetAll prior to acquiring queueLock 117 cachedInput []T 118 // outputLock makes output thread-safe 119 // - outputLock also makes Get1 Get critical sections 120 outputLock sync.Mutex 121 // output is a slice being sliced away from 122 // - behind outputLock, slice-away slice 123 output, output0 []T 124 // outputs contains entire-slice values 125 // - behind outputLock, slice-away slice 126 outputs, outputs0 [][]T 127 // a pre-allocated slice for queue 128 // - behind outputLock 129 // - allocated by Get Get1 GetAll prior to acquiring queueLock 130 cachedOutput []T 131 // lazy DataWaitCh 132 dataWait LazyCyclic 133 // lazy emptyWait 134 emptyWait LazyCyclic 135 } 136 137 // Send enqueues a single value. Thread-safe 138 func (s *AwaitableSlice[T]) Send(value T) { 139 defer s.postSend() 140 s.queueLock.Lock() 141 142 // add to queue if no slices 143 if len(s.slices) == 0 { 144 if s.queue != nil { 145 s.queue = append(s.queue, value) 146 // create s.queue 147 } else if s.cachedInput != nil { 148 // use cachedInput allocated under outputLock 149 s.queue = append(s.cachedInput, value) 150 s.cachedInput = nil 151 } else { 152 s.queue = s.make(value) 153 } 154 return // value in queue return 155 } 156 157 // add to slices 158 // - if last slice not locally created, append to new slice 159 if s.isLocalSlice { 160 // append to ending local slice 161 var index = len(s.slices) - 1 162 s.slices[index] = append(s.slices[index], value) 163 return 164 } 165 166 // append local slice 167 var q []T 168 if s.cachedInput != nil { 169 q = append(s.cachedInput, value) 170 s.cachedInput = nil 171 } else { 172 q = s.make(value) 173 } 174 pslices.SliceAwayAppend1(&s.slices, &s.slices0, q) 175 s.isLocalSlice = true 176 } 177 178 // SendSlice provides values by transferring ownership of a slice to the queue 179 // - SendSlice may reduce allocations and increase performance by handling multiple values 180 // - Thread-safe 181 func (s *AwaitableSlice[T]) SendSlice(values []T) { 182 // ignore empty slice 183 if len(values) == 0 { 184 return 185 } 186 defer s.postSend() 187 s.queueLock.Lock() 188 189 // append to slices 190 s.slices = append(s.slices, values) 191 s.isLocalSlice = false 192 } 193 194 // DataWaitCh returns a channel that closes once values becomes available 195 // - Thread-safe 196 func (s *AwaitableSlice[T]) DataWaitCh() (ch AwaitableCh) { 197 // this may initialize the cyclic awaitable 198 ch = s.dataWait.Cyclic.Ch() 199 200 // if previously invoked, no need for initialization 201 if s.dataWait.IsActive.Load() { 202 return // not first invocation 203 } 204 205 // establish proper state 206 // - data wait ch now in use 207 if !s.dataWait.IsActive.CompareAndSwap(false, true) { 208 return 209 } 210 211 // set initial state 212 s.updateWait() 213 214 return 215 } 216 217 // [AwaitableSlice.EmptyCh] initialize: this invocation 218 // will wait for close-like state, do not activate EmptyCh awaitable 219 const CloseAwaiter = false 220 221 // EmptyCh returns an awaitable channel that closes on queue being or 222 // becoming empty 223 // - doNotInitialize missing: enable closing of ch which will happen as soon 224 // as the slice is empty, possibly prior to return 225 // - doNotInitialize CloseAwaiter: obtain the channel but do not enable it closing. 226 // A subsequent invocation with doNotInitialize missing will enable its closing thus 227 // act as a deferred Close function 228 // - thread-safe 229 func (s *AwaitableSlice[T]) EmptyCh(doNotInitialize ...bool) (ch AwaitableCh) { 230 // this may initialize the cyclic awaitable 231 ch = s.emptyWait.Cyclic.Ch() 232 233 // if previously invoked, no need for initialization 234 if len(doNotInitialize) > 0 || s.emptyWait.IsActive.Load() { 235 return // not first invocation 236 } 237 238 // establish proper state 239 // - data wait ch now in use 240 if !s.emptyWait.IsActive.CompareAndSwap(false, true) { 241 return 242 } 243 244 // set initial state 245 if !s.hasData.Load() { 246 s.emptyWait.Cyclic.Close() 247 } 248 249 return 250 } 251 252 // Get returns one value if the queue is not empty 253 // - hasValue true: value is valid 254 // - hasValue false: the queue is empty 255 // - Get may attain allocation-free receive or allocation-free operation 256 // - — a slice is not returned 257 // - — an internal slice may be reused reducing allocations 258 // - thread-safe 259 func (s *AwaitableSlice[T]) Get() (value T, hasValue bool) { 260 if !s.hasData.Load() { 261 return 262 } 263 defer s.postGet() 264 s.outputLock.Lock() 265 266 // if output empty, transfer outputs[0] to output 267 if len(s.output) == 0 && len(s.outputs) > 0 { 268 // possibly save output to cachedOutput 269 if c := cap(s.output); c == defaultSize && s.cachedOutput == nil { 270 s.cachedOutput = s.output0 271 } 272 // write new s.output 273 var so = s.outputs[0] 274 s.output = so 275 s.output0 = so 276 s.outputs[0] = nil 277 s.outputs = s.outputs[1:] 278 } 279 280 // try output 281 if hasValue = len(s.output) > 0; hasValue { 282 value = s.output[0] 283 var zeroValue T 284 s.output[0] = zeroValue 285 s.output = s.output[1:] 286 return // got value from s.output 287 } 288 289 // transfer from queueLock 290 var slice = s.sliceFromQueue(isOne) 291 if hasValue = len(slice) > 0; !hasValue { 292 return // no value available 293 } 294 295 // store slice as output and fetch value 296 s.output0 = slice 297 value = slice[0] 298 var zeroValue T 299 slice[0] = zeroValue 300 s.output = slice[1:] 301 302 return 303 } 304 305 // GetSlice returns a slice of values from the queue 306 // - values non-nil: a non-empty slice at a time, not necessarily all data. 307 // values is never non-nil and empty 308 // - — Send-GetSlice: each GetSlice empties the queue 309 // - — SendMany-GetSlice: each GetSlice receives one SendMany slice 310 // - values nil: the queue is empty 311 // - GetSlice may increase performance by slice-at-a-time operation, however, 312 // slices need to be allocated: 313 // - — Send-GetSlice requires internal slice allocation 314 // - — SendMany-GetSlice requires sender to allocate slices 315 // - — Send-Get1 may reduce allocations 316 // - thread-safe 317 func (s *AwaitableSlice[T]) GetSlice() (values []T) { 318 if !s.hasData.Load() { 319 return 320 } 321 defer s.postGet() 322 s.outputLock.Lock() 323 324 // try output 325 if len(s.output) > 0 { 326 values = s.output 327 s.output0 = nil 328 s.output = nil 329 return 330 } 331 332 // try s.outputs 333 var so = s.outputs 334 if len(so) > 0 { 335 values = so[0] 336 so[0] = nil 337 s.outputs = so[1:] 338 return 339 } 340 341 // transfer from queueLock 342 // - values may be nil 343 values = s.sliceFromQueue(isSlice) 344 345 return 346 } 347 348 // GetAll returns a single slice of all unread values in the queue 349 // - values nil: the queue is empty 350 // - thread-safe 351 func (s *AwaitableSlice[T]) GetAll() (values []T) { 352 if !s.hasData.Load() { 353 return 354 } 355 defer s.postGet() 356 s.outputLock.Lock() 357 358 // aggregate outputLock data 359 // output is a copy of s.output since preAlloc may destroy it 360 var size = len(s.output) 361 for _, o := range s.outputs { 362 size += len(o) 363 } 364 365 // aggregate queueLock data 366 s.preAlloc(onlyCachedTrue) 367 defer s.queueLock.Unlock() 368 s.queueLock.Lock() 369 s.transferCached() 370 371 // aggregate queueLock data 372 size += len(s.queue) 373 for _, s := range s.slices { 374 size += len(s) 375 } 376 // size is now length of the returned slice 377 378 // no data 379 if size == 0 { 380 return // no data return 381 } 382 383 // will return all data so queue will be empty 384 // - because size is not zero, hasData is changing 385 // - written while holding queueLock 386 s.hasData.Store(false) 387 388 // attempt allocation-free single slice return 389 if values = s.singleSlice(size); len(values) > 0 { 390 return // single slice 391 } 392 393 // create aggregate slice 394 values = make([]T, 0, size) 395 if len(s.output) > 0 { 396 values = append(values, s.output...) 397 pslices.SetLength(&s.output, 0) 398 } 399 for _, s := range s.outputs { 400 values = append(values, s...) 401 } 402 pslices.SetLength(&s.outputs, 0) 403 if len(s.queue) > 0 { 404 values = append(values, s.queue...) 405 pslices.SetLength(&s.queue, 0) 406 } 407 for _, s := range s.slices { 408 values = append(values, s...) 409 } 410 pslices.SetLength(&s.slices, 0) 411 412 return 413 } 414 415 // Init allows for AwaitableSlice to be used in a for clause 416 // - returns zero-value for a short variable declaration in 417 // a for init statement 418 // - thread-safe 419 // 420 // Usage: 421 // 422 // var a AwaitableSlice[…] = … 423 // for value := a.Init(); a.Condition(&value); { 424 // // process received value 425 // } 426 // // the AwaitableSlice closed 427 func (s *AwaitableSlice[T]) Init() (value T) { return } 428 429 // Condition allows for AwaitableSlice to be used in a for clause 430 // - updates a value variable and returns whether values are present 431 // - thread-safe 432 // 433 // Usage: 434 // 435 // var a AwaitableSlice[…] = … 436 // for value := a.Init(); a.Condition(&value); { 437 // // process received value 438 // } 439 // // the AwaitableSlice closed 440 func (s *AwaitableSlice[T]) Condition(valuep *T) (hasValue bool) { 441 var endCh AwaitableCh 442 for { 443 444 // try obtaining value 445 if s.hasData.Load() { 446 var v T 447 if v, hasValue = s.Get(); hasValue { 448 *valuep = v 449 return // value obtained: *valuep valid, hasValue true 450 } 451 continue 452 } 453 // hasData is false 454 // - wait until the slice has data or 455 // - the slice closes 456 457 // atomic-performance check for channel end 458 if s.emptyWait.IsActive.Load() { 459 // channel is out of items and closed 460 return // closed: hasValue false, *valuep unchanged 461 } 462 463 // await data or close 464 if endCh == nil { 465 // get endCh without initializing close mechanic 466 endCh = s.EmptyCh(CloseAwaiter) 467 } 468 select { 469 470 // await data, possibly initializing dataWait 471 case <-s.DataWaitCh(): 472 473 // await close and end of data 474 case <-endCh: 475 return // closed: hasValue false, *valuep unchanged 476 } 477 } 478 } 479 480 // SetSize set initial allocation size of slices. Thread-safe 481 func (s *AwaitableSlice[T]) SetSize(size int) { 482 var maxSize int 483 if size < 1 { 484 size = defaultSize 485 } else if size > maxForPrealloc { 486 maxSize = size 487 } else { 488 maxSize = maxForPrealloc 489 } 490 s.size.Store(size) 491 s.maxRetainSize.Store(maxSize) 492 } 493 494 // make returns a new slice of length 0 and configured capacity 495 // - value, if present, is added to the new slice 496 func (s *AwaitableSlice[T]) make(value ...T) (newSlice []T) { 497 498 // ensure size sizeMax are initialized 499 var size = s.size.Load() 500 if size == 0 { 501 s.SetSize(0) 502 size = s.size.Load() 503 } 504 505 //create slice optionally with value 506 newSlice = make([]T, len(value), size) 507 if len(value) > 0 { 508 newSlice[0] = value[0] 509 } 510 511 return 512 } 513 514 const ( 515 isOne = false 516 isSlice = true 517 ) 518 519 // sliceFromQueue fetches slices from queue to output 520 // - getSlice true: seeking entire slice 521 // - getSlice false: seeking single value 522 // - invoked when output empty 523 func (s *AwaitableSlice[T]) sliceFromQueue(getSlice bool) (slice []T) { 524 //prealloc outside queueLock 525 s.preAlloc() 526 s.queueLock.Lock() 527 defer s.queueLock.Unlock() 528 s.transferCached() 529 530 // three tasks while holding queueLock: 531 // - find what slice to return 532 // - transfer all other slices to outputLock 533 // - update hasData 534 535 // retrieve queue if non-empty 536 if len(s.queue) > 0 { 537 slice = s.queue 538 s.queue = nil 539 } 540 // possibly transfer pre-made output0 to queueLock 541 if s.queue == nil { 542 // transfer output0 to queueLock 543 s.queue = s.output0 544 s.output0 = nil 545 } 546 547 // if slice empty, try first of slices 548 if len(slice) == 0 && len(s.slices) > 0 { 549 slice = s.slices[0] 550 s.slices[0] = nil 551 s.slices = s.slices[1:] 552 } 553 554 // transfer any remaining slices 555 if len(s.slices) > 0 { 556 pslices.SliceAwayAppend(&s.outputs, &s.outputs0, s.slices) 557 // empty and zero-out s.slices 558 pslices.SetLength(&s.slices, 0) 559 s.slices = s.slices0[:0] 560 } 561 562 // hasData must be updated while holding queueLock 563 // - it is currently true 564 if len(s.outputs) > 0 { 565 return // slices in outputs mean data still available: no change 566 567 // if fetching single value and more than one value in that slice, 568 // not end of data 569 } else if !getSlice && len(slice) > 1 { 570 return // no, single-value fetch and more than one value 571 } 572 573 // the queue is empty: update hasData while holding queueLock 574 s.hasData.Store(false) 575 576 return 577 } 578 579 // setData updates dataWaitCh if DataWaitCh was invoked 580 // - eventually consistent 581 // - atomized hasData observation with dataWait emptyWait update 582 // - shielded atomic performance 583 func (s *AwaitableSlice[T]) updateWait() { 584 585 // dataWait closes on data available, then re-opens 586 // - hasData true: dataWait should be closed 587 // - emptyWait closes on empty and remains closed 588 // - hasData observed false, emptyWait should be closed 589 590 // is dataWait or emptyWait in use? 591 // - both only go once from false to true 592 var dataWait = s.dataWait.IsActive.Load() 593 594 if dataWait { 595 // atomic check based on dataWait 596 if s.hasData.Load() == s.dataWait.Cyclic.IsClosed() { 597 return // atomically state was ok 598 } 599 // atomic check based on emptyWait 600 } else if !s.emptyWait.IsActive.Load() { 601 return // neither is active 602 // close emptyCh if empty 603 } else if s.hasData.Load() || s.emptyWait.Cyclic.IsClosed() { 604 return // atomically state was ok 605 } 606 607 // alter state using the lock of dataWait 608 // - even if dataWait is not active 609 // - atomizes hasData observation with Open/Close operation 610 s.dataWait.Lock.Lock() 611 defer s.dataWait.Lock.Unlock() 612 613 // hasData inside lock 614 var hasData = s.hasData.Load() 615 616 // if dataWait active: 617 if dataWait { 618 // check against dataWait state 619 if hasData == s.dataWait.Cyclic.IsClosed() { 620 return // no change 621 } else if hasData { 622 // hasData true: close dataWait 623 s.dataWait.Cyclic.Close() 624 // emptyWait does not re-open 625 } else { 626 // hasData false: open dataWait 627 s.dataWait.Cyclic.Open() 628 // hasData false: trigger emptyWait 629 if s.emptyWait.IsActive.Load() { 630 s.emptyWait.Cyclic.Close() 631 } 632 return 633 } 634 } 635 // emptyWait is active, dataWait inactive 636 637 // if not empty, no action 638 if hasData { 639 return 640 } 641 // no data: trigger emptyWait 642 s.emptyWait.Cyclic.Close() 643 } 644 645 // ensureSize ensures that size and maxRetainSize are initialized 646 // - size: the configured allocation-size of a new queue slice 647 func (s *AwaitableSlice[T]) ensureSize() (size int) { 648 // ensure size sizeMax are initialized 649 if size = s.size.Load(); size == 0 { 650 s.SetSize(0) 651 size = s.size.Load() 652 } 653 return 654 } 655 656 // preAlloc onlyCached 657 const onlyCachedTrue = true 658 659 // preAlloc ensures that output0 and cachedOutput are allocated 660 // to configured size 661 // - must hold outputLock 662 func (s *AwaitableSlice[T]) preAlloc(onlyCached ...bool) { 663 664 var size = s.ensureSize() 665 if len(onlyCached) == 0 || !onlyCached[0] { 666 667 // output0 first pre-allocation 668 // - ensure output0 is a slice of good capacity 669 // - may be transferred to queueLock 670 // - avoids allocation while holding queueLock 671 // should output0 be allocated? 672 var makeOutput = s.output0 == nil 673 if !makeOutput { 674 // check capacity of existing output 675 var c = cap(s.output0) 676 // reuse for capcities defaultSize–maxRetainSize 677 makeOutput = c < defaultSize || c > s.maxRetainSize.Load() 678 } 679 if makeOutput { 680 var so = s.make() 681 s.output0 = so 682 s.output = so 683 } 684 } 685 686 // cachedOutput second pre-allocation 687 // - possibly have ready for transfer 688 // - configured size may be large so only for defaultSize 689 if s.cachedOutput == nil && size == defaultSize { 690 s.cachedOutput = s.make() 691 } 692 } 693 694 // transferCached transfers cachedOutput from 695 // outputLock to queueLock if possible 696 // - invoked while holding outputLock queueLock 697 func (s *AwaitableSlice[T]) transferCached() { 698 699 // transfer cachedOutput to queueLock 700 if s.cachedInput == nil && s.cachedOutput != nil { 701 s.cachedInput = s.cachedOutput 702 s.cachedOutput = nil 703 } 704 } 705 706 // postGet relinquishes outputLock and 707 // initializes eventual update of DataWaitCh and EmptyCh 708 // - aggregates deferred actions to reduce latency 709 // - invoked while holding outputLock 710 func (s *AwaitableSlice[T]) postGet() { 711 s.outputLock.Unlock() 712 s.updateWait() 713 } 714 715 // singleSlice fetches values if contained in a single slice 716 // - reduces slice allocations by using an existing slice 717 // - invoked while holding outputLock queueLock 718 func (s *AwaitableSlice[T]) singleSlice(size int) (values []T) { 719 720 // only output 721 if size == len(s.output) { 722 values = s.output 723 s.output = nil 724 s.output0 = nil 725 return // got values 726 } else if len(s.output) > 0 { 727 return // is aggregate 728 } 729 730 // only outputs[0] 731 if len(s.outputs) == 1 && size == len(s.outputs[0]) { 732 values = s.outputs[0] 733 s.outputs[0] = nil 734 s.outputs = s.outputs[1:] 735 return // got values 736 } else if len(s.outputs) > 0 { 737 return // is aggregate 738 } 739 740 // only queue 741 if len(s.queue) == size { 742 values = s.queue 743 s.queue = nil 744 } 745 // possibly transfer pre-made output0 to queueLock 746 if s.queue == nil { 747 // transfer output0 to queueLock 748 s.queue = s.output0 749 s.output0 = nil 750 } 751 if len(s.queue) > 0 || len(s.queue) == size { 752 return // got values or is aggregate 753 } 754 755 // only s.slices[0] 756 if len(s.slices) == 1 { 757 values = s.slices[0] 758 s.slices = s.slices[1:] 759 } 760 761 return // got values or is aggregate 762 } 763 764 // postSend set hasData true, relinquishes queueuLock and 765 // initializes eventual update of DataWaitCh and EmptyCh 766 // - aggregates deferred actions to reduce latency 767 // - invoked while holding queueLock 768 func (s *AwaitableSlice[T]) postSend() { 769 s.hasData.Store(true) 770 s.queueLock.Unlock() 771 s.updateWait() 772 }