面试题答案
一键面试资源竞争产生的原因及常见场景
- 原因:在Go语言中,资源竞争通常是由于多个goroutine同时访问和修改共享资源而没有适当的同步机制。Go语言的并发模型鼓励通过共享内存来通信,这使得共享资源容易被多个goroutine同时访问,从而导致数据不一致或其他未定义行为。
- 常见场景:
- 读写操作:一个goroutine读取共享变量,同时另一个goroutine写入该变量。例如,多个goroutine同时读取和更新一个全局计数器。
- 复合操作:某些操作看似原子,但实际上由多个步骤组成。如先读取变量值,然后基于该值进行计算并写回,在这期间可能有其他goroutine修改了该变量。
使用sync
包工具解决资源竞争问题
- sync.Mutex:互斥锁用于保护共享资源,确保同一时间只有一个goroutine可以访问共享资源。当一个goroutine获取了锁,其他goroutine必须等待锁被释放才能获取并访问资源。
package main import ( "fmt" "sync" ) var ( balance int mu sync.Mutex ) func deposit(amount int, wg *sync.WaitGroup) { defer wg.Done() mu.Lock() balance = balance + amount mu.Unlock() } func main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go deposit(10, &wg) } wg.Wait() fmt.Println("Final balance:", balance) }
- sync.WaitGroup:用于等待一组goroutine完成。在上述代码中,
sync.WaitGroup
确保所有存款操作完成后才打印最终余额。wg.Add(1)
表示增加等待的goroutine数量,wg.Done()
表示一个goroutine完成任务,wg.Wait()
阻塞当前goroutine直到所有任务完成。
使用工具可能引入的性能瓶颈及优化方法
- 性能瓶颈:
- 锁争用:如果多个goroutine频繁地竞争锁,会导致性能下降,因为等待锁的goroutine处于阻塞状态,无法执行其他任务。
- 死锁:如果锁的使用不当,例如相互等待对方释放锁,会导致死锁,整个程序无法继续执行。
- 优化方法:
- 减少锁的粒度:只在必要的代码块上加锁,尽量缩短锁的持有时间。例如,在银行转账程序中,如果有多个账户,可以为每个账户设置独立的锁,而不是使用全局锁。
- 读写锁:对于读多写少的场景,可以使用
sync.RWMutex
。读操作可以并发执行,只有写操作需要独占锁,这样可以提高并发性能。 - 避免死锁:确保锁的获取顺序一致,避免循环依赖。可以使用资源分配图算法(如死锁检测算法)来检测和预防死锁。
银行转账模拟程序
package main
import (
"fmt"
"sync"
)
type Account struct {
balance int
mu sync.Mutex
}
func (a *Account) Withdraw(amount int, wg *sync.WaitGroup) {
defer wg.Done()
a.mu.Lock()
if a.balance >= amount {
a.balance -= amount
}
a.mu.Unlock()
}
func (a *Account) Deposit(amount int, wg *sync.WaitGroup) {
defer wg.Done()
a.mu.Lock()
a.balance += amount
a.mu.Unlock()
}
func Transfer(from, to *Account, amount int, wg *sync.WaitGroup) {
defer wg.Done()
// 按固定顺序获取锁,避免死锁
if from < to {
from.mu.Lock()
to.mu.Lock()
} else {
to.mu.Lock()
from.mu.Lock()
}
if from.balance >= amount {
from.balance -= amount
to.balance += amount
}
from.mu.Unlock()
to.mu.Unlock()
}
func main() {
var wg sync.WaitGroup
account1 := &Account{balance: 100}
account2 := &Account{balance: 200}
for i := 0; i < 10; i++ {
wg.Add(1)
go Transfer(account1, account2, 10, &wg)
}
wg.Wait()
fmt.Println("Account1 balance:", account1.balance)
fmt.Println("Account2 balance:", account2.balance)
}
在上述银行转账模拟程序中,每个账户都有自己的互斥锁来保护余额。Transfer
函数通过按固定顺序获取锁,避免了死锁问题,从而确保了资源竞争问题的解决并优化了性能。