Go 语言入门很简单 -- 16. Go 并发互斥锁 #私藏项目实操分享#

简介: Go 语言入门很简单 -- 16. Go 并发互斥锁 #私藏项目实操分享#

image.png

互斥是并发编程中最关键的概念之一。当我们使用  goruntine 和channels 进行并发编程时,如果两个 goruntine  尝试同时访问同一个内存位置的同一数据会发生竞争,有时候会产生意想不到的结果,通常很难调试,不符合日常要求,出现错误甚至很难修复。

生活场景

假设在生活中可能会发生的例子:有一个银行系统,我们可以从银行余额中存款和取款。在一个单线程的同步程序中,这个操作很简单。我们可以通过少量的单元测试有效地保证它每次都能按计划工作。

然而,如果我们开始引入多个线程,在 Go 语言中使用多个 goroutine,我们可能会开始在我们的代码中看到问题。

  1. 假如有一个余额为 1000 元的客户。
  2. 客户将 500 元存入他的账户。
  3. 一个 goroutine 会看到这个交易,读取价值为 1000 ,并继续将 500 添加到现有的余额中。(此时应该是 1500 的余额)
  4. 然而,在同一时刻,他拿 800 元来还分期付款的 iphone 13.
  5. 第二个程序在第一个程序能够增加 500 元的额外存款之前,读取了 1000 元的账户余额,并继续从他的账户中扣除 800 元。(1000 - 800 = 200)
  6. 第二天,客户检查了他的银行余额,发现他的账户余额减少到了 200 元,因为第二个程序没有意识到第一笔存款,并在存款完成前做了扣除操作。

这就是一个线程竞赛的例子,如果我们不小心落入这样的代码,我们的并发程序就会出现问题。

互斥锁和读写锁

互斥锁,英文名 Mutex,顾名思义,就是相互排斥,是保护程序中临界区的一种方式。

而临界区是程序中需要独占访问共享资源的区域。互斥锁提供了一种安全的方式来表示对这些共享资源的独占访问。

为了使用资源,channel 通过通信共享内存,而 Mutex 通过开发人员的约定同步访问共享内存。

image.png

让我们看一个没有 Mutex 的并发编程示例

package main
import (
    "fmt"
    "sync"
)
type calculation struct {
    sum int
}
func main() {
    test := calculation{}
    test.sum = 0
    wg := sync.WaitGroup{}
    for i := 0; i < 500; i++ {
        wg.Add(1)
        go dosomething(&test, &wg)
    }
    wg.Wait()
    fmt.Println(test.sum)
}
func dosomething(test *calculation, wg *sync.WaitGroup) {
    test.sum++
    wg.Done()
}

第一次结果为:491

image.png

第二次结果:493

[Running] go run "e:\Coding Workspaces\LearningGoTheEasiestWay\concurrency\mutex\v0\main.go"
493

在上面的例子中,我们声明了一个名为  test 的计算结构体,并通过 for 循环产生了多个 GoRoutines,将 sum 的值加 1。(如果你对 GoRoutines 和  WaitGroup 不熟悉,请参考之前的教程)。 我们可能期望 for 循环后 sum 的值应该是 500。然而,这可能不是真的。  有时,您可能会得到小于 500(当然永远不会超过 500)的结果。 这背后的原因是两个 GoRoutine  有一定的概率在相同的内存位置操作相同的变量,从而导致这种意外结果。 这个问题的解决方案是使用互斥锁。

使用 Mutex

package main
import (
    "fmt"
    "sync"
)
type calculation struct {
    sum   int
    mutex sync.Mutex
}
func main() {
    test := calculation{}
    test.sum = 0
    wg := sync.WaitGroup{}
    for i := 0; i < 500; i++ {
        wg.Add(1)
        go dosomething(&test, &wg)
    }
    wg.Wait()
    fmt.Println(test.sum)
}
func dosomething(test *calculation, wg *sync.WaitGroup) {
    test.mutex.Lock()
    test.sum++
    test.mutex.Unlock()
    wg.Done()
}

结果为:

[Running] go run "e:\Coding Workspaces\LearningGoTheEasiestWay\concurrency\mutex\v0.1\main.go"
500

在第二个示例中,我们在结构中添加了一个互斥锁属性,它是一种类型的 sync.Mutex。然后我们使用互斥锁的 Lock() 和 Unlock() 来保护 test.sum 当它被并发修改时,即 test.sum++。

请记住,使用互斥锁并非没有后果,因为它会影响应用程序的性能,因此我们需要适当有效地使用它。  如果你的 GoRoutines 只读取共享数据而不写入相同的数据,那么竞争条件就不会成为问题。 在这种情况下,您可以使用 RWMutex 代替  Mutex 来提高性能时间。

Defer 关键字

对 Unlock() 使用 defer 关键字通常是一个好习惯。

func dosomething(test *calculation) error{
    test.mutex.Lock()
    defer test.mutex.Unlock()
    err1 :=...
    if err1 != nil {
        return err1
    }   
    err2 :=...
    if err2 != nil {
        return err2
    }
    // ... do more stuff ...  
    return nil
}

在这种情况下,我们有多个  if err!=nil 这可能会导致函数提前退出。 通过使用 defer,无论函数如何返回,我们都可以保证释放锁。 否则,我们需要将  Unlock() 放在函数可能返回的每个地方。 然而,这并不意味着我们应该一直使用 defer。 让我们再看一个例子。

func dosomething(test *calculation){
    test.mutex.Lock()
    defer test.mutex.Unlock()
    // modify the variable which requires mutex protect
    test.sum =...
    // perform a time consuming IO operation
    http.Get()
}

在这个例子中,mutex 不会释放锁,直到耗时的函数(这里是 http.Get())完成。 在这种情况下,我们可以在 test.sum=... 行之后解锁互斥锁,因为这是我们操作变量的唯一地方。

总结

很多时候 Mutex 并不是单独使用的,而是嵌套在 Struct 中使用,作为结构体的一部分,如果嵌入的 struct 有多个字段,我们一般会把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔开来。

甚至可以把获取锁、释放锁、计数加一的逻辑封装成一个方法。

     

相关文章
|
2天前
|
Serverless Go
Go语言中的并发编程:从入门到精通
本文将深入探讨Go语言中并发编程的核心概念和实践,包括goroutine、channel以及sync包等。通过实例演示如何利用这些工具实现高效的并发处理,同时避免常见的陷阱和错误。
|
15天前
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。
|
17天前
|
存储 安全 Go
Go语言切片:从入门到精通的深度探索###
本文深入浅出地剖析了Go语言中切片(Slice)这一核心概念,从其定义、内部结构、基本操作到高级特性与最佳实践,为读者提供了一个全面而深入的理解。通过对比数组,揭示切片的灵活性与高效性,并探讨其在并发编程中的应用优势。本文旨在帮助开发者更好地掌握切片,提升Go语言编程技能。 ###
|
6月前
|
开发框架 安全 中间件
Go语言开发小技巧&易错点100例(十二)
Go语言开发小技巧&易错点100例(十二)
74 1
|
5天前
|
Go 数据安全/隐私保护 开发者
Go语言开发
【10月更文挑战第26天】Go语言开发
17 3
|
6天前
|
Java 程序员 Go
Go语言的开发
【10月更文挑战第25天】Go语言的开发
14 3
|
3月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
122 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
3月前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
3月前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
5月前
|
中间件 Go
go语言后端开发学习(三)——基于validator包实现接口校验
go语言后端开发学习(三)——基于validator包实现接口校验