首页 > 其他分享 >Go 并发之WaitGroup,并发锁,Context

Go 并发之WaitGroup,并发锁,Context

时间:2024-12-29 08:59:33浏览次数:6  
标签:func Context 互斥 fmt sync 并发 context Go

目录

1 Go并发

1.1 WaitGroup

sync.WaitGroup 是 Go 标准库提供的一种同步原语,常用于等待一组 Goroutine 执行完成。它提供了一种简单的方式来管理多个 Goroutine 的并发执行,确保主程序在所有 Goroutine 完成后再继续。

在多线程(Goroutine)编程中,主线程(main 函数)需要等待子 Goroutine 完成工作后才能退出。如果直接退出,子 Goroutine 的执行会被中断。这时就需要 WaitGroup 来协调这些 Goroutine 的执行。

注意

  • 避免重复使用 WaitGroup
    一个 WaitGroup 实例不应该在计数器值已经变为 0 后再次调用 Add() 或 Done()。
  • WaitGroup 是一个值类型
    必须传递指针给 Goroutine,否则每个 Goroutine 会得到一个拷贝,无法正确修改计数器。
  • 计数器不能为负数:
    如果 Done() 调用次数多于 Add(),会引发运行时错误。

WaitGroup 的核心方法有以下几个:

  • Add(delta int)
    增加等待计数器的值,delta 是变化值。
    每次启动一个 Goroutine 时,调用 Add(1) 来增加计数。
    可以使用负数来减少计数,但通常减少计数用 Done
  • Done()
    将等待计数器减一,表示某个 Goroutine 完成了工作。
    每个 Goroutine 执行完毕后,都应该调用 Done()
  • Wait()
    阻塞主线程,直到等待计数器变为 0。
    主线程调用 Wait(),表示等待所有计数器的 Goroutine 执行完毕。

同步多个 Goroutine:

package main

import (
        "fmt"
        "sync"
)

func worker(id int, wg *sync.WaitGroup) {
        defer wg.Done() // Goroutine 完成时调用 Done()
        fmt.Printf("Worker %d started\n", id)
        fmt.Printf("Worker %d finished\n", id)
}

func main() {
        var wg sync.WaitGroup

        for i := 1; i <= 3; i++ {
                wg.Add(1) // 增加计数器
                go worker(i, &wg)
        }

        wg.Wait() // 等待所有 Goroutine 完成
        fmt.Println("All workers done")
}

结果:
Worker 1 started
Worker 1 finished
Worker 2 started
Worker 2 finished
Worker 3 started
Worker 3 finished
All workers done

1.2 并发锁

在go 代码中可能会存着多个 goroutine 同时操作一个资源(临界区),这种情况会发生竞态。

1.2.1 互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证只有一个 goroutine 访问共享资源。例如:网上购票。
互斥锁作用:同一时间有且仅有一个 goroutine 进入临界区,其他 goroutine 则在等待锁,等互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 都在等待一个锁时,唤醒机制是随机的。

示例:资源竞争的情况

package main

import (
	"fmt"
	"sync"
)
// 全局变量
var x int64
// 计时器
var sw sync.WaitGroup
// 累加函数
func add() {   
	defer sw.Done()
	for i:=0;i<5000;i++ {        
		x++	// 不同 goroutine 竞争 x 资源
	}
}

func main() {        
	sw.Add(2)
	go add()
	go add()
	sw.Wait()
	fmt.Println(x)
}
/*
6115/10000/7436......
会有各种情况
 */

示例:加互斥锁

package main
import (
    "fmt"
    "sync"
)
// 全局变量
var x int64
// 计时器
var sw sync.WaitGroup
// 互斥锁
var lock sync.Mutex
// 累加函数
func add() {       
    for i:=0;i<5000;i++ {        
        lock.Lock()	// 互斥锁把门锁上
        x++
        lock.Unlock()	// 互斥锁把门解锁
    }
    sw.Done()
}

func main() {      
    sw.Add(2)
    go add()
    go add()
    sw.Wait()
    fmt.Println(x)
}

/*
10000
*/

使用信道 解决竞争

package main  
import (  
    "fmt"
    "sync"
    )
var x  = 0  
func increment(wg *sync.WaitGroup, ch chan bool) {          
    ch <- true
    x = x + 1
    <- ch
    wg.Done()   
}
func main() {         
    var w sync.WaitGroup
    ch := make(chan bool, 1)
    for i := 0; i < 1000; i++ {        
        w.Add(1)        
        go increment(&w, ch)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

在上述程序中,我们创建了容量为 1 的缓冲信道,并将它传入 increment 协程。该缓冲信道用于保证只有一个协程访问增加 x 的临界区。原理是:方法是在 x 增加之前,传入 true 给缓冲信道。由于缓冲信道的容量为 1,其他协程试图往通道写数据时被阻塞,当 x 增加后,信道的值被读取。实际上这就保证了只允许一个协程访问临界区。

1.2.2 读写互斥锁

互斥锁是完全互斥的,但是很多的实际场景,读多写少,当并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景使用读写锁是更好的一种选择,可以提高性能。
读写锁分两种:读锁写锁。当一个 goroutine 获取读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。

//实验不加锁、使用互斥锁、使用读写互斥锁的区别
//不加锁最快,使用互斥锁最慢
package main

import (
    "fmt"
    "sync"
    "time"
)
var x int64
var sw sync.WaitGroup
// 使用互斥锁
var lock sync.Mutex
// 使用读写互斥锁
var rwlock sync.RWMutex

func read() {        
    defer sw.Done()
	rwlock.RLock()	// 读互斥加锁
	// lock.Lock()	// 互斥加锁
	time.Sleep(time.Millisecond * 1)
	// lock.Unlock()	// 互斥解锁
	rwlock.RUnlock()	// 读互斥解锁
}

func write() {   
    defer sw.Done()
    rwlock.Lock()	// 写互斥加锁
    // lock.Lock()	// 互斥锁加锁
    x++
    time.Sleep(time.Millisecond * 5)
    // lock.Unlock()	// 互斥锁解锁
    rwlock.Unlock()	// 写锁解锁
}

func main() {        
    start := time.Now()
    // 写入 10 次
    for i:=0;i<10;i++ {        
        sw.Add(1)
        go write()
    }
    // 读取 1000 次
    for i:=0;i<1000;i++ {        
        sw.Add(1)
        go read()
    }
    sw.Wait()
    end := time.Now()
    fmt.Printf("用时:%v.\n",end.Sub(start))
}
/*
用时:58.3229ms.
*/

1.2.3 sync.Once

延迟一个开销很大的初始化操作,到真正用到它的时候再执行,例如:定义了一个 init 初始化函数,程序启动的时候会被自动加载,无论是否用到都会加载,这样程序就会增加程序的启动延时。
sync.Once 是 Go 中提供的一个同步原语,确保某个操作只执行一次。无论这个操作被调用多少次,sync.Once 只会保证它的 Do 方法中的函数只执行一次
关键点:

  • 并发安全sync.Once 是并发安全的,它可以被多个 goroutine 安全地调用。在多个 goroutine 同时调用 Do 方法时,只有第一次调用会执行传入的函数,后续调用会被忽略。
  • 只执行一次:无论调用多少次,传入的函数只会执行一次。

sync.Once 只有一个 Do 方法,示例:

// 定义 sync.Once
var onlyOne sync.Once
func initDatabase() {
    fmt.Println("Database initialized!")
}
// 被多个 goroutine 调用时不是并发安全的
func card(name string) {        
    if cards == nil {        
        onlyOne.Do(initDatabase)
    }
}

示例:使用 sync.Once 调用带参函数

package main

import (
    "fmt"
    "sync"
)

// 定义 sync.Once
var onlyOne sync.Once
func test(x int) {   
    fmt.Println(x)
}
// 闭包
func closer(x int) func() {        
    return func() {        
        test(x)
    }
}
func main() {        
    t := closer(10)
    onlyOne.Do(t)
}

/*
10
*/

1.2.4 sync.Map

sync.Map 是 Go 语言标准库中提供的一个并发安全的映射(字典)实现,它用于在并发环境中处理共享数据。与普通的 map 不同,sync.Map 为多 goroutine 提供了内建的同步机制,避免了传统的 map 在并发读写时可能引发的数据竞态问题。
sync.Map 是专为并发设计的,它内部使用了一些优化,使得它在高并发场景下的表现优于使用 sync.Mutexsync.RWMutex 手动加锁的方案。

sync.Map 的特点:

  • 并发安全sync.Map 的所有操作(读、写、删除)在多个 goroutine 中是并发安全的,避免了使用普通的 map 时可能遇到的竞态条件。
  • 高效:在读多写少的场景下,sync.Map 的性能表现较好。它使用了内部分段的锁和无锁的操作,以优化性能。
  • 不支持直接迭代:不像传统的 map,sync.Map 不支持直接通过 range 来遍历,因此没有原生的顺序性。
  • 支持原子操作:sync.Map 提供了原子级别的操作,使得并发访问数据时不会发生冲突。

常用方法:

  • Store(key, value):
    用于向 sync.Map 中存储键值对。如果键已存在,则覆盖原有的值。
  • Load(key):
    用于获取 sync.Map 中指定键的值。返回两个值,第二个值是一个布尔值,表示键是否存在。
  • LoadOrStore(key, value):
    如果 key 已存在,返回现有值和 true,否则存储新的值并返回 false。
  • Delete(key):
    用于删除 sync.Map 中指定的键值对。
  • Range(f func(key, value interface{}) bool):
    遍历 sync.Map 中所有的键值对。与普通的 map 不同,sync.Map 不支持 range 遍历,需要通过 Range 方法进行遍历。
    Range 接受一个回调函数 f,这个函数会被每个键值对调用。如果回调函数返回 false,则遍历会停止。
m.Store("key1", "value1")
m.Store("key2", "value2")
m.Range(func(key, value interface{}) bool {
    fmt.Println(key, value)
    return true
})

示例

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    // Store一些键值对
    m.Store("name", "John")
    m.Store("age", 30)
    // Load读取值
    if value, ok := m.Load("name"); ok {
        fmt.Println("name:", value)
    }
    // LoadOrStore 如果存在则返回现有值,不存在则存储新的值
    value, loaded := m.LoadOrStore("age", 40)
    fmt.Println("age:", value, "loaded:", loaded)
    // Delete 删除键值对
    m.Delete("name")
    // Range遍历
    m.Range(func(key, value interface{}) bool {
        fmt.Println(key, value)
        return true
    })
}

1.3 Context

1.3.1 简介

context 是一个用于跨 API 边界传递元数据、取消信号、超时控制以及其他请求范围内的信息的包。它常用于处理并发编程中的超时、取消、以及请求范围内的数据传递。

那么为什么需要 context?
在并发编程中,当多个协程运行时,可能会遇到以下问题:

  • 请求取消:在某些情况下,可能需要在某个请求被取消时终止正在进行的操作。
  • 超时控制:有时需要设置操作的超时时间,超过时间就终止操作。
  • 跨 API 边界传递元数据:需要在不同的函数、服务或系统之间传递共享的状态信息。

为了处理这些问题,context 被引入 Go 语言。通过 context,你可以更好地管理和控制协程的生命周期,特别是在复杂的并发程序中。

context 的常见用途:

  • 取消操作:context 可以传递取消信号,能够在不再需要某个操作时立即停止它。
  • 设置超时:通过 context.WithTimeout()context.WithDeadline() 设置操作的超时限制。
  • 传递共享数据:context 可以携带元数据,如请求 ID 或认证信息,供多个函数调用共享。

1.3.2 主要功能

context接口

type Context interface {        
	// 获取设置的截止时间:
	// 第一个返回值是截止时间,到了这个时间点,Context 会自动发起取消请求;
	//第二个返回值 ok==false 时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消
      Deadline() (deadline time.Time, ok bool)
      // 该方法返回一个只读的 chan,类型为 struct{},如果该方法返回的 chan 可以读取,则意味着parent context已经发起了取消请求,我们通过 Done 方法收到这个信号后,就应该做清理操作,然后退出 goroutine,释放资源。
      Done() <- chan struct {   }    
      // 返回取消的错误原因,因为什么 Context 被取消。
      Err() error 
      // 获取该 Context 上绑定的值,是一个键值对,所以要通过一个 Key 才可以获取对应的值,这个值一般是线程安全的。
      Value(key interface{}) interface{ }
}

context 包提供了以下几种常见功能:

  • 创建根 context(背景 Context)
    ctx := context.Background()
    context.Background() 是一个返回空 context 的函数,通常用作根 context,在应用程序的入口点被调用,用于所有后续的 context 创建和操作。
  • 创建带取消信号的 Context
    ctx, cancel := context.WithCancel(parentContext)
    context.WithCancel 创建一个可以被取消的 context。cancel 是一个函数,调用它可以取消 context,通知所有通过该 context 传递的操作取消。
  • 设置超时的 Context
    ctx, cancel := context.WithTimeout(parentContext, 5*time.Second)
    context.WithTimeout 创建一个有超时限制的 context,如果超时,context 会自动被取消。
  • 设置具体的截止时间
    context.WithDeadline 创建一个有具体截止时间的 context,在指定的时间点自动取消 context。
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(parentContext, deadline)
  • 传递值的 Context
    ctx := context.WithValue(parentContext, "key", "value")
    context.WithValue 允许在 context 中传递一些值。这些值可以是请求上下文中的元数据,例如请求 ID、用户信息等。

1.3.3 使用示例

1.3.3.1 取消信号

package main

import (
    "context"
    "fmt"
    "time"
)

func longRunningTask(ctx context.Context) {
    select {
    case <-time.After(10 * time.Second):
        fmt.Println("Task completed")
    case <-ctx.Done():
        fmt.Println("Task cancelled")
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go longRunningTask(ctx)
    // 模拟 3 秒后取消任务
    time.Sleep(3 * time.Second)
    // 使用context 的cancel 函数停止goroutine
    cancel()

    // 给任务足够时间打印结果
    time.Sleep(1 * time.Second)
}

输出:
Task cancelled

在这个例子中,longRunningTask 函数执行一个长时间运行的操作(假设是 10 秒)。但是,主函数会在 3 秒后调用 cancel(),使得任务提前被取消,ctx.Done() 通知任务取消。

1.3.3.2 设置超时

package main

import (
    "context"
    "fmt"
    "time"
)

func longRunningTask(ctx context.Context) {
    select {
    case <-time.After(10 * time.Second):
        fmt.Println("Task completed")
    case <-ctx.Done():
        fmt.Println("Task timed out")
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    go longRunningTask(ctx)

    // 等待 6 秒,让超时发生
    time.Sleep(6 * time.Second)
}

输出:
Task timed out

这里,我们设置了一个 5 秒的超时,任务将在 5 秒后自动取消。

1.3.3.3 传递值

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.WithValue(context.Background(), "user_id", 1234)
    printUserInfo(ctx)
}

func printUserInfo(ctx context.Context) {
    userID := ctx.Value("user_id")
    fmt.Println("User ID:", userID)
}
输出:
User ID: 1234

在这个示例中,context.WithValue 将一个键值对(user_id 和 1234)存储到 context 中。然后,通过 ctx.Value("user_id") 获取这个值。

标签:func,Context,互斥,fmt,sync,并发,context,Go
From: https://www.cnblogs.com/jingzh/p/18638388

相关文章

  • 高级java每日一道面试题-2024年12月27日-并发篇-锁的优化机制了解吗 ?
    如果有遗漏,评论区告诉我进行补充面试官:锁的优化机制了解吗?我回答:在Java高级面试中,锁的优化机制是一个重要且常见的考点。以下是对Java锁优化机制的详细解释:一、锁的基本概念锁是多线程编程中至关重要的同步机制,用于确保线程间共享数据的正确性。然而,使用锁也会引......
  • client-go InClusterConfig方法
    InClusterConfig方法packagemainimport( "context" "test/signals" "time" "os" core_v1"k8s.io/api/core/v1" metav1"k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes......
  • 2024-12-28:求出出现两次数字的 XOR 值。用go语言,给定一个数组 nums,其中的数字出现的频
    2024-12-28:求出出现两次数字的XOR值。用go语言,给定一个数组nums,其中的数字出现的频率要么是一次,要么是两次。请找出所有出现两次的数字,并计算它们的按位XOR值。如果没有数字出现两次,则返回0。1<=nums.length<=50。1<=nums[i]<=50。nums中每个数字要么出现过一......
  • 介绍一下logos这个词法分析工具,它和nom相比如何?我看lalrpop官网给出的示例就是logos配
    UUUUUUUUUUUUUUUUUUUUUULogos简介Logos是一个用于词法分析的高效Rust库,其设计目标是简单、快速且高效。它通过声明式的方式定义词法规则,并利用Rust的强类型系统生成轻量级的词法分析器。Logos的特点声明式规则:使用Rust的枚举定义每种Token类型,并通过属性宏指定......
  • Java技术深度解析:探索并发编程的艺术与实战
    Java技术深度解析:探索并发编程的艺术与实战在当今的软件开发领域,高效处理多任务、充分利用多核CPU资源已成为衡量应用性能的关键指标之一。Java,作为一种广泛应用于企业级开发的编程语言,凭借其强大的并发处理能力,在众多编程语言中脱颖而出。本文将深入探讨Java并发编程的核......
  • C# 和 Go 的协同开发:打造高效并发与企业级应用的最佳实践
    在现代软件开发中,微服务架构和分布式系统成为主流。开发者面临着多种挑战,其中最常见的两个需求是高并发处理和复杂的企业级业务逻辑。C#和Go作为两种广泛使用的编程语言,各自有独特的优势,在应对这些挑战时能够发挥不同的作用。C#强调企业级开发的完整性和稳定性,特别适合构......
  • 从高并发到企业级应用:C# 和 Go 的完美结合
    在现代软件开发中,随着微服务架构和分布式系统的广泛应用,开发者需要应对各种高并发、高性能的需求。而在选择编程语言时,C#和Go是两种非常流行且各具优势的语言,分别擅长不同的应用场景。C#,以其强大的企业级开发支持和丰富的生态系统在后端、桌面和Web开发中占据重要地位;而......
  • Java 并发编程:掌握多线程的四个核心要点
    Java并发编程是后端开发中至关重要的一部分,它涉及到如何有效地使用多核处理器、提高程序的执行效率,并确保线程安全。无论是面试还是实际项目开发,掌握多线程编程的核心要点都至关重要。本文将围绕Java多线程编程的四个核心要点展开,帮助读者深入理解并发编程的基本原理、应用......
  • 【Java 并发编程】详解
    Java并发编程在当今的软件开发领域,随着多核处理器的广泛应用以及对系统性能要求的不断提高,Java并发编程变得愈发重要。它允许我们充分利用计算机的多核资源,同时处理多个任务,提高程序的执行效率和响应能力。然而,并发编程并非易事,它涉及到诸多复杂的概念、机制以及需要注......
  • 基于python+Django+mysql校园二手书籍交易平台系统设计与实现
     博主介绍:黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者,CSDN博客专家,在线教育专家,CSDN钻石讲师;专注大学生毕业设计教育、辅导。所有项目都配有从入门到精通的基础知识视频课程,学习后应对毕业设计答辩,提供核心代码讲解,答辩指导。项目配有对应开发......