Atomicity in Programming

2 October 2024

@Bibhabendu Mukherjee

Atomicity in programming ensures operations complete entirely or not at all, preventing partial updates and protecting data integrity.

Atomicity or Atomic Operation

Atomicity refers to operations that are performed as a single, indivisible step. In programming, an atomic operation ensures that a particular task is completed entirely without interference from other operations. This guarantees data consistency, especially in concurrent or multi-threaded environments where multiple processes might attempt to read or write shared data simultaneously.

Example

Imagine a bank transaction where you transfer money from your account to a friend's account. Atomicity ensures that either the entire transaction completes successfully (debiting your account and crediting your friend's account) or, in case of any failure, the transaction doesn't happen at all. This prevents scenarios where money is deducted from your account but not credited to your friend's account, maintaining the integrity of both accounts.

Implementing Atomicity in Golang

Golang provides several mechanisms to achieve atomicity, especially when dealing with shared variables in concurrent programming. One of the primary tools for this purpose is the sync/atomic package

Example Scenario: Let's consider a scenario where multiple goroutines increment a shared counter. Without atomic operations, this can lead to race conditions where multiple goroutines read, modify, and write the counter simultaneously, causing incorrect results.

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var counter int64 = 0
	var wg sync.WaitGroup
	numGoroutines := 100
	incrementsPerGoroutine := 1000

	// Start multiple goroutines to increment the counter
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < incrementsPerGoroutine; j++ {
				// Atomic increment
				atomic.AddInt64(&counter, 1)
			}
		}()
	}

	// Wait for all goroutines to finish
	wg.Wait()

	// Expected counter value: numGoroutines * incrementsPerGoroutine
	expected := int64(numGoroutines * incrementsPerGoroutine)
	fmt.Printf("Final Counter: %d\n", counter)
	fmt.Printf("Expected Counter: %d\n", expected)

	if counter == expected {
		fmt.Println("Atomic operation successful. Counter is correct.")
	} else {
		fmt.Println("Atomic operation failed. Counter is incorrect.")
	}
}

Explanation:

Shared Variable:

  • counter is a shared int64 variable that multiple goroutines will increment.

WaitGroup:

  • sync.WaitGroup is used to wait for all goroutines to finish execution before proceeding.

Launching Goroutines:

  • We launch numGoroutines (e.g., 100) goroutines.
  • Each goroutine increments the counter incrementsPerGoroutine (e.g., 1000) times.

Atomic Increment:

  • Instead of using counter++, which is not atomic and can lead to race conditions, we use atomic.AddInt64(&counter, 1).
  • The atomic.AddInt64 function ensures that the increment operation is performed atomically, preventing race conditions.

Image