Go语言 互斥锁

标签: go语言  翻译

导言

  • 原文链接: Part 25: Mutex
  • If translation is not allowed, please leave me in the comment area and I will delete it as soon as possible.

互斥锁

在这一部分,我们将讨论一下互斥锁。在之后,我们也会讨论:如何使用互斥锁和通道,去解决竞态条件。

竞态条件的英文是 race condition

临界区 (critical section)

在讨论互斥锁前,我们首先得理解,在并发编程中 临界区 的概念。
当程序并发运行时,修改共享资源的代码 不能同时 被多个协程执行。这一段修改共享资源的代码,被叫做临界区。

举个例子,下面有一段代码,它能让 x 增加 1

x = x + 1

单个协程执行这段代码时,并不会出现任何问题。

接下来,我们来看看,当协程并发时,为什么这段代码会出现错误。简单起见,我们假设这只有 2 个协程并发执行上面的代码。

对于上面的代码,系统将会通过下列几个步骤执行它。(这涉及到了一些附加知识,比如寄存器、加法器工作原理…但为了简单起见,我们假设该指令执行只需 3 个步骤)

  1. 获取 x 的当前值
  2. 计算 x + 1
  3. 将第 2 步的计算结果赋值给 x

当只有 1 个协程执行上面的 3 个步骤时,这完全没问题。

接下来,我们来讨论 2 个协程并发执行的情况。下图描绘了 2 个协程并发执行时,可能出现的一种情况。

在这里插入图片描述

我们假设 x 的初始值为 0。刚开始,协程1 获得了 x 的初始值,并计算 x + 1,在把该值赋给 x 前,系统的上下文切换给了 协程2。此时,协程 2 获得了 x 的初始值 — 此时 x 还为 0,计算 x + 1,之后系统上下文又切换回 协程1。现在,协程1 把计算所得的值 1 赋给了 x,因此,此时 x 变为了 1。之后,协程2 重新开始运行,它也把计算所得的值 1 分配给 x,此时 x 依旧等于 1。因此,两个协程执行后,x 的值为 1

接下来,我们来看看另外一种可能发生的情况。

在这里插入图片描述

在上面的情况中,协程1 开始运行并完成 3 个步骤,此时, x 的值为 1。之后,协程2 开始执行,也完成了 3 个步骤。最终,x 的值为 2

通过上面的两种情况,你就可以看出: x 的值可能是 1,也可能是 2,这取决于上下文的切换。
这一类无法确定的情况,被叫做竞态条件。(之所以无法确定,是因为程序的输出取决于协程的执行顺序)

在上面的情况中,竞态条件其实是可以避免的 — 在任意时间点,如果我们只允许一个协程执行临界区的代码,我们就能避免竞态条件。为了实现这个目的,我们可以使用 互斥锁

互斥锁介绍

互斥锁提供了一个锁机制,这个机制保证:在任意时间点,只有一个协程运行临界区的代码,从而避免竞态条件发生。

互斥锁的英文是:mutex

Mutex结构 位于 Go语言 的 sync包。Mutex 定义了 2 个操作: LockUnlock。在任意时间点,在 LockUnlock 之间的代码,都只能被一个协程执行,从而避免竞态条件。

mutex.Lock()  
x = x + 1  
mutex.Unlock()  

上面的代码可以保证:在任意时间点,只有一个协程执行x = x + 1,从而避免竞态条件。

原理:如果 协程a 已经获得了锁,而 协程b 想要获得这个锁,那么 协程b 将会阻塞,直到该锁解锁。

具有竞态条件的程序

在这一节,我们将写一个具有竞态条件的程序,而在下一节,我们将修复它。

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

在上面的程序中,第 7 行的 increment函数 将 x 的值增加 1,并调用 Done函数 — 表示已经完成任务。在第 15 行,我们创建了 1000 个协程。协程们是并发的,于是,这将会产生竞态条件,因为有多个协程访问 x = x + 1,这是一段临界区代码。

在本地运行这段代码,你会发现结果不是确定的 — 因为出现了竞态条件。我自己运行的结果如下:

  1. 第一次: final value of x 941
  2. 第二次: final value of x 928
  3. 第三次: final value of x 922

使用互斥锁解决竞态条件

在上面的程序中,我们创建了 1000 个协程。如果每次 x 都增长 1,最终 x 应该是 1000。在这一节,我们将使用互斥锁,去解决存在的竞态条件。

package main  
import (  
    "fmt"
    "sync"
    )
var x  = 0  
func increment(wg *sync.WaitGroup, m *sync.Mutex) {  
    m.Lock()
    x = x + 1
    m.Unlock()
    wg.Done()   
}
func main() {  
    var w sync.WaitGroup
    var m sync.Mutex
    for i := 0; i < 1000; i++ {
        w.Add(1)        
        go increment(&w, &m)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

Mutex 是一个结构体类型,在第 15 行,我们创建了一个 Mutex形对象m。在上面的程序中,我们已经改变了increment函数,使 x = x + 1 位于 m.Lock()m.Unlock() 之间。此时,这段代码就不会出现竞态条件了,因为在任意时刻,只有一个协程能执行临界区代码 — x = x + 1

运行这个程序,它会输出:

final value of x 1000  

注意:在第 18 行,我们传递的是 m 的指针,这是因为:假如我们传递的是 m 的值,那么每个协程都将拥有一份 m 的拷贝,这会使得竞态条件依旧存在。

使用通道解决竞态条件

我们也使用通道来解决竞态条件。我们看看怎么做。

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。在第 18 行,它被传递给 increment函数。使用这个有缓冲的通道,我们可以保证:在任意时间点,只有 1 个协程访问临界区代码 — x = x + 1
我们的做法是,在增加 x 前,我们将 true 传入通道。因为这个通道的容量是 1,所以如果有其他的协程,它们想向通道写入数据,它们都会阻塞,直到通道内的数据被读出。实际上,这个方法也可以保证只有一个协程位于临界区。

程序也会输出:

final value of x 1000 

互斥锁 vs 通道

通过分别使用互斥锁和通道,我们解决了竞态条件。那我们在解决竞态条件时,要选择哪个呢?
这取决于你要解决的问题是什么。如果你要解决的问题更适用于互斥锁,那你就用互斥锁吧,不用犹豫。如果你要解决的问题更适用于通道,那你就用通道。

大多数的 Go语言 新手,倾向于使用通道解决所有并发问题,因为他们认为:通道是 Go语言 中一个很棒的特性,这其实是错误的。Go语言 为我们提供了互斥锁和通道的选项,我们选择任意一个都没有错。

一般来说,当协程们需要通信时,这时可以采用通道,当协程们要访问临界区时,这时可以使用互斥锁。

对于我们上面解决的问题,我更倾向于使用互斥锁,因为此时协程们并不需要通信,互斥锁是很自然的选择。

我的建议是:选择合适的工具解决问题,而不是被工具本身所束缚。

源码透析

通道其实内置了一个互斥锁,通道结构如下:

// 文件位于: $GOROOT/src/runtime/chan.go

type hchan struct {
	// ....
	lock mutex
}

原作者留言

优质内容来之不易,您可以通过该 链接 为我捐赠。

最后

感谢原作者的优质内容。

这是我的第三次翻译,欢迎指出文中的任何错误。

版权声明:本文为qq_19018277原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_19018277/article/details/105267491

智能推荐

B1105 Spiral Matrix (画图)

B1105 Spiral Matrix (25分) //第一次只拿了21分 矩阵的长和宽,求最大因子,从sqrt(num)开始枚举. 每次循环一次,s++,t--,d--,r++ 测试点四运行超时,是因为输入一个数字的时候,需要直接输出这个数字。//1分 测试点二运行超时,最后一个数字不必再while循环一次,直接输出即可。//3分 最后一个测试点卡了好久/(ㄒoㄒ)/~~ 螺旋矩阵...

Java基础=>String,StringBuffer与StringBuilder的区别

字符串常量池 什么是字符串常量池? JVM为了减少字符串对象的重复创建,其维护了一块特殊的内存,这段内存被称为字符串常量池(存储在方法区中)。 具体实现 当代码中出现字符串时,JVM首先会对其进行检查。 如果字符串常量池中存在相同内容的字符串对象,如果有,则不再创建,直接返回这个对象的地址返回。 如果字符串常量池中不存在相同内容的字符串对象,则创建一个新的字符串对象并放入常量池,并返回新创建的字符...

java调用其他java项目的Https接口

项目中是这样的: 用户拿出二维码展示,让机器识别二维码, 机器调用开门的后台系统接口, 然后开门的后台系统接口需要调用管理系统的接口, 管理系统需要判断能不能开门.这两个系统是互相独立的.当时使用http调用是没有问题的.当时后来要求必须用https.废话不说,直接代码: 我的项目中调用的是 HttpsUtils.Get(utlStr) 这个接口 开门系统接口如下图:   管理系统的接口...

Hadoop1.2.1全分布式模式配置

一 集群规划 主机名            IP                               安装的软件 &nbs...

Go语言gin框架的安装

尝试安装了一下gin,把遇到的一些小问题来记录一下 安装步骤 首先来看看官方文档,链接点这里 可以看到安装步骤很简单,就一句话 在命令行中输入这句话运行等待就好。 问题来了,因为墙的问题,go get会很慢,所以命令行里面半天什么反应也没有,不要急,慢慢等着就会看到gin-gonic/gin这个目录出现 这个时候命令行还是没有结束,表示还在下一些东西。有的时候可能心急的人就停了(比如我),然后写个...

猜你喜欢

uni-app表单组件二

input(输入框) 属性名 类型 说明 平台差异 value String 输入框的初始内容 type String input 的类型 password Boolean(默认false) 是否是密码类型 placeholder String 输入框为空时占位符 placeholder-style String 指定 placeholder 的样式 placeholder-class Strin...

深入理解 JavaScript 代码执行机制

深入理解 JavaScript 代码执行机制 前言 本文仅为个人见解,如有错误的地方欢迎留言区探讨和指正。 1、餐前甜品 如下是一段 JavaScript 代码,如果你毫不犹豫的说出代码执行顺序。那么请直接滚动到底部,留下你的足迹,接受膜拜。如果还不是很确定,那么请往下继续查看。 2、磨刀不误砍柴工(了解浏览器原理) (1) 进程和线程 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小...

Centos7下配置DRBD Cluster扩展节点

操作环境 CentOS Linux release 7.4.1708 (Core) DRBDADM_BUILDTAG=GIT-hash:\ ee126652638328b55dc6bff47d07d6161ab768db\ build\ by\ [email protected]\,\ 2018-07-30\ 22:23:07 DRBDADM_API_VERSION=2 DRBD_KERNEL_VER...

选择排序了解一下

选择排序是一种简单直观的排序算法,它的主要思想:初始时在序列中找到最小(大)的元素,放到序列的起始位置作为已排序序列;然后再从剩余未排序元素中继续寻找最小(大)的元素,放到已排序序列的末尾,以此类推,直到所有元素均排序完毕。 即每遍历一次就记住了最大(小)的元素的位置,最后仅需要一次交换操作就可以放到其适合的位置。 如下图所示: 实现代码如下: 选择排序是不稳定排序,时间复杂度在最优、最坏情况下都...