问题

go mutex阻塞线程吗,如果不阻塞,为什么呢?

回答
要理解 Go 的 `sync.Mutex` 是否会阻塞线程,我们需要先弄清楚“线程”在 Go 的语境下是什么,以及“阻塞”的真正含义。

Go 的“线程”与操作系统线程

首先,一个重要的概念是,Go 语言中的“goroutine”与操作系统中的“线程”(Thread)是不同的。虽然 goroutine 最终会映射到操作系统线程上来执行,但 Go 的运行时有一个叫做“GOMAXPROCS”的调度器,它决定了有多少个 goroutine 可以同时在操作系统线程上运行。

想象一下,你有一群小蚂蚁(goroutines)在一个大的花园(操作系统线程)里搬东西。即使你有很多蚂蚁,一次也只能有有限数量的蚂蚁真正踏实在地上活动。Go 的调度器就像那个管理者,它负责把蚂蚁(goroutines)分配到花园里(操作系统线程)的哪些位置,以及什么时候让它们轮流出来休息(切换)。

所以,当我们谈论 Go 的“阻塞”时,我们通常指的是 goroutine 的阻塞,而不是直接说操作系统线程的阻塞。

`sync.Mutex` 的工作原理与阻塞

`sync.Mutex`(互斥锁)是一种同步原语,它的核心目的是为了保护共享资源,防止多个 goroutine 同时访问和修改同一份数据,导致数据损坏或不可预测的行为。

它的工作方式很简单:

1. `Lock()` 方法: 当一个 goroutine 调用 `Lock()` 方法时,它会尝试获取锁。
如果锁当前是未被占用的状态,这个 goroutine 就会成功获取锁,并可以继续执行它需要保护的代码段。
如果锁已经被其他 goroutine 占用了,那么这个尝试获取锁的 goroutine 就必须等待。

2. 阻塞的发生: 这个等待的过程,就是我们通常所说的“阻塞”。 当一个 goroutine 调用 `Lock()` 并且锁已经被占用时,这个 goroutine 不会再继续执行它后面的代码,直到它最终能够获取到锁。

3. `Unlock()` 方法: 当持有锁的 goroutine 完成了对共享资源的访问后,它会调用 `Unlock()` 方法来释放锁。
一旦锁被释放,等待在 `Lock()` 方法上的其他 goroutine 就可以有机会尝试获取锁了。

那么,为什么说 `sync.Mutex` 会阻塞 goroutine?

正是因为 `Lock()` 方法在锁被占用时会使调用它的 goroutine 暂停执行,直到锁被释放并且该 goroutine 能够成功获取锁为止。这个暂停执行的状态,就是阻塞。

举个例子来理解:

假设有三个 goroutine 想要打印一个计数器的值,但计数器只能被一个 goroutine 在同一时间访问和修改:

```go
package main

import (
"fmt"
"sync"
"time"
)

var (
counter int
mutex sync.Mutex
)

func increment() {
mutex.Lock() // 尝试获取锁
// 关键保护区域
currentCounter := counter // 读取当前值
time.Sleep(10 time.Millisecond) // 模拟一些耗时操作
counter = currentCounter + 1 // 更新值
fmt.Printf("Counter: %d ", counter)
// 保护结束
mutex.Unlock() // 释放锁
}

func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
}
```

在这个例子中:

1. 当第一个 goroutine 调用 `increment()` 时,它会调用 `mutex.Lock()`。此时锁是空的,所以它成功获取锁。
2. 它执行读取 `counter`、等待、然后更新 `counter` 的操作。
3. 在它执行完这些操作之前,第二个和第三个 goroutine 也调用了 `increment()`,并调用了 `mutex.Lock()`。
4. 由于第一个 goroutine 仍然持有锁,第二个和第三个 goroutine 在调用 `mutex.Lock()` 时就会被阻塞。它们会停在 `mutex.Lock()` 这一行,等待锁的释放。
5. 当第一个 goroutine 调用 `mutex.Unlock()` 时,锁被释放。
6. 此时,等待的第二个和第三个 goroutine 中的一个(Go 的调度器决定哪个)会获得锁,然后继续执行它在 `mutex.Lock()` 之后被暂停的代码。
7. 这个过程会一直重复,直到所有 goroutine 都成功获取锁并完成操作。

Go 调度器在阻塞时的作用

重要的是,当一个 goroutine 因为 `sync.Mutex.Lock()` 被阻塞时,Go 的调度器并不会让底层的操作系统线程也“睡大觉”或者什么都不做。Go 的调度器非常聪明:

它会将这个被阻塞的 goroutine 从当前正在运行的操作系统线程上“摘下来”,放到一个等待队列中。
然后,它会将另一个“准备就绪”(未被阻塞)的 goroutine 调度到那个操作系统线程上继续运行。

这样,即使有多个 goroutine 在等待锁,底层的操作系统线程仍然可以有效地利用起来,执行其他的 goroutine,而不是白白浪费CPU时间。这使得 Go 的并发模型非常高效。

结论

所以,回到你的问题:Go 的 `sync.Mutex` 会阻塞 goroutine 吗?

答案是肯定的,它会阻塞持有锁的 goroutine。 当一个 goroutine 调用 `Lock()` 方法时,如果锁已经被占用,该 goroutine 会暂停执行,直到成功获取锁为止。这个暂停就是阻塞。

但不应与底层的操作系统线程混淆。Go 的调度器会在 goroutine 阻塞时,巧妙地将操作系统线程重新分配给其他可运行的 goroutine,从而实现高效的并发处理。它阻塞的是 goroutine 的执行流,而不是直接让底层操作系统线程进入休眠状态。

网友意见

user avatar

不阻塞。

go的任何并发原语(包括mutex、channel)都是操作的go调度器(go scheduler)。

go调度器对于每个操作系统线程都安排了一个工作队列,队列里每个元素都是目前可以直接执行(runable)(不需要等待任何资源)的协程。于是,操作系统线程可以始终高效率地执行队列里的协程,而不需要担心被阻塞。

当go调度器发现一个协程无法继续执行时,会将该协程挂起(park),将其从当前线程的工作队列移除,于是当前线程能继续执行工作队列里的其他协程。

对于mutex,当协程无法获取锁时,协程会被标记为park然后放到mutex的等待队列里。

……但,其实事情没那么简单。mutex这东西,有时候并不会锁很长时间,比如两个协程竞争一把锁,而且上锁后很快解锁。在这种高频锁操作的场景,如果一发现上锁就将协程从工作队列移除,然后发现解锁了又将协程激活,这个开销比较大。于是Go 1.8引入了自旋(spinning),让协程发现无法获取锁时死循环几次,如果循环几次后发现能获得锁了,就直接取得锁,这样就能避免协程从工作队列移开又移回来的开销。当然,如果几次自选后还是没获得锁,这个协程还是会被从工作队列里移除的。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有