SECRET OF CSS

Atomic Pointers in Go 1.19. Easily manage shared resources | by Cheikh seck | Aug, 2022


Easily manage shared resources

“Atomic”, in computer programming, refers to performing operations one at a time. Objective-C has atomic properties, it ensures safe read and writes to a property from different threads.

In Objective-C, it is used with immutable types. This is because immutable types are really “recreated” in order to change it. In other words, changing an immutable type in your code will not cause the compiler to throw an error.

However, it’ll instantiate a new object when you do so. An example is Go’s append function, it makes a new array when the array capacity is full. In Obj-C, atomic properties will ensure operations are performed one after another, to prevent threads from accessing a memory address at the same time. Since Go is multithreaded, it supports atomic operations as well.

Go 1.19 introduces new atomic types. My favorite addition is atomic.Pointer , it provides a sleek alternative to atomic.Value . It’s also a great showcase of how generics enhance the developer experience.

atomic.Pointer is a generic type. Unlike Value , it does not require asserting your stored value to access it. Here is a code block that defines and stores a pointer :

package mainimport (
"fmt"
"net"
"sync/atomic"
)
type ServerConn struct {
Connection net.Conn
ID string
Open bool
}
func main() {
p := atomic.Pointer[ServerConn]{}
s := ServerConn{ ID : "first_conn"}
p.Store( &s )
fmt.Println(p.Load()) // Will display value stored.
}

I instantiate variable pas a struct literal. I then proceed to store the pointer of variable s in p , s represents a server connection. Voila, we’ve passed the first step toward atomicity.

By storing the variable as an atomic value, we’ll ensure that there is no simultaneous access to the memory address. For example, maps will cause a program to panic if it is being read and written to at the same time. Locks are a great way to prevent these panics from occurring, so are atomic operations.

To build on the previously provided code block, I’ll use an atomic.Pointer to recreate a database connection every 13 seconds. I’ll start by writing one function that will be used to log the connection ID every 10 seconds.

It’ll be my mechanism to see if the new connection object has been propagated. I’ll then define an inline function to change the connection every 13 seconds. Here is what the code will look like :

...func ShowConnection(p * atomic.Pointer[ServerConn]){for {
time.Sleep(10 * time.Second)
fmt.Println(p, p.Load())
}

}

func main() { c := make(chan bool)
p := atomic.Pointer[ServerConn]{}
s := ServerConn{ ID : "first_conn"}
p.Store( &s )
go ShowConnection(&p) go func(){
for {
time.Sleep(13 * time.Second)
newConn := ServerConn{ ID : "new_conn"}
p.Swap(&newConn)
}
}()
<- c
}

ShowConnection is invoked as a Goroutine. The inline function will instantiate aServerConn object and swap it with the current connection object. This is possible with just pointers, however, it would require implementing a “lock-unlock” system.

The atomic package abstracts this, and makes sure each load and save is processed one after the other. This is a simple example, and a not-so-common use case. Plus, the usage of atomic.Pointer may be a case of “over-engineering,” as my program’s Goroutines run on separate intervals. I’ll use Go’s race flag to see if my program’s Goroutines access the same memory address at the same time. I’ll also rewrite my code above to use pointers instead of atomic.Pointer.

“A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write.” To quickly verify a data race, you can execute go run with flag race to perform the test. To demonstrate how atomic types can prevent this, I’ve rewritten my example above to use classic Go pointers. Here is how the code will look like :

package mainimport (
"fmt"
"net"
"time"
)
type ServerConn struct {
Connection net.Conn
ID string
Open bool
}
func ShowConnection(p * ServerConn){for {
time.Sleep(10 * time.Second)
fmt.Println(p, *p)
}

}

func main() { c := make(chan bool)
p := ServerConn{ ID : "first_conn"}
go ShowConnection(&p) go func(){
for {
time.Sleep(13 * time.Second)
newConn := ServerConn{ ID : "new_conn"}
p = newConn
}
}()
<- c
}

After checking for a data race, this was the output on the terminal :

cheikh@cheikh-s5-1110:~/go/src/atomic$ go run -race main_classic.go 
&{<nil> first_conn false} {<nil> first_conn false}
==================
WARNING: DATA RACE
Write at 0x00c000074570 by goroutine 8:
main.main.func1()
/home/cheikh/go/src/atomic/main_classic.go:37 +0x6f
Previous read at 0x00c000074570 by goroutine 7:
runtime.convT()
/usr/lib/go-1.18/src/runtime/iface.go:321 +0x0
main.ShowConnection()
/home/cheikh/go/src/atomic/main_classic.go:19 +0x65
main.main.func2()
/home/cheikh/go/src/atomic/main_classic.go:30 +0x39
Goroutine 8 (running) created at:
main.main()
/home/cheikh/go/src/atomic/main_classic.go:33 +0x16e
Goroutine 7 (running) created at:
main.main()
/home/cheikh/go/src/atomic/main_classic.go:30 +0x104
==================
&{<nil> new_conn false} {<nil> new_conn false}
&{<nil> new_conn false} {<nil> new_conn false}
&{<nil> new_conn false} {<nil> new_conn false}

Although the 2 functions are running at different intervals, they collide at some point. The code with atomic pointers returned no feedback regarding a data race. This is an example of how atomic pointers performs better in a multithreaded environment.

Go atomic types are an easy way to manage shared resources. It removes the need to constantly implement a mutex to control resource access.

This does not imply that mutexes are obsolete, as they are still required in certain operations.

In conclusion, atomic.Pointer is a great way to bring atomic memory primitives into your program. It is a simple way to prevent data races without fancy mutex code in place. There is a link to the code used in this post below.



News Credit

%d bloggers like this: