ctxlock.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. package ctxlock
  2. import (
  3. "context"
  4. "sync"
  5. )
  6. // Lock is a channels based sync.Locker, as well as a read-write locker,
  7. // and supporting context-timeouts on locking with LockCtx and RLockCtx.
  8. //
  9. // This lock allows lock-attempts to abort on context cancellation.
  10. // It is used like a standard sync.RWMutex.
  11. //
  12. // The Lock must not be copied after use.
  13. // Run `go vet -copylocks .` to detect copies.
  14. //
  15. // The implementation is heavily inspired by the channel based RWMutex described
  16. // in [Roberto Clapis's series on advanced concurrency patterns].
  17. // This very blog is [referenced in the Go 1.22 standard-library].
  18. // But adapted to support context-cancellation and detect invalid usage.
  19. //
  20. // [referenced in the Go 1.22 standard-library]: https://github.com/golang/go/blob/a10e42f219abb9c5bc4e7d86d9464700a42c7d57/src/sync/cond.go#L34
  21. // [Roberto Clapis's series on advanced concurrency patterns]: https://blogtitle.github.io/go-advanced-concurrency-patterns-part-3-channels/
  22. type Lock struct {
  23. // enables us to just embed Lock anywhere without calling a constructor, alike to sync.Mutex usage.
  24. initOnce sync.Once
  25. // write and global lock. Empty if no lock is held.
  26. // Holds true if a write-purpose lock is held.
  27. // Holds false if a read-purpose lock is held.
  28. write chan bool
  29. // readers lock. Only non-empty if write lock is held.
  30. // Holds a counter of the number of active readers.
  31. // May be temporarily empty while readers enter or leave.
  32. readers chan int
  33. }
  34. func (l *Lock) init() {
  35. l.write = make(chan bool, 1)
  36. l.readers = make(chan int, 1)
  37. }
  38. // Lock acquires the write-lock, blocking until acquired.
  39. func (l *Lock) Lock() {
  40. l.initOnce.Do(l.init)
  41. l.write <- true
  42. }
  43. // LockCtx tries to get the write-lock, but may abort with error if the provided ctx is canceled first.
  44. func (l *Lock) LockCtx(ctx context.Context) error {
  45. if ctx == nil {
  46. panic("nil context argument")
  47. }
  48. l.initOnce.Do(l.init)
  49. select {
  50. case l.write <- true:
  51. return nil
  52. case <-ctx.Done():
  53. return ctx.Err()
  54. }
  55. }
  56. // Unlock releases the write-lock. Unlock panics if the state was not write-locked, or held for reading purposes.
  57. func (l *Lock) Unlock() {
  58. l.initOnce.Do(l.init)
  59. select {
  60. case v := <-l.write:
  61. if !v {
  62. panic("Unlock complete, but lock was held for reading")
  63. }
  64. default:
  65. panic("cannot Unlock: no write lock was held")
  66. }
  67. }
  68. // RLock acquires a read-lock, blocking until acquired.
  69. func (l *Lock) RLock() {
  70. _ = l.rlock(nil)
  71. }
  72. // RLockCtx tries to get a read lock, but may abort with error if the provided ctx is canceled first.
  73. func (l *Lock) RLockCtx(ctx context.Context) error {
  74. if ctx == nil {
  75. panic("nil context argument")
  76. }
  77. if l.rlock(ctx.Done()) {
  78. return ctx.Err()
  79. }
  80. return nil
  81. }
  82. // rlock is an internal helper, implementing read-locking.
  83. // The read-lock may be aborted by signaling through a non-nil abort channel.
  84. // If nil, the read-lock cannot be aborted.
  85. func (l *Lock) rlock(abort <-chan struct{}) (aborted bool) {
  86. l.initOnce.Do(l.init)
  87. // Count current readers. Default to 0.
  88. var rs int
  89. // Select on the channels without default.
  90. // One and only one case will be selected and this
  91. // will block until one case becomes available.
  92. select {
  93. case l.write <- false: // One sending case for write.
  94. // If the write lock is available we have no readers.
  95. // We grab the write lock to prevent concurrent
  96. // read-writes.
  97. case rs = <-l.readers: // One receiving case for read.
  98. // There already are readers, let's grab and update the
  99. // readers count.
  100. case <-abort: // if abort == nil: the abort case is effectively ignored
  101. return true
  102. }
  103. // If we grabbed a write lock this is 0.
  104. rs++
  105. // Updated the readers count. If there are none this
  106. // just adds an item to the empty readers channel.
  107. l.readers <- rs
  108. return false
  109. }
  110. // RUnlock releases a read-lock.
  111. // RUnlock panics if the state was not read-locked.
  112. // RUnlock will block if there is a write-lock, and panic once the write lock
  113. // is released and if a read-lock isn't acquired first.
  114. func (l *Lock) RUnlock() {
  115. l.initOnce.Do(l.init)
  116. var rs int
  117. select {
  118. case l.write <- false:
  119. <-l.write
  120. panic("cannot RUnlock; no readers left, as there was no shared write lock held")
  121. case rs = <-l.readers:
  122. }
  123. // Take the value of readers and decrement it.
  124. rs--
  125. // If zero, make the write lock available again and return.
  126. if rs == 0 {
  127. <-l.write
  128. return
  129. }
  130. // If not zero just update the readers count.
  131. // 0 will never be written to the readers channel,
  132. // at most one of the two channels will have a value
  133. // at any given time.
  134. l.readers <- rs
  135. }