Race condition

Race condition

What is the race condition?

A race condition occurs when two or more threads can access shared data and they try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any time, you don't know the order in which the threads will attempt to access the shared data. Therefore, the result of the change in data is dependent on the thread scheduling algorithm, i.e. both threads are "racing" to access/change the data.


Problems often occur when one thread does a "check-then-act" (e.g. "check" if the value is X, then "act" to do something that depends on the value being X) and another thread does something to the value in between the "check" and the "act".

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"

   // If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
   // y will not be equal to 10.
}        

The point being, y could be 10, or it could be anything, depending on whether another thread changed x in between the check and act. You have no real way of knowing.


How do you detech them??

Detecting and identifying race conditions is considered difficult. They are a semantic problem that can arise from many possible flaws in code. It's best to design code in a way that prevents these problems from the start.


In golang , use? Go Project's Data Race Detector?

go run -race mysrc.go        


Manual inspection of the code can often reveal potential race conditions, especially when multiple threads? access shared resources.


Writing tests specifically designed to expose race conditions can help detect them. Techniques such as stress testing, where the system is subjected to heavy loads or unusual conditions, can help uncover race conditions that might not occur during normal operation.


How do you handle them?

Here are some strategies to handle race conditions in a database:


Database Transactions

Use database transactions to group multiple operations into an atomic unit. Transactions ensure that either all the operations within the transaction are applied or none of them, maintaining data consistency.


Isolation Levels

Choose an appropriate isolation level for your transactions. Isolation levels like Read Committed, Repeatable Read, or Serializable provide different levels of protection against race conditions and allow you to control the visibility of uncommitted changes


Use of Locks


In order to prevent race conditions from occurring, you would typically put a lock around the shared data to ensure only one thread can access the data at a time. This would mean something like this:

// Obtain lock for x
if (x == 5)
{
   y = x * 2; // Now, nothing can change x until the lock is released. 
              // Therefore y = 10
}
// release lock for x        

Most modern relational databases support row-level locking, which allows you to lock individual rows during updates. By locking only the specific rows that are being modified, you reduce contention and the risk of race conditions.

In addition to row-level locking, some databases allow you to use explicit locks like shared locks and exclusive locks to control access to data. Shared locks allow multiple processes to read a data simultaneously, while exclusive locks prevent other processes from accessing the data until the lock is released.

Pessimistic Locking: this involves explicitly locking a record or row before modifying it. Other processes will have to wait until the lock is released, avoiding simultaneous modifications.

Optimistic Locking: Optimistic locking is a technique where you check if the data has been modified by another process since you last read it. You typically do this by including a version or timestamp in the record and checking it before performing an update. If the version or timestamp has changed, you know that another process has modified the data, and you can handle the conflict accordingly.



Concurrency Control Mechanisms:

Many databases support built-in mechanisms for handling concurrency and preventing race conditions. For example, PostgreSQL uses Multi-Version Concurrency Control (MVCC) to manage concurrent access to data, ensuring each transaction sees a consistent snapshot of the data.




In Golang:

Synchronization with Mutexes: Use mutexes (mutual exclusions) to protect shared resources from concurrent access. By locking and unlocking mutexes appropriately, you ensure that only one goroutine can access the shared resource at a time.

import "sync"

var mutex sync.Mutex
var sharedData int

func updateSharedData(newValue int) {
    mutex.Lock()
    defer mutex.Unlock()
    sharedData = newValue
}
        


Atomic Operations: Go provides atomic operations in the sync/atomic package, which allow you to perform operations such as incrementing and swapping values atomically without the need for explicit locking.

import "sync/atomic"

var sharedCounter uint64

func incrementCounter() {
    atomic.AddUint64(&sharedCounter, 1)
}        


Channels: Use channels for communication between goroutines instead of shared memory. Channels provide a safe way for goroutines to communicate by passing messages. By design, sending and receiving operations on channels are synchronized, preventing race conditions.

var sharedChannel = make(chan int)

func sendData(data int) {
    sharedChannel <- data
}

func receiveData() int {
    return <-sharedChannel
}
        

Immutable Data: Whenever possible, design your data structures to be immutable. Immutable data cannot be modified once created, so there's no need for synchronization when accessing it concurrently.

type ImmutableData struct {
    Value int
}

func (d ImmutableData) Increment() ImmutableData {
    return ImmutableData{Value: d.Value + 1}
}
        

Caching: Use caching mechanisms to reduce the frequency of database accesses. By caching frequently used data in memory, you can reduce the chance of race conditions when multiple processes need to access the same data concurrently

Testing and Race Detectors: Utilize Go's race detector tool (go test -race) to identify and debug race conditions in your code. Additionally, thorough testing with concurrent scenarios can help uncover potential race conditions before they manifest in production.



Example

Imagine a banking application where multiple clients can deposit and withdraw money from their accounts concurrently. Each account has a balance, and clients can make multiple transactions simultaneously.

package main

import (
	"fmt"
	"sync"
)

type Account struct {
	balance int
	mutex   sync.Mutex
}

func (acc *Account) Deposit(amount int) {

	acc.balance += amount
}

func (acc *Account) Withdraw(amount int) {

	acc.balance -= amount
}

func (acc *Account) Balance() int {
	return acc.balance
}

func main() {
	acc := &Account{balance: 1000}
	var wg sync.WaitGroup
	numTransactions := 1000 // Number of concurrent transactions

	for i := 0; i < numTransactions; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			acc.Deposit(100) // Simulate concurrent deposits
		}()

		wg.Add(1)
		go func() {
			defer wg.Done()
			acc.Withdraw(50) // Simulate concurrent withdrawals
		}()
	}

	wg.Wait()

	fmt.Println("Final balance:", acc.Balance())
}
        

Output:

go run main.go
Final balance: 47350

go run main.go
Final balance: 47450

go run main.go
Final balance: 47250        


To fix the race condition in the provided code, you can continue to use the mutex to synchronize access to the account's balance. The mutex ensures that only one goroutine can access the account's balance at a time, preventing concurrent modifications that could lead to race conditions.

package main

import (
	"fmt"
	"sync"
)

type Account struct {
	balance int
	mutex   sync.Mutex
}

func (acc *Account) Deposit(amount int) {
	acc.mutex.Lock()
	defer acc.mutex.Unlock()
	acc.balance += amount
}

func (acc *Account) Withdraw(amount int) {
	acc.mutex.Lock()
	defer acc.mutex.Unlock()
	acc.balance -= amount
}

func (acc *Account) Balance() int {
	acc.mutex.Lock()
	defer acc.mutex.Unlock()
	return acc.balance
}

func main() {
	acc := &Account{balance: 1000}
	var wg sync.WaitGroup
	numTransactions := 1000 // Number of concurrent transactions

	for i := 0; i < numTransactions; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			acc.Deposit(100) // Simulate concurrent deposits
		}()

		wg.Add(1)
		go func() {
			defer wg.Done()
			acc.Withdraw(50) // Simulate concurrent withdrawals
		}()
	}

	wg.Wait()

	fmt.Println("Final balance:", acc.Balance())
}
        

Output:

go run main.go
Final balance: 51000

go run main.go
Final balance: 51000

go run main.go
Final balance: 51000        

In scenarios where performance is critical and locking might introduce contention, an optimistic locking approach can be more suitable. Optimistic locking minimizes the use of locks and allows multiple threads to work concurrently with the assumption that conflicts are rare

In Go, you can achieve optimistic locking using techniques like Compare-and-Swap (CAS) operations provided by the sync/atomic package. Instead of locking entire sections of code, CAS allows you to perform atomic compare-and-swap operations on shared variables.


package main

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

type Account struct {
	balance int64
}

func (acc *Account) Deposit(amount int64) {
	for {
		oldBalance := atomic.LoadInt64(&acc.balance)
		newBalance := oldBalance + amount
		if atomic.CompareAndSwapInt64(&acc.balance, oldBalance, newBalance) {
			break
		}
	}
}

func (acc *Account) Withdraw(amount int64) {
	for {
		oldBalance := atomic.LoadInt64(&acc.balance)
		newBalance := oldBalance - amount
		if atomic.CompareAndSwapInt64(&acc.balance, oldBalance, newBalance) {
			break
		}
	}
}

func (acc *Account) Balance() int64 {
	return atomic.LoadInt64(&acc.balance)
}

func main() {
	acc := &Account{balance: 1000}
	var wg sync.WaitGroup
	numTransactions := 1000 // Number of concurrent transactions

	for i := 0; i < numTransactions; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			acc.Deposit(100) // Simulate concurrent deposits
		}()

		wg.Add(1)
		go func() {
			defer wg.Done()
			acc.Withdraw(50) // Simulate concurrent withdrawals
		}()
	}

	wg.Wait()

	fmt.Println("Final balance:", acc.Balance())
}
        

Output

go run main.go
Final balance: 51000

go run main.go
Final balance: 51000

go run main.go
Final balance: 51000        

要查看或添加评论,请登录

trong luong van的更多文章

  • Upload Files to S3 from Go Backend - Part 2 : Setup Amazon CloudFront vs Security

    Upload Files to S3 from Go Backend - Part 2 : Setup Amazon CloudFront vs Security

    Amazon CloudFront is a content delivery network (CDN) that delivers your content with low latency and high transfer…

  • Upload Files to S3 from Go Backend - Part 1 : Setup S3 Bucket vs Security

    Upload Files to S3 from Go Backend - Part 1 : Setup S3 Bucket vs Security

    Step 1: Sign in to the AWS Management Console Step 2: Create an S3 Bucket Navigate to the S3 service from the console…

  • Cache avalanche , Cache penetration , Cache breakdown

    Cache avalanche , Cache penetration , Cache breakdown

    Cache avalanche Cache avalanche is a scenario where lots of cached data expire at the same time or the cache service is…

  • Mysql Master-Slave Replication setup on Docker

    Mysql Master-Slave Replication setup on Docker

    In the world of database management, replication plays a vital role in ensuring data availability, scalability, and…

    2 条评论
  • How to Install PostgreSQL on Amazon Linux

    How to Install PostgreSQL on Amazon Linux

    Step 1- Launching and Configuring Your EC2 Instance 1. Log in to AWS services and select EC2.

    2 条评论
  • Solid

    Solid

    The SOLID principles are a set of design guidelines that help developers write more maintainable, scalable, and…

  • Change Data Capture

    Change Data Capture

    What is CDC ? Change Data Capture (CDC) is the process of recognizing when data has changed in source system so that a…

  • SQL injection

    SQL injection

    What is SQL injection? SQL injection is an attack that occurs when malicious SQL (Structured Query Language) code is…

  • ORM

    ORM

    What is ORM? ORM stands for object-relational mapping. It's a programming technique that allows data to be mapped…

    1 条评论
  • What is ACID Database?

    What is ACID Database?

    Transaction is a collection of sequel queries that are treated as one unit of work Atomicity group multiple operations…

社区洞察

其他会员也浏览了