go 语言学习之 mutex

竞争条件和数据竞争是并发编程中常见的两个概念,它们都涉及到多个线程或进程对共享数据的并发访问,但具体定义和应用场景有所不同。

竞争条件(Race Condition)

定义:竞争条件是指多个线程或进程在读写一个共享数据时,其最终结果依赖于它们执行的相对时间的情形。也就是说,当事件的时序影响一段代码的正确性时,就会发生竞争条件。

示例:假设两个进程P1和P2共享变量a。在某一执行时刻,P1更新a为1,在另一时刻,P2更新a为2。因此两个任务竞争地写变量a,最终变量a的值取决于哪个进程最后执行写操作。

解决方法

  1. 互斥访问:通过使用互斥锁或信号量等机制,确保在任意时刻只有一个线程可以访问共享资源。
  2. 同步访问:通过使用条件变量或事件等机制,确保线程之间的协调和通信。
  3. 数据副本:对于只读的共享资源,可以为每个线程创建一个副本,使每个线程都有自己的私有数据,从而避免竞争条件。
  4. 原子操作:使用原子操作来执行对共享资源的读取和写入操作,这样就能保证这些操作是不可中断的,从而避免竞争条件。
  5. 使用线程安全的数据结构:选择使用线程安全的数据结构,这些数据结构在内部已经实现了对共享资源的同步和互斥访问。
  6. 避免共享资源:尽可能地避免多个线程同时访问和修改共享资源的情况,可以通过设计合理的程序结构和减少共享资源的使用来避免竞争条件。

数据竞争(Data Race)

定义:数据竞争是指多个线程或进程同时访问共享数据,其中至少一个线程或进程在进行写操作,而这些访问没有进行适当的同步。数据竞争的结果通常是未定义的行为,可能导致程序崩溃或输出错误的结果。

特征

  • 并发访问:多个线程或进程同时访问共享数据。
  • 不一致的状态:数据在不同线程中可能处于不同的状态。
  • 不可预测的结果:程序的行为可能因为数据竞争而不可预测。

示例:考虑一个银行转账的场景,两个线程同时对同一个账户进行读写操作,但没有适当的同步机制,导致最终账户余额可能不一致。

解决方法

  1. 使用同步机制:通过使用互斥锁(Mutex)、读写锁(Read/Write Lock)等同步工具来保护共享数据。
  2. 设计良好的并发模型:使用高层次的并发库,以避免手动管理锁和同步,降低数据竞争的风险。
  3. 定期进行代码审查:通过定期的代码审查和测试,识别并修复潜在的并发问题。确保代码遵循最佳实践,并且使用了适当的同步机制。
  4. 使用原子操作:使用原子操作来执行对共享资源的读取和写入操作,这样就能保证这些操作是不可中断的,从而避免数据竞争。

区别

  • 定义:竞争条件是指多个线程或进程在读写共享数据时,其最终结果依赖于它们执行的相对时间的情形。数据竞争是指多个线程或进程同时访问共享数据,其中至少一个线程或进程在进行写操作,而这些访问没有进行适当的同步。
  • 影响:竞争条件可能导致程序行为不确定,但不一定导致未定义行为。数据竞争通常会导致未定义行为,可能导致程序崩溃或数据损坏。
  • 检测:数据竞争可以通过 -race 来检测。竞争条件的检测通常需要更复杂的时序分析和测试。

通过理解这两个概念的区别,可以更好地设计和实现并发程序,避免潜在的错误和性能问题。

原子操作

package main

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

type Account struct {
	balance int64
}

func (a *Account) Deposit(amount int64) {
	atomic.AddInt64(&a.balance, amount)
}

func (a *Account) Withdraw(amount int64) bool {
	for {
		currentBalance := atomic.LoadInt64(&a.balance)
		if currentBalance < amount {
			return false
		}
		newBalance := currentBalance - amount
		// 使用 CompareAndSwapInt64 确保原子性更新
		if atomic.CompareAndSwapInt64(&a.balance, currentBalance, newBalance) {
			return true
		}
		// 如果 CAS 失败,说明有其他 goroutine 修改了余额,重新尝试
	}
}

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

func transfer(from, to *Account, amount int64, wg *sync.WaitGroup) {
	defer wg.Done()
	if from.Withdraw(amount) {
		to.Deposit(amount)
		fmt.Println("Transfer successful")
	} else {
		fmt.Println("Insufficient funds")
	}
}

func main() {
	account1 := &Account{balance: 10000}
	account2 := &Account{balance: 5800}
	var wg sync.WaitGroup

	// 模拟多个并发转账
	for i := 0; i < 98; i++ {
		wg.Add(1)
		go transfer(account1, account2, 100, &wg)
	}
	wg.Wait()

	fmt.Println("Final balance of account1:", account1.Balance())
	fmt.Println("Final balance of account2:", account2.Balance())
}

使用mutex

package main

import (
	"fmt"
	"sync"
)

type Account struct {
	balance int64
	mu      sync.Mutex
}

func (a *Account) Deposit(amount int64) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.balance += amount
}

func (a *Account) Withdraw(amount int64) bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.balance < amount {
		return false
	}
	a.balance -= amount
	return true
}

func (a *Account) Balance() int64 {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.balance
}

func transfer(from, to *Account, amount int64, wg *sync.WaitGroup) {
	defer wg.Done()
	if from.Withdraw(amount) {
		to.Deposit(amount)
		fmt.Println("Transfer successful")
	} else {
		fmt.Println("Insufficient funds")
	}
}

func main() {
	account1 := &Account{balance: 10000}
	account2 := &Account{balance: 5800}
	var wg sync.WaitGroup

	// 模拟多个并发转账
	for i := 0; i < 98; i++ {
		wg.Add(1)
		go transfer(account1, account2, 100, &wg)
	}
	wg.Wait()

	fmt.Println("Final balance of account1:", account1.Balance())
	fmt.Println("Final balance of account2:", account2.Balance())
}`
posted @ 2025-01-11 19:29  yongliu  阅读(76)  评论(0)    收藏  举报