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 放在要控制的字段上面,然后使用空格把字段分隔开来。

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

     

相关文章
|
3月前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
333 4
|
4月前
|
Linux Go iOS开发
Go语言100个实战案例-进阶与部署篇:使用Go打包生成可执行文件
本文详解Go语言打包与跨平台编译技巧,涵盖`go build`命令、多平台构建、二进制优化及资源嵌入(embed),助你将项目编译为无依赖的独立可执行文件,轻松实现高效分发与部署。
|
7月前
|
Go 开发者
Go语言包的组织与导入 -《Go语言实战指南》
本章详细介绍了Go语言中的包(Package)概念及其使用方法。包是实现代码模块化、复用性和可维护性的核心单位,内容涵盖包的基本定义、命名规则、组织结构以及导入方式。通过示例说明了如何创建和调用包,并深入讲解了`go.mod`文件对包路径的管理。此外,还提供了多种导入技巧,如别名导入、匿名导入等,帮助开发者优化代码结构与可读性。最后以表格形式总结了关键点,便于快速回顾和应用。
324 61
|
6月前
|
JSON 中间件 Go
Go语言实战指南 —— Go中的反射机制:reflect 包使用
Go语言中的反射机制通过`reflect`包实现,允许程序在运行时动态检查变量类型、获取或设置值、调用方法等。它适用于初中级开发者深入理解Go的动态能力,帮助构建通用工具、中间件和ORM系统等。
345 63
|
7月前
|
存储 Go 开发者
Go 语言中如何处理并发错误
在 Go 语言中,并发编程中的错误处理尤为复杂。本文介绍了几种常见的并发错误处理方法,包括 panic 的作用范围、使用 channel 收集错误与结果,以及使用 errgroup 包统一管理错误和取消任务,帮助开发者编写更健壮的并发程序。
171 4
Go 语言中如何处理并发错误
|
7月前
|
测试技术 程序员 Go
Go语言测试简明指南:深度解读go test命令
总的来说,go test是 Go 语言中一个强而有力的工具,每个 Go 程序员都应该掌握并把它融入到日常的开发和调试过程中。就像是一个眼镜过滤出的太阳,让我们在宽阔的代码海洋中游泳,而不是淹没。用好它,让我们的代码更健壮,让我们的生产力更强效。
588 23
|
5月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
5月前
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。
|
7月前
|
数据可视化 测试技术 Go
Go 语言测试与调试:`go test` 工具用法
`go test` 是 Go 语言内置的测试工具,支持单元测试、基准测试、示例测试等功能。本文详解其常用参数、调试技巧及性能测试命令,并提供实际项目中的应用示例与最佳实践。
|
7月前
|
测试技术 Go 开发者
Go语言常见接口设计技巧-《Go语言实战指南》
本文分享了 Go 语言中接口设计的最佳实践与技巧。首先介绍了接口设计原则,包括面向接口编程和接口隔离原则(定义最小化接口)。接着详细讲解了常用技巧:关注行为而非数据、优先返回接口隐藏实现细节、遵循“-er”命名惯例、使用接口组合提升灵活性、通过 Mock 接口简化单元测试,以及避免导出仅内部使用的接口。最后以表格形式总结了各技巧的核心要点,帮助开发者编写更清晰、可维护的代码。
273 11

热门文章

最新文章