Notes: Solving readers-writers problem in Go

Notes: Solving readers-writers problem in Go

This blog shows how we can use locks to avoid readers-writers problem in concurrent programming

ยท

3 min read

Table of contents

No heading

No headings in the article.

The readers-writers problem is a hot topic in concurrent programming. It may happen that many goroutines, both readers and writers might be handling the same object, reading it, or modifying it. In that case, we will face an inconsistent state of that object. It will create a race condition for that object.

So, it's important to manage synchronization. One approach is to allow multiple readers to read data at the same time while blocking write, but blocking all reads and writes if one writer writes on that data. This way we'll not have a race condition. For this, readers take shared lock while writers take exclusive lock.

A read or share lock prevents a resource from being written while allowing other concurrent reads.

A write or exclusive lock disallows both read and write operations on a given resource.

Let's look at an example:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

var mutex = new(sync.RWMutex)

var lockedVal int

var (
    colorReset  = "\033[0m"
    colorRed    = "\033[31m"
    colorGreen  = "\033[32m"
    colorYellow = "\033[33m"
    colorCyan   = "\033[36m"
)

const (
    layoutTime = "15:04:05"
)

func writer(wg *sync.WaitGroup) {
    defer wg.Done()

    var wg2 sync.WaitGroup
    numWriters := 1000

    for i := 0; i < numWriters; i++ {
        // fmt.Printf("%vspawned writer num: %v\n", string(colorYellow), i)
        wg2.Add(1)

        // to slow down spawing of writers
        // time.Sleep(500 * time.Millisecond)

        go func(i int) {
            defer wg2.Done()

            mutex.Lock()
            defer mutex.Unlock()
            lockedVal = rand.Intn(100)

            fmt.Printf("%vlocked at writer num: %v, val: %v at %v\n", string(colorRed), i, lockedVal, time.Now().Format(layoutTime))

            // nothing will happen for 5 seconds as this is exclusive lock
            // so whenever writer locks, it blocks execution for 5 seconds
            time.Sleep(5 * time.Second)

            // after 5 seconds, lock will again be available for both reader and writer
            // whoever gets it first will again decide the next step
        }(i)
    }

    wg2.Wait()
}

func reader(wg *sync.WaitGroup) {
    defer wg.Done()

    var wg2 sync.WaitGroup
    numReaders := 1000

    for i := 0; i < numReaders; i++ {
        // fmt.Printf("%vspawned reader num: %v\n", string(colorCyan), i)
        wg2.Add(1)

        // to slow down spawing of readers
        time.Sleep(time.Second)

        go func(i int) {
            defer wg2.Done()

            mutex.RLock()
            defer mutex.RUnlock()

            fmt.Printf("%vlocked at reader num: %v, val: %v at %v\n", string(colorGreen), i, lockedVal, time.Now().Format(layoutTime))

            // this will not allow writer to get lock
            // while we can have multiple readers sharing this lock
            // so writer will not be able to have lock for at least
            // 5 seconds, while we can see multiple readers accessing
            // lockedVal without any issues
            time.Sleep(5 * time.Second)

            // after 5 seconds, lock will again be available for both reader and writer
            // whoever gets it first will again decide the next step
        }(i)
    }

    wg2.Wait()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go writer(&wg)
    go reader(&wg)
    wg.Wait()

    fmt.Println(string(colorReset))
}

In the above code, we create readers and writers to access one common variable lockedVal. To access this variable, both readers and writers try to get lock on it. For lock, we're using sync.RWMutex object from sync package.

In writer we use mutex.Lock() to get exclusive lock while in reader, we use mutex.RLock() to get shared lock. Now, run the program and you'll see something like this:

Screenshot 2022-05-06 at 5.15.09 PM.png

This output shows that whenever a writer has the lock, it doesn't allow any other operation until it releases it after 5 seconds, while readers are working in a group together and then releasing the lock after 5 seconds. And readers and writers never work together.

One thing to note here is that I've put sleep timer before spawning readers to slow down the generation of readers as in a single reader lock, all the readers can be consumed leaving only writers behind. Not so good for the demo!

Try running the code and modify it to test a few more cases for better understanding.

ย