简介
相比于Java和C语言,Go的并发并没有那么昂贵。具体的讲,Go语言用goroutines映射到线程中。 今天我主要来用代码来帮大家浅浅理解一下。代码中的评论可以让大家更好的理解。
- 创建程序
- 同步
- 等待组
- 互斥
import "fmt"
import "time" func main()
{
sayHello()
// just run this function
// green thread, create abstrction go thread
// goroutines
// map goroutines onto operating thread go sayHello() time.Sleep(100 * time.Millisecond)
go sayHello() //用go关键词来开启并发的过程
time.Sleep(100 * time.Millisecond)
}
func main() {
var msg = "Hello"
go func() {
fmt.Pritnln(msg)
}()
msg = "Goodbye"
time.Sleep(100 * time.Millisecond) } // try to avoid this. Print Goodbye
//GO语言自己会把自己搞混,在这里我们输出的是在main函数里的而不是我们想要的hello
func main() {
var msg = "Hello"
go func(msg string) {
fmt.Pritnln(msg)
}(msg)
msg = "Goodbye" time.Sleep(100 * time.Millisecond) }
// print hello 为了避免这个问题,我们可以带上parameter,这个参数默认的就是一开始定义的
//介绍并行,已经锁
import "sync"
var wg = sync.WaitGroup{}
func main() {
var msg = "Hello" wg.Add(1)
// 向等待组 wg 添加一个等待任务。 /* go func(msg string) {...}(msg):创建了一个匿名的 Go 协程, 使用闭包的方式捕获了 msg 变量的值,并在协程中打印该值,然后调用 wg.Done() 表示任务完成。 */ go func(msg string) { fmt.Pritnln(msg) wg.Done() }(msg) msg= "Goodbye" wg.Wait() // 等待所有的任务完成。 }
// 不用sleep的原因
// 这是因为在使用 go func() 创建协程时,该协程会在调度后尽可能快地执行,
//因此在 go func() 中打印 msg 的值时,通常 msg 的值还没有被修改为 "Goodbye",
//而是保持着 "Hello"。 这个行为是由于 Go 语言的特性,在这种情况下,
//主线程和协程之间会共享变量 msg。 请注意,尽管在大多数情况下可能会打印出 "Hello",
//但这并不能保证一定如此。 在更复杂的程序中,可能会发生竞争条件(race condition),
//导致 msg 的值不稳定。 为了避免这种不确定性,
//最好在主线程和协程之间通过通道或者其他同步方式传递数据,而不是直接共享变量
--- // mutilple goroutine
// 导入 "sync" 包,用于等待所有 goroutine 完成 import "sync"
// 创建一个全局的等待组,用于确保所有 goroutine
//执行完成后再结束 main 函数 var wg = sync.WaitGroup{}
// 创建一个全局的计数器变量 var counter = 0
var m = sync.RWMutex{}
func main() { // 循环创建 10 组 goroutine,每组包含两个 goroutine
for i := 0; i < 10; i++ { wg.Add(2)
// 每组两个 goroutine,因此等待组需要添加 2 个等待任务
m.RLock()
go sayHello()
// 启动一个 goroutine 来执行 sayHello() 函数
m.lock()
go increment()
// 启动另一个 goroutine 来执行 increment() 函数 }
wg.Wait()
// 等待所有 goroutine 执行完成,确保 main 函数不会过早地结束
}
// sayHello 函数用于打印当前计数器的值
func sayHello() {
// 打印 "Hello" 以及当前计数器的值
fmt.Println("Hello #", counter)
m.RUnLock()
wg.Done() /
// 完成一个等待任务,表示该 goroutine 执行完成 }
// increment 函数用于将计数器增加 1 func increment() { counter++ // 将计数器加 1
m.Unlock()
wg.Done()
// 完成一个等待任务,表示该 goroutine 执行完成
}
func increment() {
counter++ // 将计数器加 1
m.Unlock()
wg.Done()
// 完成一个等待任务,表示该 goroutine 执行完成
}
Mutex
在并发编程中,当多个 goroutine 同时访问或修改共享资源时, 可能会导致竞态条件(race condition)和数据不一致的问题。 竞态条件是指多个并发操作执行的先后顺序会影响最终结果的情况,这会导致程序表现出不可预测的行为。 为了避免竞态条件和确保数据一致性,我们使用互斥锁(Mutex)。 互斥锁是一种同步原语,它允许我们保护共享资源,一次只允许一个 goroutine 访问被保护的资源。 当一个 goroutine 获得了互斥锁的所有权, 其他 goroutine 就必须等待该 goroutine 释放锁之后才能获得锁继续执行。 这样就保证了对共享资源的互斥访问,避免了竞态条件。在前面的代码示例中, 全局的计数器变量 counter 被多个 goroutine 并发地递增。如果没有使用互斥锁,就可能会发生以下情况: 两个 goroutine 同时读取 counter 的值,得到相同的结果,假设都是 0。 然后两个 goroutine 都对 counter 加 1,得到结果都是 1。 但实际上,期望的结果应该是 2(每个 goroutine 分别将 counter 从 0 加到 1)。 这就是典型的竞态条件问题,导致了数据不一致。 使用互斥锁可以解决这个问题, 它可以确保在任意时刻只有一个 goroutine 能够修改 counter, 其他 goroutine 必须等待互斥锁的释放。 修正后的代码示例中使用了 sync.WaitGroup 和 sync.Mutex, 这样保证了 counter 变量的并发安全性。当每个 goroutine 运行 increment() 函数时, 它首先会获取互斥锁的所有权,然后递增 counter 的值,最后释放互斥锁。 这样,每个 goroutine 通过互斥锁依次对 counter 进行递增,确保了正确的计数结果。
var m = sync.RWMutex{}
func main() {
// 循环创建 10 组 goroutine,每组包含两个 goroutine
for i := 0; i < 10; i++ {
wg.Add(2)
// 每组两个 goroutine,因此等待组需要添加 2 个等待任务
m.RLock()
go sayHello()
// 启动一个 goroutine 来执行
sayHello() 函数
m.lock()
go increment()
// 启动另一个 goroutine 来执行
increment() 函数
}
wg.Wait()
// 等待所有 goroutine 执行完成,确保 main 函数不会过早地结束
}
// sayHello 函数用于打印当前计数器的值
func sayHello() {
// 打印 "Hello" 以及当前计数器的值
fmt.Println("Hello #", counter)
m.RUnLock()
wg.Done()
// 完成一个等待任务,表示该 goroutine 执行完成
} // increment 函数用于将计数器增加 1 func increment() { counter++ // 将计数器加 1 m.Unlock() wg.Done() // 完成一个等待任务,表示该 goroutine 执行完成 }
读写锁
当涉及到并发访问共享资源时,读写锁(RWMutex)是一种用于管理并发访问的同步原语。RWMutex 允许多个 goroutine 并发地读取共享资源,但只允许一个 goroutine 写入(修改)共享资源。这种机制在读操作频繁而写操作较少的场景下,可以提高并发性能,因为多个 goroutine 可以同时读取资源,而只有写入时需要互斥。
RWMutex 提供了三个方法来管理读写锁:
- RLock():获取读锁。多个 goroutine 可以同时调用 RLock() 方法,只要没有 goroutine 持有写锁。当有 goroutine 持有写锁时,其他 goroutine 尝试获取读锁将被阻塞,直到写锁被释放。
- RUnlock():释放读锁。每个成功获取读锁的 goroutine 都必须调用 RUnlock() 来释放读锁。在读锁被完全释放之前,任何尝试获取写锁的 goroutine 都将被阻塞。
- Lock():获取写锁。当没有任何读锁或写锁被持有时,一个 goroutine 可以获取写锁,这将阻止其他所有的 goroutine(包括读锁的持有者)进一步获取读锁或写锁,直到写锁被释放。
RWMutex 的典型应用场景是在读操作频繁但写操作较少的情况下。当有多个 goroutine 同时需要读取共享资源时,它们可以通过获取读锁而并发执行,不会相互阻塞。而当某个 goroutine 需要写入共享资源时,它必须获取独占的写锁,这会阻塞其他所有 goroutine 的读写操作,直到写入完成并释放写锁为止。
值得注意的是,RWMutex 并不能保证公平性,即不保证等待的 goroutine 获得锁的顺序。因此,在高度竞争的情况下,可能会导致某些 goroutine 长时间地等待,这可能会引起饥饿问题。如果需要公平性,可以使用 sync.Mutex 结合其他同步原语来实现。