preface

Because Go already has Mutex, RWMutex, etc., we don’t usually implement a new lock ourselves, but we can use this implementation to deepen our understanding of locks and channels.

Realize the principle of

A channel with a buffer length of 1 is similar to a semaphore with a value of 1 as a lock.

Lock

We only send a single piece of data to the channel when we Lock it, and since the channel’s buffer length is 1, subsequent Lock operations are blocked until the data is fetched.

Unlock

To unlock the lock, we simply need to retrieve the data in the channel so that other Goroutines waiting for the lock can send data into the channel to obtain the lock.

TryLock

The default of a select supports non-blocking attempts to send data to the channel, so we can use the default non-blocking attempt of a select to send data to the channel.

fairness

This lock is a fair lock because a channel guarantees that the goroutine that sends data to the channel first gets the right to send data first. This is done by adding the blocked goroutine to a send wait queue, putting the send wait queue header into the buffer when the data in the channel buffer is fetched, and adding the sender Goroutine to the current processor runNext. The scheduler will wake up the goroutine the next time it is blocked because it can’t get the lock.

implementation

The data structure

We just encapsulate a channel, but since we need to specify a channel length of 1, we add a NewChannelLock() constructor.

type ChannelLock struct {
   c chan struct{}}func NewChannelLock(a) *ChannelLock {
   return &ChannelLock{
      c: make(chan struct{}, 1),}}Copy the code

TryLock(), Lock(), Unlock()

Note that Unlock() must be invoked with a lock in place, otherwise it exits the process (in keeping with the semantics of Mutex).

func (l *ChannelLock) TryLock(a) bool {
   select {
   case l.c <- struct{} {} :return true
   default:
      return false}}func (l *ChannelLock) Lock(a) {
   l.c <- struct{}{}
}

func (l *ChannelLock) Unlock(a) {
   select {
   case <-l.c:
   default:
      log.Fatal("sync: unlock of unlocked mutex")}}Copy the code

conclusion

  • It is easy to implement a fair lock using channel, but since the Go language does not come with fair locks, this implementation can be used if fair locks are needed. Of course, in most cases it’s more elegant to use channels directly.
  • Mutex and RWMutex are still recommended in most cases, because these locks are already fair enough to prevent starvation.

Full code and test code: github.com/jiaxwu/lock…