You actually only need a cas and a condition variable (technically the condition in Go requires a backing lock, but if you have wait/notify it isn’t needed).
You can read this
https://code.woboq.org/userspace/glibc/nptl/pthread_rwlock_common.c.html and/or
https://6826.csail.mit.edu/2019/papers/HO17.pdf for the general idea.
Essentially, you use some bits of the ‘atomic value’ to encode phases (waiting for write lock, holding write lock, reader bits) - and use the cond variable to block the writers (or readers for that matter) and loop and retry the cas.
The following is simplified since it isn’t “fair”, but you can add another bit for “writer waiting” to accomplish this. Also, the ‘condition’ must be a ‘flag’ so that a signal() with no waiters is satisfied by the next wait()
bits 0 - 30 number of readers
bits 31 writer has lock
WRITER = 1<<31
read() is the atomic read of v
pseudo code
try_lock() {
for {
v = read()
if v has writer {
return WOULD_BLOCK
}
if(cas(v,v + 1 reader)) {
return OK
} else {
// loop and try again
}
}
}
runlock() {
for{
v = read()
if(cas(v,v -1 reader)) {
cond.signal() // can be optimized to not always do this, i.e. only when readers is 0
return
} else {
// loop and try again
}
}
}
wlock() {
for {
v = read()
if v !=0 {
cond.wait()
}
else {
if(cas(v,WRITER)) { // we are the writer
return
} else {
// loop and try again
}
}
}
wunlock() {
set(v,0);
cond.signal() // for other waiting writers
}
Obviously if you need readers to block on writers (i.e. rlock() ) it is slightly more work, but you get the idea.