【Go系列】 Sync并发控制

承上启下

在上一篇文章中,我们介绍了goroutine和channel,理论上,通过channel可以实现并发控制,但是其他语言的开发者可能更习惯一些原子操作的库。当然Go语言也会提供这样的库,所以我们今天了解一下sync库。

开始学习

首先还是介绍一下资源竞争和同步原语

资源竞争

资源竞争(Race Condition)是在并发编程中常见的问题,它发生在两个或多个goroutine(或其他并发执行单元)同时访问共享资源,并且至少有一个写操作时。由于执行顺序的不确定性,资源竞争可能会导致以下问题:

  • 数据竞争:当多个goroutine同时读取和写入同一个变量时,可能会导致变量的最终值与预期不符。
  • 竞态条件:程序的执行结果依赖于事件或操作的顺序,而这些顺序是不确定的。

资源竞争的后果可能是不可预测的,包括但不限于以下几种情况:

  • 数据损坏:共享数据结构或变量被破坏,导致程序崩溃或产生错误的结果。
  • 死锁:多个goroutine在等待对方释放资源时陷入永久阻塞状态。
  • 饥饿:某些goroutine因为资源竞争而长时间无法执行。

同步原语

同步原语(Synchronization Primitives)是并发编程中用于控制对共享资源访问的机制。它们确保在多个并发执行单元(如goroutine、线程等)之间协调操作,从而避免资源竞争和其他并发问题。以下是一些常见的同步原语:

  • 互斥锁(Mutex):互斥锁可以确保同一时间只有一个goroutine能够访问共享资源。当一个goroutine持有锁时,其他goroutine必须等待锁被释放才能访问资源。

  • 读写锁(RWMutex):读写锁是一种允许多个读操作同时进行,但在写操作时需要独占访问的锁。它适用于读多写少的场景,可以提高程序的并发性能。

  • 条件变量(Cond):条件变量通常与互斥锁结合使用,允许goroutine在某个条件成立之前等待或被唤醒。当条件成立时,一个goroutine可以通知其他正在等待的goroutine。

  • 原子操作(Atomic):原子操作提供了一种无需锁即可安全访问变量的方法。它们通常用于对基本数据类型的简单操作,确保操作的原子性和一致性。

  • 信号量(Semaphore):信号量是一个整数变量,可以用来控制对共享资源的访问数量。它通常用于实现资源池或限制并发执行的数量。

  • 等待组(WaitGroup) :等待组用于等待一组goroutine完成。主goroutine可以调用Wait方法阻塞,直到所有goroutine通过调用Done方法完成。

使用同步原语可以有效地避免资源竞争,并确保并发程序的正确性和稳定性。然而,过度使用同步原语可能会导致性能下降,因此需要根据具体场景合理选择和使用。

Sync库

Go语言的sync包提供了基本的并发编程同步原语,如互斥锁(Mutex)、读写锁(RWMutex)、条件变量(Cond)、原子操作(Atomic)以及等待组(WaitGroup)等。这些原语是构建并发程序的基础,用于控制对共享资源的访问,确保并发安全。

以下是sync包中一些常用并发控制工具的介绍:

1. Mutex(互斥锁)

互斥锁用于在代码中保护临界区,确保同一时间只有一个goroutine可以访问共享资源。

var mutex sync.Mutex

mutex.Lock() // 加锁
// 临界区代码
mutex.Unlock() // 解锁

2. RWMutex(读写锁)

读写锁允许多个goroutine同时读取同一资源,但在写入资源时,需要独占访问。适用于读多写少的场景。

var rwMutex sync.RWMutex

rwMutex.RLock() // 加读锁
// 读操作
rwMutex.RUnlock() // 解读锁

rwMutex.Lock() // 加写锁
// 写操作
rwMutex.Unlock() // 解写锁

3. Cond(条件变量)

条件变量用于等待或通知一个或多个goroutine,通常与互斥锁结合使用。

var cond sync.Cond

func main() {
    cond.L = new(sync.Mutex)
    // ...
    cond.L.Lock()
    for !condition() {
        cond.Wait() // 等待通知
    }
    // 处理条件满足后的逻辑
    cond.L.Unlock()
}

func someFunc() {
    cond.L.Lock()
    // 改变条件
    cond.Broadcast() // 通知所有等待的goroutine
    cond.L.Unlock()
}

4. Atomic(原子操作)

原子操作提供了一种无需锁即可安全访问变量的方法,适用于简单的数据类型。

import "sync/atomic"

var count int32

atomic.AddInt32(&count, 1) // 原子地增加计数
value := atomic.LoadInt32(&count) // 原子地读取值

5. WaitGroup(等待组)

等待组用于等待一组goroutine完成。主goroutine调用Wait方法阻塞,直到所有goroutine通过调用Done方法完成。

var wg sync.WaitGroup

func worker(id int) {
    defer wg.Done()
    // 执行工作
}

func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 增加等待组的计数
        go worker(i)
    }
    wg.Wait() // 等待所有goroutine完成
}

在使用这些并发控制工具时,应当注意以下几点:

  • 避免死锁:确保加锁和解锁的顺序一致,避免循环等待。
  • 最小化临界区:尽量减少锁内代码的执行时间,避免不必要的阻塞。
  • 避免饥饿:确保所有goroutine都有机会执行,避免某些goroutine长时间等待。
  • 正确使用原子操作:仅当适用于简单数据类型时使用原子操作,对于复杂操作仍需使用锁。

通过正确使用sync包提供的并发控制工具,可以有效地在Go程序中管理并发,确保程序的正确性和性能。

相关推荐
哎呦没11 分钟前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
2401_8576009513 分钟前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化
一点媛艺3 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风3 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生4 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程5 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk6 小时前
Go-性能调优实战案例
开发语言·后端·golang