【Go语言学习系列31】并发编程(四):sync包

【Go】Golang Sync包 一. 前言 Golang sync包提供了基础的异步操作方法,包括互斥锁Mutex,执行一次Once和并发等待组WaitGroup。 本文主要介绍sync包提供的这些功能的基本使用方法。 Mutex: 互斥锁 RWMutex:读写锁 WaitGroup:并发等待组 Once:执行一次 Cond:信号量 Pool:临时对象池 Map:自带锁的map 二. sync.Mutex sync.Mute... 阅读详情

📚 原创系列: “Go语言学习系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言学习系列导航

本文是【Go语言学习系列】的第31篇,当前位于第三阶段(进阶篇)

🚀 第三阶段:进阶篇
  1. 并发编程(一):goroutine基础
  2. 并发编程(二):channel基础
  3. 并发编程(三):select语句
  4. 并发编程(四):sync包 👈 当前位置
  5. 并发编程(五):并发模式
  6. 并发编程(六):原子操作与内存模型
  7. 数据库编程(一):SQL接口
  8. 数据库编程(二):ORM技术
  9. Web开发(一):路由与中间件
  10. Web开发(二):模板与静态资源
  11. Web开发(三):API开发
  12. Web开发(四):认证与授权
  13. Web开发(五):WebSocket
  14. 微服务(一):基础概念
  15. 微服务(二):gRPC入门
  16. 日志与监控
  17. 第三阶段项目实战:微服务聊天应用

📚 查看完整Go语言学习系列导航

📖 文章导读

在本文中,您将了解:

  • Go语言sync包提供的各种同步原语及其使用场景
  • 互斥锁(Mutex)和读写锁(RWMutex)的正确使用方法
  • WaitGroup、Once和Cond等工具的实际应用
  • Map和Pool等并发安全的数据结构
  • 使用sync包的最佳实践和常见陷阱

虽然Go推崇"通过通信来共享内存"的CSP模型,但在某些场景下,使用传统的同步原语控制共享资源访问更加直接高效。本文将帮助你掌握sync包中的工具,以便在适当的场景下选择正确的并发控制机制。

Go sync包示意图

并发编程(四):sync包

在前面的文章中,我们深入探讨了Go语言的并发基础:goroutine、channel和select语句。这些机制主要基于CSP(通信顺序进程)模型,强调"通过通信来共享内存"。然而,在某些情况下,我们需要直接控制对共享资源的访问。这时,Go语言标准库中的sync包就派上用场了。

本文将详细介绍sync包中提供的同步原语,帮助你在适当的场景下选择正确的同步工具。

一、Mutex与RWMutex

互斥锁(Mutex)和读写锁(RWMutex)是sync包中最常用的同步原语,用于保护共享资源免受并发访问的干扰。

1.1 Mutex(互斥锁)

Mutex提供了一种互斥机制,确保同一时间只有一个goroutine可以访问共享资源。

基本用法:

package main

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

func main() {
    var mutex sync.Mutex
    counter := 0
    
    // 并发更新counter
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            
            mutex.Lock()         // 加锁
            defer mutex.Unlock() // 解锁
            
            counter++
        }()
    }
    
    wg.Wait()
    fmt.Println("计数器最终值:", counter) // 输出: 计数器最终值: 1000
}

重要方法:

  • Lock():获取锁。如果锁已被其他goroutine获取,则阻塞直到锁可用
  • Unlock():释放锁。应在与Lock()相同的goroutine中调用
  • TryLock():(Go 1.18+)尝试获取锁,如果锁不可用则立即返回false而不阻塞

最佳实践:

  1. 总是使用defer mutex.Unlock()确保锁被释放
  2. 尽量减小临界区(加锁和解锁之间的代码)
  3. 避免在持有锁的情况下调用可能阻塞的操作
  4. 不要在goroutine A中锁定,然后在goroutine B中解锁

常见错误:

// 错误1: 忘记解锁
func increment(counter *int, mu *sync.Mutex) {
    mu.Lock()
    *counter++
    // 忘记调用mu.Unlock(),导致死锁
}

// 错误2: 重复解锁
func decrement(counter *int, mu *sync.Mutex) {
    mu.Lock()
    *counter--
    mu.Unlock()
    mu.Unlock() // 错误: 再次解锁已经解锁的互斥量会导致panic
}

// 错误3: 复制互斥锁
func copyMutex() {
    var mu sync.Mutex
    mu.Lock()
    
    muCopy := mu // 错误: 复制了互斥锁
    muCopy.Unlock() // 未定义行为
}

1.2 RWMutex(读写锁)

RWMutex允许多个读操作并发执行,但写操作是互斥的。当有写锁时,所有的读操作都会被阻塞。

基本用法:

package main

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

func main() {
    var rwMutex sync.RWMutex
    data := make(map[string]string)
    
    // 写入操作
    go func() {
        for i := 0; i < 10; i++ {
            rwMutex.Lock() // 写锁
            key := fmt.Sprintf("key%d", i)
            data[key] = fmt.Sprintf("value%d", i)
            time.Sleep(100 * time.Millisecond) // 模拟写入耗时
            rwMutex.Unlock()
            time.Sleep(200 * time.Millisecond) // 给读操作时间
        }
    }()
    
    // 多个并发读取操作
    var wg sync.WaitGroup
    for r := 0; r < 5; r++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            for i := 0; i < 10; i++ {
                rwMutex.RLock() // 读锁
                for k, v := range data {
                    fmt.Printf("读取者 %d: %s = %s\n", id, k, v)
                }
                rwMutex.RUnlock()
                time.Sleep(150 * time.Millisecond)
            }
        }(r)
    }
    
    wg.Wait()
}

重要方法:

  • Lock()/Unlock():获取/释放写锁。与Mutex相同,写操作是互斥的
  • RLock()/RUnlock():获取/释放读锁。多个goroutine可以同时持有读锁
  • TryLock()/TryRLock():(Go 1.18+)尝试获取写锁/读锁,非阻塞

使用场景:

当共享资源的读操作远多于写操作时,RWMutex比Mutex更有效。例如:

  • 配置信息:频繁读取,偶尔更新
  • 缓存系统:大量读取,少量写入
  • 统计数据:持续读取,定期更新

性能考虑:

  • 如果读写比例接近1:1,普通的Mutex可能更高效
  • RWMutex有额外的开销来跟踪读锁
  • 写锁会阻塞所有的新读锁请求,可能导致"写饥饿"

二、WaitGroup

WaitGroup用于等待一组goroutine完成执行。它提供了一种简单的方式来协调多个并发操作的完成。

2.1 基本用法

WaitGroup的使用非常直观:

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 工作完成时通知WaitGroup
    
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second) // 模拟工作
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    
    // 启动5个worker
    for i := 1; i <= 5; i++ {
        wg.Add(1) // 增加计数器
        go worker(i, &wg)
    }
    
    // 等待所有worker完成
    wg.Wait()
    fmt.Println("All workers completed")
}

2.2 重要方法

WaitGroup只有三个方法:

  • Add(delta int):增加WaitGroup的计数器值
  • Done():减少WaitGroup的计数器值,相当于Add(-1)
  • Wait():阻塞直到计数器变为0

2.3 最佳实践

  1. 正确设置计数器:在启动goroutine之前调用Add()

    // 正确
    wg.Add(1)
    go func() {
        defer wg.Done()
        // 工作...
    }()
    
    // 错误 - 竞态条件
    go func() {
        wg.Add(1) // 可能在主goroutine调用Wait()后才执行
        defer wg.Done()
        // 工作...
    }()
    
  2. 总是通过指针传递WaitGroup:WaitGroup不应被复制

    // 正确
    func worker(wg *sync.WaitGroup) {
        defer wg.Done()
        // 工作...
    }
    
    // 错误 - 复制了WaitGroup
    func worker(wg sync.WaitGroup) {
        defer wg.Done() // 在复制的WaitGroup上操作,不会影响原始WaitGroup
        // 工作...
    }
    
  3. 使用defer语句确保Done()被调用:

    func worker(wg *sync.WaitGroup) {
        defer wg.Done() // 即使发生panic也能确保Done()被调用
        
        // 工作代码...
        // 如果这里发生panic,Done()仍会被调用
    }
    

2.4 实际应用

WaitGroup常用于以下场景:

  1. 并行任务处理:等待多个并发任务全部完成

    func processItems(items []Item) error {
        var wg sync.WaitGroup
        errCh := make(chan error, len(items)) // 收集错误
        
        for _, item := range items {
            wg.Add(1)
            go func(i Item) {
                defer wg.Done()
                if err := processItem(i); err != nil {
                    errCh <- err
                }
            }(item)
        }
        
        wg.Wait() // 等待所有处理完成
        close(errCh)
        
        // 检查是否有错误
        for err := range errCh {
            if err != nil {
                return err // 返回第一个遇到的错误
            }
        }
        
        return nil
    }
    
  2. 分阶段同步:确保一个阶段完成后再开始下一个阶段

    func processPipeline(data []Data) {
        var wg1, wg2 sync.WaitGroup
        intermediate := make([]Intermediate, len(data))
        result := make([]Result, len(data))
        
        // 阶段1: 处理所有数据
        for i, d := range data {
            wg1.Add(1)
            go func(idx int, input Data) {
                defer wg1.Done()
                intermediate[idx] = stage1(input)
            }(i, d)
        }
        
        wg1.Wait() // 等待阶段1完成
        
        // 阶段2: 处理中间结果
        for i, d := range intermediate {
            wg2.Add(1)
            go func(idx int, input Intermediate) {
                defer wg2.Done()
                result[idx] = stage2(input)
            }(i, d)
        }
        
        wg2.Wait() // 等待阶段2完成
        
        // 所有处理完成,使用result
    }
    

三、Once

Once用于确保某个函数只被执行一次,即使在多个goroutine中并发调用也是如此。它通常用于单例模式、延迟初始化或执行只需要一次的设置操作。

3.1 基本用法

package main

import (
    "fmt"
    "sync"
)

func main() {
    var once sync.Once
    done := make(chan bool)
    
    // 尝试在多个goroutine中执行初始化
    for i := 0; i < 10; i++ {
        go func(id int) {
            fmt.Printf("Goroutine %d trying to initialize\n", id)
            once.Do(func() {
                fmt.Printf("Initialization done by goroutine %d\n", id)
            })
            done <- true
        }(i)
    }
    
    // 等待所有goroutine完成
    for i := 0; i < 10; i++ {
        <-done
    }
}

在这个例子中,尽管有10个goroutine尝试执行初始化函数,但once.Do()确保只有一个goroutine能够执行它。

3.2 特性与限制

  • Once实例只能用于执行一个指定的函数一次
  • 如果需要确保多个不同的函数都只执行一次,需要为每个函数创建单独的Once实例
  • 一旦Do()方法完成调用,对同一个Once实例的后续Do()调用将不会执行提供的函数
  • 如果在Do()调用的函数中发生panic,Once将认为操作已完成
  • Once没有重置机制,一旦使用就不能重新使用

3.3 常见应用场景

  1. 单例模式:确保只创建一个实例
type Singleton struct {
    // 单例结构字段
}

var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
        // 初始化代码...
    })
    return instance
}
  1. 延迟初始化:首次需要时才执行耗时初始化
type Resource struct {
    data []byte
}

var (
    resource *Resource
    initOnce sync.Once
)

func GetResource() *Resource {
    initOnce.Do(func() {
        fmt.Println("Loading resource...")
        // 耗时的资源加载
        resource = &Resource{
            data: loadLargeFile(),
        }
    })
    return resource
}
  1. 只执行一次的操作:如配置加载、日志初始化等
var loggerInit sync.Once

func initLogger() {
    loggerInit.Do(func() {
        // 设置日志系统
        fmt.Println("Initializing logging system...")
        // 打开日志文件
        // 设置日志级别
        // 配置日志格式
    })
}

四、Cond(条件变量)

Cond实现了一个条件变量,它是等待或宣布事件发生的goroutine的会合点。它允许goroutine等待某个条件成立,然后在条件成立时得到通知。

4.1 基本概念

条件变量总是与互斥锁关联,并通过锁来保护条件的检查和更新。Cond提供了三个主要方法:

  • Wait():释放关联的锁,等待通知,被唤醒后重新获取锁
  • Signal():唤醒一个等待的goroutine
  • Broadcast():唤醒所有等待的goroutine

4.2 基本用法

package main

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

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    
    ready := false
    
    // 消费者goroutine
    for i := 0; i < 3; i++ {
        go func(id int) {
            mu.Lock()
            defer mu.Unlock()
            
            fmt.Printf("Consumer %d is waiting...\n", id)
            for !ready { // 使用循环检查条件
                cond.Wait() // 等待信号
            }
            fmt.Printf("Consumer %d received signal\n", id)
        }(i)
    }
    
    // 给消费者一点时间启动
    time.Sleep(time.Second)
    
    // 生产者goroutine
    go func() {
        mu.Lock()
        defer mu.Unlock()
        
        fmt.Println("Producer is ready")
        ready = true
        
        fmt.Println("Producer broadcasts signal")
        cond.Broadcast() // 通知所有等待的goroutine
    }()
    
    // 等待足够时间让所有goroutine完成
    time.Sleep(3 * time.Second)
}

在这个例子中:

  1. 多个消费者goroutine等待ready条件变为true
  2. 每个消费者获取锁,检查条件,如果不满足则调用Wait()等待
  3. 生产者将条件设置为true,然后调用Broadcast()通知所有等待的消费者
  4. 所有消费者被唤醒,重新获取锁,再次检查条件,然后继续执行

4.3 使用模式

  1. 始终在循环中使用Wait():这是避免虚假唤醒的重要模式
mu.Lock()
for !condition {
    cond.Wait() // 可能被虚假唤醒,所以需要循环检查
}
// 条件为真,处理...
mu.Unlock()
  1. Signal vs Broadcast:选择适当的通知机制
  • Signal():唤醒单个等待者,适用于任务队列等场景,只需一个工作者处理
  • Broadcast():唤醒所有等待者,适用于状态变化等场景,需要所有人都知道
  1. 确保锁的正确使用:在调用Signal或Broadcast时通常需要持有锁
mu.Lock()
// 改变条件
condition = true
cond.Signal() // 或 cond.Broadcast()
mu.Unlock()

4.4 实际应用

  1. 有界队列:生产者-消费者场景
type BoundedQueue struct {
    queue []interface{}
    size  int
    mu    sync.Mutex
    notEmpty *sync.Cond
    notFull  *sync.Cond
}

func NewBoundedQueue(size int) *BoundedQueue {
    bq := &BoundedQueue{
        queue: make([]interface{}, 0, size),
        size:  size,
    }
    bq.notEmpty = sync.NewCond(&bq.mu)
    bq.notFull = sync.NewCond(&bq.mu)
    return bq
}

func (bq *BoundedQueue) Put(v interface{}) {
    bq.mu.Lock()
    defer bq.mu.Unlock()
    
    // 队列已满,等待有空间
    for len(bq.queue) == bq.size {
        bq.notFull.Wait()
    }
    
    // 添加元素
    bq.queue = append(bq.queue, v)
    
    // 通知可能等待的消费者
    bq.notEmpty.Signal()
}

func (bq *BoundedQueue) Get() interface{} {
    bq.mu.Lock()
    defer bq.mu.Unlock()
    
    // 队列为空,等待有元素
    for len(bq.queue) == 0 {
        bq.notEmpty.Wait()
    }
    
    // 获取元素
    v := bq.queue[0]
    bq.queue = bq.queue[1:]
    
    // 通知可能等待的生产者
    bq.notFull.Signal()
    
    return v
}
  1. 资源池:等待可用资源
type Pool struct {
    mu      sync.Mutex
    cond    *sync.Cond
    closed  bool
    resources []interface{}
}

func NewPool(resources []interface{}) *Pool {
    p := &Pool{
        resources: resources,
    }
    p.cond = sync.NewCond(&p.mu)
    return p
}

func (p *Pool) Acquire() (interface{}, error) {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    for len(p.resources) == 0 && !p.closed {
        p.cond.Wait()
    }
    
    if p.closed {
        return nil, errors.New("pool is closed")
    }
    
    resource := p.resources[0]
    p.resources = p.resources[1:]
    return resource, nil
}

func (p *Pool) Release(resource interface{}) {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    if p.closed {
        return
    }
    
    p.resources = append(p.resources, resource)
    p.cond.Signal()
}

func (p *Pool) Close() {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    p.closed = true
    p.cond.Broadcast() // 通知所有等待的goroutine
}

五、Map

sync.Map是Go 1.9引入的一种并发安全的映射实现,专为高并发场景设计,不需要额外的锁就可以安全地被多个goroutine同时访问。

5.1 与传统map+mutex的区别

传统的map不是并发安全的,需要使用mutex保护:

var (
    mu       sync.Mutex
    counters = make(map[string]int)
)

func Increment(key string) {
    mu.Lock()
    defer mu.Unlock()
    counters[key]++
}

func Get(key string) int {
    mu.Lock()
    defer mu.Unlock()
    return counters[key]
}

而sync.Map则是专门为并发设计的:

var counters sync.Map

func Increment(key string) {
    value, _ := counters.LoadOrStore(key, 0)
    counters.Store(key, value.(int) + 1)
}

func Get(key string) int {
    value, ok := counters.Load(key)
    if !ok {
        return 0
    }
    return value.(int)
}

5.2 主要方法

sync.Map提供以下关键方法:

  • Store(key, value):存储键值对
  • Load(key) (value, ok):加载给定键的值
  • Delete(key):删除给定键
  • LoadOrStore(key, value) (actual, loaded):如果键存在,返回存在的值;否则,存储提供的值
  • Range(f func(key, value) bool):遍历映射中的所有键值对
  • LoadAndDelete(key) (value, loaded):加载并删除键(Go 1.15+)

5.3 适用场景

sync.Map适用于以下场景:

  1. 读多写少:当读操作远多于写操作时性能最佳
  2. 键的生命周期:当键被写入一次后很长时间不会被删除
  3. 使用情景:常用于缓存、配置存储等场景

5.4 使用示例

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    
    // 存储键值对
    m.Store("name", "John")
    m.Store("age", 30)
    m.Store("city", "New York")
    
    // 读取
    name, ok := m.Load("name")
    if ok {
        fmt.Println("Name:", name)
    }
    
    // 加载或存储
    email, loaded := m.LoadOrStore("email", "john@example.com")
    if !loaded {
        fmt.Println("Email set to:", email)
    }
    
    // 遍历所有键值对
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true // 返回false会停止遍历
    })
    
    // 加载并删除
    if value, loaded := m.LoadAndDelete("age"); loaded {
        fmt.Printf("Deleted age: %v\n", value)
    }
    
    // 删除
    m.Delete("city")
    
    // 验证删除
    _, ok = m.Load("city")
    fmt.Println("City exists:", ok)
}

六、Pool

sync.Pool提供了一个可以重复使用临时对象的池,有助于减少垃圾回收压力,特别是在高并发环境下。

6.1 基本概念

Pool的主要特性:

  • 线程安全:可以在多个goroutine之间共享
  • 无容量限制:可以存储任意数量的对象
  • 临时存储:池中的对象可能在任何时候被自动移除(特别是在GC发生时)
  • 高效复用:避免频繁分配和回收对象,减轻GC压力

6.2 核心方法

Pool提供了两个主要方法:

  • Get() interface{}:从池中获取对象。如果池为空,则调用New函数创建一个新对象
  • Put(x interface{}):将对象放回池中以供后续重用

6.3 使用示例

package main

import (
    "bytes"
    "fmt"
    "sync"
)

func main() {
    // 创建一个池,用于复用bytes.Buffer
    var bufferPool = sync.Pool{
        New: func() interface{} {
            fmt.Println("Creating a new buffer")
            return new(bytes.Buffer)
        },
    }
    
    // 获取一个Buffer
    buffer1 := bufferPool.Get().(*bytes.Buffer)
    buffer1.WriteString("Hello")
    fmt.Println("Buffer1:", buffer1.String())
    
    // 清空并放回池中
    buffer1.Reset()
    bufferPool.Put(buffer1)
    
    // 获取一个Buffer(可能是刚才放回的那个)
    buffer2 := bufferPool.Get().(*bytes.Buffer)
    buffer2.WriteString("World")
    fmt.Println("Buffer2:", buffer2.String())
    
    // 清空并放回池中
    buffer2.Reset()
    bufferPool.Put(buffer2)
    
    // 同时获取多个Buffer
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            // 获取Buffer
            buf := bufferPool.Get().(*bytes.Buffer)
            
            // 使用Buffer
            buf.WriteString(fmt.Sprintf("Goroutine %d", id))
            fmt.Printf("Goroutine %d: %s\n", id, buf.String())
            
            // 清空并放回
            buf.Reset()
            bufferPool.Put(buf)
        }(i)
    }
    
    wg.Wait()
}

6.4 最佳实践

  1. 清除对象状态:在将对象放回池之前,确保清除其状态(例如使用Reset()方法)
buffer := bufferPool.Get().(*bytes.Buffer)
// 使用buffer...
buffer.Reset() // 清除内容
bufferPool.Put(buffer)
  1. 避免存储带有复杂状态的对象:池中的对象可能被GC回收,不适合保存带有状态的资源

  2. 类型断言:在获取对象时使用正确的类型断言

obj := pool.Get()
buf, ok := obj.(*bytes.Buffer)
if !ok {
    // 处理类型不匹配
}
  1. 适合的使用场景:
    • 高频创建的临时对象
    • 大小相似的对象
    • 创建成本较高的对象
    • 生命周期短的对象

6.5 实际应用

  1. JSON编码/解码:复用编码器/解码器
var encoderPool = sync.Pool{
    New: func() interface{} {
        return json.NewEncoder(io.Discard)
    },
}

func encodeJSON(w io.Writer, v interface{}) error {
    encoder := encoderPool.Get().(*json.Encoder)
    encoder.SetWriter(w)
    err := encoder.Encode(v)
    encoder.SetWriter(io.Discard) // 重置
    encoderPool.Put(encoder)
    return err
}
  1. HTTP请求处理:复用请求/响应缓冲区
var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf)
    
    n, err := r.Body.Read(buf)
    if err != nil && err != io.EOF {
        http.Error(w, "Error reading body", http.StatusBadRequest)
        return
    }
    
    // 处理请求...
}

六、sync包与channel的选择

在Go并发编程中,我们经常面临选择使用sync包中的同步原语还是channel的问题。这两种方法各有优缺点,选择哪种取决于具体场景。

6.1 选择依据

  1. 数据流向:

    • 如果需要传递数据,使用channel
    • 如果只需要同步或保护共享资源,使用sync原语
  2. 耦合度:

    • channel将生产者和消费者解耦
    • sync原语使goroutine通过共享内存直接耦合
  3. 性能考虑:

    • 对于简单的同步操作,sync原语通常更高效
    • channel有额外的开销,但提供更强的抽象
  4. 代码清晰度:

    • 哪种方式让你的代码更易理解?
    • 哪种方式更符合你的程序逻辑?
  5. 比较:

特性ChannelSync原语
数据传递是否
共享内存保护间接直接
耦合度低高
适用场景消息传递、事件通知共享数据访问控制
复杂性较高较低
调试难度较难相对简单

👨‍💻 关于作者与Gopher部落

"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。

🌟 为什么关注我们?

  1. 系统化学习路径:本系列56篇文章循序渐进,带你完整掌握Go开发
  2. 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
  3. 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
  4. 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长

📱 关注方式

  1. 微信公众号:搜索 “Gopher部落” 或 “GopherTribe”
  2. 优快云专栏:点击页面右上角"关注"按钮

💡 读者福利

关注公众号回复 “Go学习” 即可获取:

  • 完整Go学习路线图
  • Go面试题大全PDF
  • Go项目实战源码
  • 定制学习计划指导

期待与您在Go语言的学习旅程中共同成长!

Go语言学习之sync包(临时对象池Pool、互斥锁Mutex、等待Cond)(the way to go) 生命不止,继续 go go go!!!golang的特点就是语言层面支持并发,并且实现并发非常简单,只需在需要并发的函数前面添加关键字go。 但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包来解决相关的问题,当然还有其他的方式比如channel,原子操作atomic等等,这里先介绍sync包的用法.这里,跟大家一些学习golang的标准库,sync 阅读详情

相关推荐

VL53L0X测距不准?手把手教你做RefSPAD校准与环境补偿(附Arduino/STM32代码)

本文详细解析了VL53L0X激光测距传感器精度不足的原因,并提供了RefSPAD校准与环境补偿的完整解决方案。通过Arduino和STM32代码示例,手把手教你实现毫米级测距稳定性,适用于机器人避障、工业检测等场景。

weixin_28744423的博客 256

Go语言的并发模型与sync包

1.背景介绍 Go语言是一种现代编程语言,由Google开发,于2009年首次公开。Go语言的设计目标是简单、高效、可扩展和易于使用。Go语言的并发模型是其核心特性之一,它使得Go语言能够轻松地处理大量并发任务。 Go语言的并发模型基于Goroutine和Channels等原语,Goroutine是Go语言的轻量级线程,Channels是Go语言的通信机制。sync包是Go语言标准库中的一个包...

AI天才研究院 1099

无需Root!用VirtualXposed+JustTrustMe在安卓高版本上抓包拼多多API数据

本文详细介绍了在安卓高版本设备上无需Root权限即可抓取拼多多API数据的完整方案,通过VirtualXposed与JustTrustMe组合技术,有效绕过SSL Pinning限制。文章涵盖工具准备、环境配置、应用处理及实战分析,特别针对PDD应用的混合通信策略提供了专业解决方案,适合开发者进行竞品分析和数据采集。

weixin_28678151的博客 453

Go语言Sync包

Go语言Sync包

YIYIYI 1026

Cesium 实战 04 - 标绘功能系列之画点(draw point)

Cesium 实战 - 标绘功能之画点(draw point)。

非科班Java出身GISer的博客 1083

Jetson系列目标检测及推流

使用硬件编码,gstreamer推rtsp流至平台

xiao__run的博客 2710

Go语言sync包的应用详解

在并发编程中同步原语也就是我们通常说的锁的主要作用是保证多个线程或者 goroutine在访问同一片内存时不会出现混乱的问题。Go语言的sync包提供了常见的并发编程同步原语,上一期转载的文章《Golang 并发编程之同步原语》中也详述了 Mutex、RWMutex、WaitGroup、Once 和 Cond 这些同步原语的实现原...

kevin_tech的博客,微信搜「网管叨bi叨」 6057

第一节:Vben Admin介绍和初次运行

本文是【实战Vben Admin + Python】的第一节,本文内容主要是介绍Vben Admin框架和我们后端使用的技术,开发环境和工具的准备本节文章的目的是Vben Admin的简单介绍,通过配置可以让项目前端运行起来,由于项目默认开启了mock,展示的都是静态模拟数据,我们在此基础上,下一节将研究分析下Vben Admin的目录。为我们后端搭建准备。

cuiwin的专栏 7410

31 | sync.WaitGroup和sync.Once

sync代码包的WaitGroup类型和Once类型都是非常易用的同步工具。它们都是开箱即用和并发安全的。利用WaitGroup值,我们可以很方便地实现一对多的 goroutine 协作流程,即:一个分发子任务的 goroutine,和多个执行子任务的 goroutine,共同来完成一个较大的任务。在使用WaitGroup值的时候,我们一定要注意,千万不要让其中的计数器的值小于0,否则就会引发 panic。另外,

qq_37756660的博客 1027

【雕爷学编程】Arduino智慧农业之温湿度数据上传到云平台

它们可能有错误、不适用或者无法编译。在实施过程中,需要注意传感器的选择和校准、通信和网络稳定性、数据隐私和安全保护、电源管理以及数据分析和决策支持等方面的事项。数据分析和决策支持:上传到云平台的温湿度数据可以提供丰富的信息,但需要合适的数据分析和决策支持工具来帮助农业业主理解和利用这些数据。您可以根据自己的需求和喜好选择适合您的云平台和库,并根据您的具体硬件配置进行适当的修改和调整。将温湿度数据上传到云平台,农业业主可以实时监测粮库内的温湿度情况,及时采取通风、除湿等措施,确保粮食质量和长期储存的稳定性。

雕爷学编程 1275

openclaw通俗入门系列4——智能体工作区

工作区 = 智能体的家,存放它的性格、记忆、技能。默认不是牢笼(可以用沙箱隔离)。建议放到私有 Git 备份,但绝对不要提交密钥!

m0_60605453的博客 462

GO语言:sync包——互斥锁_go语言中sync(1)

mutexLocked = 1

2401_86449896的博客 423

15 媒体存储的封装3

CMediaDisk类真正实现了录像文件的存储、查询和点播。存储录像文件时,需要满足以下的几个基本需求:1、异常断电不能丢失数据。2、录像既支持普通文件的查询方式,也支持时间轴的查询方式。3、修改设备时间时,不能够丢失已有录像和数据。向后修改时间时,如果当前时间已有录像,则不覆盖老数据;直到有未录像的时间段时,才再次写入新数据。4、支持删除媒体文件,也支持按时间段删除。5、支持标记录像文件、抓拍文件,也支持按时间段标记。6、支持自动覆盖最老的文件。7、支持多个设备、多路视频的录像存储。

hope_wisdom的技术博客 1122

Go36-31-sync.WaitGroup和sync.Once

sync.WaitGroup 之前在协调多个goroutine的时候,使用了通道。基本都是按下面这样来使用的: package main import "fmt" func main() { done := make(chan struct{}) count := 5 for i := 0; i < count; i++ { go func(i ...

weixin_33850015的博客 263

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Go: 理解 Sync.Pool 的设计

sync 包提供了一个强大且可复用的实例池,以减少 GC 压力。在使用该包之前,我们需要在使用池之前和之后对应用程序进行基准测试。这非常重要,因为如果不了解它内部的工作原理,可能会影响性能。

ldxxxxll的博客 1079

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

灰子的Go笔记:sync.Map

背景介绍:在golang中map不是并发安全的,所有才有了sync.Map的实现,尽管sync.Map的引入确实从性能上面解决了map的并发安全问题,不过sync.Map却没有实现len...

zhghost的专栏 1403

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

2024年Go最全GO语言:sync包——读写锁_go 读写锁,Golang程序员怎么优雅迈过30K+这道坎

在读写锁管辖的范围内,它允许任意个读操作的同时进行。写锁,如果在添加写锁之前已经有其他的读锁和写锁,则Lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的Lock调用会从获得的锁中排除新的读取锁,即写锁权限高于读锁,有写锁时优先进行写锁定。读写锁的写锁只能锁定一次,解锁前不能多次锁定,读锁可以多次,但读解锁次数最多只能比读锁次数多一次,一般情况下我们不建议读解锁次数多余读锁次数。读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于“读多写少”的场景。

qq_38197819的博客 908

go每日新闻--2020-10-31

go 中文网每日资讯--2020-10-31 一、Go 语言中文网 知乎社区核心业务 Golang 化实践 Kubernetes API 编程利器:Operator 和 Operator Framework 你真的了解 sync.Once 吗 二、亚军进化史 Go 技术日报(2020-10-30) 三、 Golang 梦工厂 machinery 中文文档( 值得收藏 ) machinery 入门看这一篇(异步任务队列) 四、 脑子进煎鱼了 为什么 Go 占用那么多的虚拟内存? 五、 网管叨 bi

韩亚军的博客 607

golang sync.Map和map+mutex性能比较

目录 测试环境 测试代码 运行指令 测试结果 结论 测试环境 goos: linux goarch: amd64 Run on (8 X 2394.37 MHz CPU s) CPU Caches: L1 Data 32 KiB (x8) L1 Instruction 32 KiB (x8) L2 Unified 4096 KiB (x8) L3 Unified 16384 KiB (x1) 测试代码 文件:src/benchmark/syncmap_test.go...

wyg_031113的专栏 7711

go的并发编程

go的学习--2go的并发goroutinechannel通道channel的三种操作无缓冲通道(阻塞通道)有缓冲的通道单向通道定时器select多路复用syncsync.WaitGroupsync.Oncesync.Map并发安全和锁互斥锁读写互斥锁GMP原理与调度线程与协程go的goroutinegoroutine的GMP模型的设计思想 go的并发 goroutine 奉行通过通信来共享内存,而不是共享内存来通信。 goroutine 实现goroutine的方式就是给函数前加go关键字调用 pac

MarryOnlyQueen的博客 652

Go -- 并发编程

math/rand包 crypto/rand包生随机数1、math/rand 包1.1、主要方法1.2、示例1.3、rand.Seed(time.Now().UnixNano())作用:2、crypto/rand 包2.1、主要方法2.2、代码示例Go语言中的并发编程1、并发与并行2、goroutine2.1、使用goroutine2.2、启动单个goroutine2.3、sync.WaitGroup 同步gorouting3、goroutine与线程3.1、可增长的栈 math/rand包 crypto.

CN_LiTianpeng的博客 919

Go语言并发编程高级特性

在前文中,我们已经介绍了Go语言的错误处理机制和反射特性。在本文中,我们将深入探讨Go语言的并发编程高级特性。Go语言以其简洁而强大的并发编程模型而闻名,它通过goroutine和channel提供了一种轻量级的并发编程方式,使开发者能够轻松地编写高性能的并发程序。

欢迎来到智能安全前线!这里是AI与信息安全交汇之地。我们以代码为武器,深入大模型攻防实战,破解系统漏洞。追踪OpenAI、Meta前沿动态,用红蓝对抗思维,锻造智能时代的坚盾与利矛。 809

Go语言类库-sync

通道并不是Go支持的唯一的一种并发同步技术。而且对于一些特定的情形,通道并不是最有效和可读性最高的同步技术。本文下面将介绍sync标准库包中提供的各种并发同步技术。相对于通道,这些技术对于某些情形更加适用。sync标准库包提供了一些用于实现并发同步的类型。这些类型适用于各种不同的内存顺序需求。对于这些特定的需求,这些类型使用起来比通道效率更高,代码实现更简洁。

jeremy_ke的博客 1170

Go语言学习路线图:从零基础到实战高手的完整指南

你是否正在寻找一条清晰、高效的Go语言学习路径?Go语言凭借其简洁的语法、强大的并发支持和出色的性能,已经成为云原生时代最受欢迎的后端开发语言之一。本文将为初学者和中级开发者提供一个**系统化、实践导向**的Go语言学习路线图,帮助你避免学习误区,快速掌握Go语言核心技能。 在开始之前,建议先获取完整的Go知识图谱资源: ```bash git clone https://gitcode.co

gitblog_01151的博客 381

Go语言并发编程之goroutine

这里写目录标题exm1exm2exm3exm3.5exm4exm5exm5.5exm6 并发编程在当前软件领域是一个非常重要的概念,随着CPU等硬件的发展,我们无一例外的想让我们的程序运行的快一点、再快一点。Go语言在语言层面天生支持并发,充分利用现代CPU的多核优势,这也是Go语言能够大范围流行的一个很重要的原因。 exm1 package main import "fmt" // goroutine demo func hello() { fmt.Println("hello JIeJaitt"

技术宅拯救世界 446

go Sync.Pool

sync 包提供了一个强大且可复用的实例池,以减少 GC 压力。在使用该包之前,我们需要在使用池之前和之后对应用程序进行基准测试。这非常重要,因为如果不了解它内部的工作原理,可能会影响性能。

2401_82869454的博客 427
上一篇: 【Go语言学习系列32】并发编程(五):并发模式
下一篇: 【Go语言学习系列33】并发编程(六):原子操作与内存模型
Gopher部落
博客等级 码龄2年 784粉丝 92原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值