GoLang Synchronization Primitives

by sagacity

GoLang go-routine synchronization primitives using sync.RWMutex and sync.Mutex

 

What is Synchronization Primitives

Synchronization primitives are typical ways of resolving contentions amongst the contesting execution primitives. Execution primitives are processes and threads. Threads in GoLang are observed as go-routines, though there’re subtle differences between an OS thread and a golang go-routine.

 

Need of Synchronization

Let’s first try to understand why do we need synchronization amongst the multiple execution primitives. To set the context, execution primitives in GoLang are go-routines. Let’s try to understand this through an example. Assume a typical producer-consumer pattern. A webserver receives requests from users. These requests are REST APIs. Web server passes the requests to a channel where further actions are taking place. A dispatcher fetches request from the channel and relays is to downstream where a decision is taken as to what handler is needed to be invoked. So far so good.

Now let’s try to add a small complexity. Webserver also wants to keep a track of how many requests have been received. Typically, GoLang webserver internally invokes a go-routine – which is say a unified handler – for each request received. A better orchestration is each unified handler does the book-keeping. Incrementing a global counter is a part of this book-keeping. Later it hands over the request context to the respective handler function. Now, each unified handler is going to update the global counter. But wait, there’s a small glitch. One go-routine updates the counter and almost the same time the another one does the same. So, both the go-routines update the same original value. Thus, if the value was 100, what do you guess, what’d be the next updated value? Would it be 101 or 102? This’s a typical example of race condition since the contention amongst the go-routines wasn’t resolved appropriately and thus effectively.

Synchronization in go-routines is a way of serializing claims to access a resource – maybe data or a critical section of code or a function, so to speak – when multiple execution entities attempt to establish the claims for priority. It’s very practical in a sense that one can find its applicability in their day-to-day life. Though, asymptotically it’s very difficult to predict the the number of contestants in the race at any given instance in time, synchronization primitives help resolve the contentions amongst them.

There are many places where synchronization primitives are needed to resolve race conditions. A race condition is typically when more than 1 execution unit – process or thread – is trying to access a resource without resolving the contention.

 

Explanation of  Sync.Mutex and Sync.RWMutex with the help of Code

First type, sync.Mutex, is used to resolve contention around a single record in the data set. Try to visualise more than 1 thread which is attempting to update a line a plain text file, or more to make it a point, a record in a database table. sync.Mutex is a kind of hard lock and once acquired no other go-routine can execute or acquire the locked resource.

Read and write operations are possible in the only go-routine that has the lock at any given instance in time.

The second type is used to resolve contention around the whole data set. For instance, during adding or removing an entry in a plain text file, or more to make it a point, while adding or removing a record from a database table. It’s bit complicated and we’ll go through examples in the later sections of this post.

Let’s directly jump to the code.

Type-1: sync.Mutex

==
// Demonstrates record level locking. This's a simple mutex. A go-routine that succeeds the lock gets
// the mutex and thus can execute its part. It's then the responsibility of the very go-routine // to unlock the mutext so that some other go-routine gets a chance to execute. // Remember, locking is a blocking call and the go-routine that invokes the call to lock the mutex is blocked // until its attempt of locking the mutex succeeds. package main import ( "fmt" "runtime" "sync" "time" ) var pCacheRecLock *sync.Mutex func printMsg(goRoutineName, timeStamp, msg string) { fmt.Printf("%s(%s): %s\n", goRoutineName, timeStamp, msg) } func f1(pWG *sync.WaitGroup) { t := time.Now() fmt.Printf("f1(%s): waiting for reclock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond())) pCacheRecLock.Lock() // blocking call. go-routine is blocked untile it acquires the 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 reclock. am doing something great.") pCacheRecLock.Unlock() // mutex is now available for other go-routines to acquire the same. t = time.Now() fmt.Printf("f1(%s): am done. reclock 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("f2(%s): waiting for reclock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond())) pCacheRecLock.Lock() // blocking call. go-routine is blocked untile it acquires the 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()), "got reclock.") pCacheRecLock.Unlock() // mutex is now available for other go-routines to acquire the same. t = time.Now() fmt.Printf("f2(%s): reclock 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 main() { runtime.GOMAXPROCS(runtime.NumCPU()) // allocates one logical processor for the scheduler to use var wg sync.WaitGroup wg.Add(2) pCacheRecLock = &sync.Mutex{} // mutext is initialized. go f1(&wg) go f2(&wg) t := time.Now() fmt.Printf("main(%s): waiting for reclock...\n", fmt.Sprintf("%02d-%02d-%d:%02d-%02d-%02d-%06d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond())) pCacheRecLock.Lock() // main() go-routine is also in contention. it's blocked until it acquires the 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()), "got reclock.") t = time.Now() fmt.Printf("main(%s): reclock 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())) pCacheRecLock.Unlock() // main() go-routine has done through its part. unlocking the mutext may enable other go-routines and one of the awating go-routines // will be able to acquire the lock. wg.Wait() t = time.Now() fmt.Printf("main(%s): 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())) } == In the 2nd example each go-routine executes a random sleep just to simulate the real life scenario. == // Demonstrates record level locking. This's a simple mutex. A go-routine that succeeds the lock gets // the mutex and thus can execute its part. It's then the responsibility of the very go-routine // to unlock the mutex so that some other go-routine gets a chance to execute. // Remember, locking is a blocking call and the go-routine that invokes the call to lock the mutex is blocked // until its attempt of locking the mutex succeeds. // // Introduces a random sleep in each go-routine just to simulate the real life scenario. package main import ( "fmt" "runtime" "sync" "time" "math/rand" ) var pRecLock *sync.Mutex 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", goRoutineName, timeStamp, msg) } func f1(pWG *sync.WaitGroup) { t := time.Now() fmt.Printf("f1(%s): waiting for reclock...\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) // random sleep. pRecLock.Lock() // blocking call. go-routine is blocked untile it acquires the 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 reclock. am doing something great.") time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond) // random sleep. pRecLock.Unlock() // mutex is now available for other go-routines to acquire the same. t = time.Now() fmt.Printf("f1(%s): am done. reclock 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("f2(%s): waiting for reclock...\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) // random sleep. pRecLock.Lock() // blocking call. go-routine is blocked untile it acquires the 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()), "got reclock.") time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond) // random sleep. pRecLock.Unlock() // mutex is now available for other go-routines to acquire the same. t = time.Now() fmt.Printf("f2(%s): reclock 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 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) pRecLock = &sync.Mutex{} // mutext is initialized. go f1(&wg) go f2(&wg) t := time.Now() fmt.Printf("main(%s): waiting for reclock...\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) // main() go-routine also introduces a random sleep. pRecLock.Lock() // main() go-routine is also in contention. it's blocked until it acquires the 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()), "got reclock.") time.Sleep(time.Duration(randomInt(1000, 3000)) * time.Millisecond) // random sleep. pRecLock.Unlock() // main() go-routine has done through its part. unlocking the mutext may enable other go-routines and one of the awating go-routines // will be able to acquire the lock. t = time.Now() fmt.Printf("main(%s): reclock 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("main(%s): 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())) } ==

Above 2 examples demonstrated a simple mutex which is usually invoked to serialize execution of critical section of code or some critical data structure(s) or data object(s).

sync.Mutex is a kind of hard lock and once acquired no other go-routine can execute or acquire the locked resource. Read and write operations are possible in the only go-routine that has the lock.

SHARE

Write a response

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