Combination of readers and writers – GoLang

by sagacity

In the last blog which was titled as readers and writers, we saw how multiple readers and writers do interplay. The truth-table that we saw helped us understand the rationale of multiple readers and writers.

Let’s do a quick recap of the same.

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 underwrite 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’ll demonstrate the above truth-table in conjunction with examples that involve a combination of multiple readers and writers so that readers and writers will co-exist and try to execute critical sections.

Example-1: Two readers and two writers with main() writer first In this example, main() go-routine is made to start first as in starts with minimum delay. main() go-routine takes write-store-lock so that all other go-routines are in the held up state until main() go-routine unlocks the write-store-lock. f1() and f3() are readers and f2() and f4() are writers. If either of the writers starts first amongst them, all other go-routines have to wait until the write-store-lock is unlocked. If either of the readers starts first, the writers have to wait until the read-store-lock is unlocked. Any whichever way, above mentioned truth-table always holds true.


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)
}


// reader go routine
func f1(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f1():  waiting for read-store-lock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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-%09d", 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():  read-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// writer go routine
func f2(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f2():  waiting for write-store-lock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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-%09d", 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():  write-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// reader go routine
func f3(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f3():  waiting for read-store-lock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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("f3", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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::  f3():  read-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// writer go routine
func f4(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f4():  waiting for write-store-lock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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("f4", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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::  f4():  write-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// Whichever go-routine, main() or f1() or f2() or f3() or f4(), acquires first gets to
// execute its critical section first. This's how the contention amongst these 5 go-routines
// is resolved. Simple fmt.Printf() calls help to understand the sequence of lock and unlock.
// fmt.Printf() call flushes the data to stdout as per its own way. But don't forget to look
// for the timestamp of the call.
// main() go-routine is made probable to start first since it has least amount of delay.
// One can experiment to make it still more probable by removing the sleep.
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(4)

	// go-routines f1(), f2(), f3(), and f4() are made part of workgroup wg.
	go f1(&wg)  // reader
	go f2(&wg)  // writer
	go f3(&wg)  // reader
	go f4(&wg)  // writer

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

	// just a hack. making it more probable for the main() go-routine to start first.
	time.Sleep(time.Duration(randomInt(10, 30)) * time.Millisecond)
	cacheLock.Lock()
	t = time.Now()
	printMsg("main", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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():  write-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))
}

Example-2: Two readers and two writers with main() reader first In this example, main() go-routine is made to start first as it starts with minimum delay, at least, it is the most likely probability. main() go-routine takes read-store-lock so that other readers can start parallelly but the writers are blocked. If one of the writers starts after the read-store-lock is unlocked all other go-routines are blocked. Any whichever way, above mentioned truth-table always holds true.

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)
}


// reader go routine
func f1(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f1():  waiting for read-store-lock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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-%09d", 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():  read-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// writer go routine
func f2(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f2():  waiting for write-store-lock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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-%09d", 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():  write-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// reader go routine
func f3(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f3():  waiting for read-store-lock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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("f3", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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::  f3():  read-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// writer go routine
func f4(pWG *sync.WaitGroup) {
	t := time.Now()
	fmt.Printf("%s::  f4():  waiting for write-store-lock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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("f4", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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::  f4():  write-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(),
		t.Second(), t.Nanosecond()))

	pWG.Done()
}


// Whichever go-routine, main() or f1() or f2() or f3() or f4(), acquires first gets to
// execute its critical section first. This's how the contention amongst these 5 go-routines
// is resolved. Simple fmt.Printf() calls help to understand the sequence of lock and unlock.
// fmt.Printf() call flushes the data to stdout as per its own way. But don't forget to look
// for the timestamp of the call.
// main() go-routine is made more probable to start first since it has least amount of delay.
// One can experiment to make it still more probable by removing the sleep.
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(4)

	// go-routines f1(), f2(), f3(), and f4() are made part of workgroup wg.
	go f1(&wg)  // reader
	go f2(&wg)  // writer
	go f3(&wg)  // reader
	go f4(&wg)  // writer

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

	// just a hack. making it more probable for the main() go-routine to start first.
	time.Sleep(time.Duration(randomInt(10, 30)) * time.Millisecond)
	cacheLock.RLock()
	t = time.Now()
	printMsg("main", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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():  read-store-lock released.\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%09d", 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-%09d", 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.