前言
说到并发编程,不得不提到并发与并行
| 并发 (Concurrency) | 并行 (Parallelism) | |
|---|---|---|
| 含义 | 逻辑上多个任务”同时进行”(可交错) | 物理上多个任务真的同时跑(多核) |
| 硬件 | 单核也能并发(靠切换) | 必须有多于一个 CPU 核(物理同时执行的能力) |
| 类比 | 一个人同时烧水+扫地(来回切换) | 两个人同时烧水和扫地 |
| Go 的衡量 | goroutine 数 | 实际用的 CPU 核数 |
对于Go而言,用户不能直接操纵操作系统的线程,只能通过创建Goroutine在线程上工作,实现并发。而并行,取决于CPU有几个核、OS如何分配线程到核。也就是说并发(结构)在 Go 层,并行(物理同时)在硬件/OS 层
对于广义的“并发”,实际上包含了上述的两个概念。
在runtime下存在
runtime.NumCPU()方法,他能查看CPU逻辑核心数,并通过GOMAXPROCS(允许并行度)指定Goroutine最大并行数量
并发问题
单机环境中,由于线程/协程共享进程内存,因此在对同一变量(临界资源)进行修改会引发并发问题
var ticket = 10
func main() {
var wg sync.WaitGroup
wg.Go(func() { saleTicket("sale1:") })
wg.Go(func() { saleTicket("sale2:") })
wg.Go(func() { saleTicket("sale3:") })
wg.Wait()
fmt.Println("final ticket:", ticket) // -2
}
func saleTicket(s string) {
for {
if ticket > 0 {
fmt.Println(s, ticket)
ticket--
} else {
fmt.Println(s + "库存不足")
return
}
}
}
为了解决这一问题我们引入锁
互斥锁
| 方法 | 签名 | 说明 |
|---|---|---|
| Lock | func (m *Mutex) Lock() | 加锁。若已有人持有则阻塞等待 |
| Unlock | func (m *Mutex) Unlock() | 释放锁。未加锁时调用会 panic |
| TryLock | func (m *Mutex) TryLock() bool | 非阻塞尝试加锁。拿到返回 true,否则 false |
对于TryLock,他不像Lock会一直等待抢占的资源被释放,而是当检测到资源被占用时放弃上锁。用于跳过任务/防止重复执行任务/测锁状态执行其他路径
利用互斥锁可以解决库存超卖问题:
var ticket = 10
var mu sync.Mutex
func main() {
var wg sync.WaitGroup
wg.Go(func() { saleTicket("sale1:") })
wg.Go(func() { saleTicket("sale2:") })
wg.Go(func() { saleTicket("sale3:") })
wg.Wait()
fmt.Println("final ticket:", ticket)
}
func saleTicket(s string) {
for {
mu.Lock()
if ticket > 0 {
time.Sleep(time.Duration(rand.N(1000)) * time.Microsecond) // 休眠期间被其他写成抢占修改,导致超卖
runtime.Gosched()
fmt.Println(s, ticket)
ticket--
} else {
fmt.Println(s + "库存不足")
mu.Unlock()
return
}
mu.Unlock()
}
}
读写锁
Mutex与RWMutex对比
| 特性 | Mutex | RWMutex |
|---|---|---|
| 分组 | 只有一种锁(互斥) | 读锁 + 写锁 |
| 读操作 | 也要互斥(读也排队) | 读锁共享,多读者同时进 |
| 写操作 | 互斥 | 写锁独占,挡所有读+写 |
| 方法 | Lock/Unlock/TryLock | RLock/RUnlock/Lock/Unlock/TryRLock/TryLock |
| 适用 | 读写频次相近/临界区短 | 读远多于写 |
| 性能(读多) | 较差(读也排队) | 更好(读并发) |
| 坑 | 混用/漏解锁死锁 | 漏 RUnlock、写饥饿、读写锁混用 |
注意:
- 当A执行Lock时,B无法执行RLock,Lock
- 当A执行RLock时,B可以RLock,不能Lock
由于读写锁中只有写锁能保证互斥,所以针对上述的超卖问题只有写锁有用,读锁没用
并且读写锁更适合运用在单独的业务查询,业务更新,读多写少的情况
channel
channel也可以实现类似于锁的效果
func main() {
var wg sync.WaitGroup
ch := make(chan struct{}, 1)
ch <- struct{}{}
wg.Go(func() { saleTicket("sale1:", ch) })
wg.Go(func() { saleTicket("sale2:", ch) })
wg.Go(func() { saleTicket("sale3:", ch) })
wg.Wait()
fmt.Println("final ticket:", ticket)
}
func saleTicket(s string, ch chan struct{}) {
for {
<-ch
if ticket > 0 {
fmt.Println(s, ticket)
ticket--
ch <- struct{}{}
} else {
fmt.Println(s + "库存不足")
ch <- struct{}{}
return
}
}
}
评论(0)
暂无评论