Readers and writers – GoLang

by sagacity

In the 1st blog which was titled as Golang Synchronization in Primitives, we saw what exactly are synchronization primitives. We also found the need for the same when multiple go-routines are contesting for a common resource, critical code section and even data. We went through a couple of very simple examples. I hope, you must have gotten some sense out of them. Of course, the very rudimentary nature of the code snippets were deliberate so as not to complicate the matters in the beginning itself.

sync.Mutext are of very nature that owner of the lock disallows the case of co-owner. This means, it has very specific area utility, for instance, incrementing a global counter that keeps count of number of visits to a website. This’s typically and hard lock kind of a scenario wherein the lock is mutually exclusive when multiple go-routine are contesting for the critical section of some kind of a mutable global data.

However, consider a scenario wherein multiple go-routines are expected to be allowed to read the data at any given instance in time. This means they can co-own the data as long as reading data is concerned. So, this’s a typical case wherein multiple go-routines can allow to fetch the current value of the global counter. One of the practical examples is select query by multiple connections.

As multiple go-routines can read the data at the same time, should the multiple go-routines be allowed to mutate the global data? Consider a practical case. A blogging site is receiving multiple registrations. The endpoint that’s invoked as a service is maintaining an in-memory cache of the data of registered users. The key is user’s mobile number. So, the immediate query that crops up on our mind is should multiple endpoints be allowed to make an entry of the registering user in the in-memory cache. Let’s consider each of the possible possible answers, those are, yes and no.

Each time a new record is added in the in-memory cache, the indexing of the cache is redone. If the cache is
maintained as a hash-table and any whichever methods of hashing is implemented – open hashing or closed hashing –
it’s needed to find the most appropriate address for the next record. This clearly means the effect is observed across the entire data store. This’s a perfect example where such operations are to be executed exclusively. This means during one such operation is in execution, on other operations, neither read nor write is allowed at the same time.

As the subject of this blog suggests, we’re talking about reader and writer paradigms when multiple execution units are in the play. A simple truth table below will help you understand more crisply.

Store operation in play     Other store operations allowed
       Read                              Read
       Write                              X

It tells us while the store is under read lock by one go-routine, the other readers are possible. But no writer is allowed at the same instance of time. Which in turn means, read store operation is mutually inclusive for reads but exclusive for writes. And while the store is under write lock by one go-routine, everything else is blocked. Be a reader or a writer, no one else is allowed to take a peek at the data store for the same instance of time. Which in turn means, write store operation is mutually exclusive for other readers and writers.

In this blog, we’re going to demonstrate reader and writer go-routines. First example code shows how writer is mutually exclusive for any other type of store-lock of type sync.RWMutex. The second example shows how reader is mutually inclusive for other readers and mutually exclusive for any other writer.

==
// Example-1: sync.RWMutex.all-writers-with-random-sleep.go
// - Demonstrates WR store-lock behaviour of mutex.RWMutex.
// - WR store-lock is mutually exclusive for any other lock of type mutex.RWMutex.
// - Therefore, the resources - data and code in the critical section - are exclusively
// available for the go-routine that acquires WR store-lock.
package main

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

var cacheLock sync.RWMutex


func randomInt(min int, max int) int {
	if min >= max {
		return -1
	}

	return rand.Intn(max - min + 1) + min
}


func printMsg(goRoutineName, timeStamp, msg string) {
	fmt.Printf("%s:: %s(): %s\n", timeStamp, goRoutineName, msg)
}

func f1(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f1():  waiting on wrlock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.Lock()
	t = time.Now()
	printMsg("f1", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()),
		"got write-store-lock. am doing something great.")
	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.Unlock()

	t = time.Now()
	fmt.Printf("%s::  f1():  wrlock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


func f2(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f2():  waiting on wrlock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.Lock()
	t = time.Now()
	printMsg("f2", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()),
		"i've got write-store-lock.")
	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.Unlock()

	t = time.Now()
	fmt.Printf("%s::  f2():  wrlock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// Example-2: sync.RWMutex.all-readers-with-random-sleep.go
// main() go-routine creates a waitgroup of 2 other go-routines. cacheLock is of type mutex.RWMutex.
// Each go-routine is trying to acquire write lock over cacheLock.
// Whichever go-routine, main or f1() or f2(), acquires cacheLock first gets to execute its critical
// section first. This's how the contention amongst these 3 go-routines is resolved.
// simple fmt.Printf() calls help to understand the sequence of lock and unlock.
func main() {
	rand.Seed(time.Now().UnixNano())  // seeding to nano second granularity.

	runtime.GOMAXPROCS(runtime.NumCPU())  // allocates one logical processor for the scheduler to use

	var wg sync.WaitGroup
	wg.Add(2)

	// go-routines f1() and f2() are made part of workgroup wg.
	go f1(&wg)
	go f2(&wg)

	t := time.Now()
	fmt.Printf("%s::  main():  waiting on wrlock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.Lock()
	t = time.Now()
	printMsg("main", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()),
		"it's my turn. i've got write-store-lock.")
	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.Unlock()

	t = time.Now()
	fmt.Printf("%s::  main():  wrlock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	wg.Wait()

	t = time.Now()
	fmt.Printf("%s::  main():  exiting.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))
}


This 2nd code demonstrates how multiple readers are possible at the same instance in time:
==
// - Demonstrates RD store-lock behaviour of mutex.RWMutex.
// - RD store-lock is mutually inclusive for other RD store-lock of type mutex.RWMutex
// and mutually exclusive for WR store-lock of type mutex.RWMutex.
// - Therefore, the resources - data and code in the critical section - are inclusively
// available for the go-routine that acrquires RD store-lock.

package main

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

var cacheLock sync.RWMutex


func randomInt(min int, max int) int {
	if min >= max {
		return -1
	}

	return rand.Intn(max - min + 1) + min
}


func printMsg(goRoutineName, timeStamp, msg string) {
	fmt.Printf("%s::  %s(): %s\n", timeStamp, goRoutineName, msg)
}


func f1(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f1(): waiting for rdlock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.RLock()
	t = time.Now()
	printMsg("f1", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()),
		"got read-store-lock. am doing something great.")
	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.RUnlock()

	t = time.Now()
	fmt.Printf("%s::  f1(): am done. releasing rdlock.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


func f2(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f2(): waiting for rdlock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.RLock()
	t = time.Now()
	printMsg("f2", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()),
		"i've got read-store-lock.")

	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.RUnlock()

	t = time.Now()
	fmt.Printf("%s::  f2(): rdlock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// main() go-routine creates a waitgroup of 2 other go-routines. cacheLock is of type mutex.RWMutex.
// Each go-routine is trying to acquire read-store lock over cacheLock.
// Whichever go-routine, main or f1() or f2(), acquires cacheLock first gets to execute its critical
// section. But the difference from the previous code is multiple readers are not possible.
// This's how the contention amongst these 3 go-routines is resolved.
// simple fmt.Printf() calls help to understand the sequence of lock and unlock.
func main() {
	rand.Seed(time.Now().UnixNano())  // seeding to nano second granularity.

	runtime.GOMAXPROCS(runtime.NumCPU())  // allocates one logical processor for the scheduler to use
	var wg sync.WaitGroup
	wg.Add(2)

	go f1(&wg)
	go f2(&wg)

	t := time.Now()
	fmt.Printf("%s::  main(): waiting for rdlock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.RLock()
	t = time.Now()
	printMsg("main", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()),
		"it's my turn. i've got read-store-lock.")
	time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond)
	cacheLock.RUnlock()

	t = time.Now()
	fmt.Printf("%s::  main(): rdlock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	wg.Wait()

	t = time.Now()
	fmt.Printf("%s::  main(): exiting.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))
}
SHARE

Write a response

© 2019 Sagacity. All Rights Reserved. Sagacity logo is registered trademarks of Sagacity Software Pvt. Ltd.