【golang】1、用 double check 正确的锁临界区

2023-10-31

如果写到并发的程序,就要考虑加锁。而加锁很容易出现 bug,且极难排查。本文以 golang 语言为例,介绍怎样正确地锁住临界区。

在这里插入图片描述

一、错误的互斥锁示例

我们以一段测试代码为例,初始化一个 map,我们并发的调用 f 函数,希望用 map 做去重。并且为了减小锁的粒度,我们用了读写锁,读操作互相不阻塞,而写操作互相阻塞,代码如下:

package main

import (
	"github.com/datager/codes/gocodes/dg/utils/signal"
	"sync"
	"time"
)

var (
	mp    = map[string]struct{}{}
	mutex = sync.RWMutex{}
)

func main() {
	go f("goroutine1:", "k")
	go f("goroutine2:", "k")
	signal.WaitForExit()
}

func f(tag string, k string) {
	mutex.RLock()
	_, exist := mp[k]
	mutex.RUnlock()
	if exist {
		println(tag, "already exist")
		return
	}

	// business logic
	time.Sleep(time.Second)

	mutex.Lock()
	mp[k] = struct{}{}
	println(tag, "because not exist, so put into it, now len is", len(mp))
	mutex.Unlock()
}

然而,打印结果如下,我们并发地调用了函数 f,并且用了相同的参数 k,但却并未起到去重的作用,如下结果的第一行 len = 1,而第二行的 len 居然不为 2:

goroutine2: because not exist, so put into it, now len is 1
goroutine1: because not exist, so put into it, now len is 1

这是因为 RLock、RUnlockLock、Unlock 之间的代码并未受临界区保护:假设以 goroutine1 与 goroutine2 经过 RLock 均得到 !exist 的结果为起点,当 goroutine1 先 Lock 并 UnLock 后,goroutine2 才拿到 Lock 的话,goroutine 2 就会覆盖 goroutine1 的结果,导致 mp[k] = struct{}{} 被错误地执行了2次。

根源是:读是为了写,如果有读操作,且有写操作,则应将读写操作绑定作为一个临界区。而不应该有2个临界区。

二、粗暴的临界区

所以,粗暴地方式,就如上文所说,将读写操作绑定作为一个临界区,代码如下:

package main

import (
	"sync"
	"time"
)

var (
	mp    = map[string]struct{}{}
	mutex = sync.Mutex{}
)

func main() {
	go f("goroutine1:", "k")
	go f("goroutine2:", "k")
	signal.WaitForExit()
}

func f(tag string, k string) {
	mutex.Lock()
	_, exist := mp[k]
	if exist {
		println(tag, "already exist")
		mutex.Unlock()
		return
	}

	// business logic
	time.Sleep(time.Second)

	mp[k] = struct{}{}
	println(tag, "because not exist, so put into it, now len is", len(mp))
	mutex.Unlock()
}

运行后,正确的得到了输出,效果如下:

goroutine1: because not exist, so put into it, now len is 1
goroutine2: already exist

三、double check 来提升性能

然而,为了性能,我们可以用常见的 double check 方式,来处理临界区。

即先无锁检查,再锁 + 判断 + 处理 + 解锁。

通过最先的无锁检查,可以避免没必要的临界区锁定,从而提升性能。代码如下:

package main

import (
	"github.com/datager/codes/gocodes/dg/utils/signal"
	"sync"
	"time"
)

var (
	mp    = map[string]struct{}{}
	mutex = sync.Mutex{}
)

func main() {
	go f("goroutine1:", "k")
	go f("goroutine2:", "k")
	signal.WaitForExit()
}

func f(tag string, k string) {
	_, firstExist := mp[k]
	if firstExist {
		println(tag, "already exist from first check")
		return
	}

	mutex.Lock()
	_, exist := mp[k]
	if exist {
		println(tag, "already exist from second check")
		mutex.Unlock()
		return
	}

	// business logic
	time.Sleep(time.Second)

	mp[k] = struct{}{}
	println(tag, "because not exist, so put into it, now len is", len(mp))
	mutex.Unlock()
}

正确地输出了结果,其中 goroutine1 加锁了,而后续拿到锁的 goroutine2 在无需拿锁的情况下就得到了 firstExist 的结果并提前 return,在结果如下:

goroutine1: because not exist, so put into it, now len is 1
goroutine2: already exist from second check

四、总结

本文总结了锁临界区的常见问题,因为读是为了写,所以需要将读和写放在同一个临界区中来保证正确性,并且为了性能可以用 double check 的方式减少锁的次数。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【golang】1、用 double check 正确的锁临界区 的相关文章

  • 【数据库】NoSQL数据库简介

    基于自己的理解 对几个常见的NoSQL数据库进行一下简单介绍 如有描述错误的 欢迎指正 一 数据库分类 数据库分两大类 SQL数据库和NoSQL数据库 SQL数据库 常用的有 Mysql Oracle MSSQL DB2等 这些大家应该都比

随机推荐

  • $.each()方法的使用

    jQuery中 each 方法的使用 each 是对数组 json和dom结构等的遍历 说一下他的使用方法吧 1 遍历一维数组 var arr1 aa bb cc dd each arr1 function i val 两个参数 第一个参数
  • 【AAAI 2021】多出口架构的知识蒸馏:Harmonized Dense Knowledge Distillation Training for Multi-Exit Architectures

    AAAI 2021 多出口架构的知识蒸馏 Harmonized Dense Knowledge Distillation Training for Multi Exit Architectures 论文地址 主要问题 主要思路 多出口网络
  • Java 面向对象之封装

    目录 1 类和对象 2 单个对象内存图 3 两个对象内存图 4 两个引用指向同一对象内存图 5 成员变量和局部变量 6 private 关键字 7 this 关键字 8 this 内存原理 9 封装 10 构造方法 面向过程编程 POP P
  • sqli-labs-master【Less-11/12/13/14/15/16】

    Less 11 进入Less 11之后 可以看到是全新的关卡 是一个表单的形式 需要我们填入用户名和密码才可以提交 首先我们随便填入一个用户名和密码看页面会回显给我们什么内容 我在这块用户名和密码都输入的是123 可以看到登陆尝试失败 不过
  • java中steam流的使用

    1 数组合并 原始合并 定义俩个集合合并为一个集合 例子 public class 测试 把小王合道 list中 public static void main String args throws Exception final Arra
  • 华为机试——0-1背包问题

    华为机试 0 1背包问题 给定一个数 比如20 然后给定几个数字 如1 3 5 7 8 输出 1 3 5 7 8 0 0 0 1 1 因为5 7 8 20 include
  • Qt实现窗口整体拖拽功能.

    我们都知道 当我们鼠标点击并移动应用程序的上边框时 窗口才会整体移动 但有些窗口 你点击移动其他部分 也可以让窗口跟着鼠标移动 这里就教你如何整体移动窗口 其实实现这样的功能很简单 只需要重写以下的3个鼠标函数即可 c h ifndef C
  • 数据挖掘案例

    图中的左边是SPSS在1999年提出的 跨行业数据挖掘标准流程 在图中定义了数据挖掘的6个步骤 虽然这个图已经提出有10几年了 但是在大数据环境下 这个流程依然适用 1 理解商业问题 这需要大数据科学家和行业专业 以及客户的业务专家一起来明
  • win7系统开机直接进bios 不能正常启动,插u盘可以启动

    朋友的win7系统笔记本电脑出问题了 拿来帮忙看看 现象 开机后直接进bios界面 插个u盘就能正常进硬盘的windows系统 分析 以为是开机启动项不对 进bios界面 选择硬盘优先启动 保存退出 重启后并不能进系统 还是进bios 尝试
  • 使用tf2的saved_model进行推理

    import tensorflow as tf import cv2 from PIL import Image import numpy as np import colorsys import os import matplotlib
  • 蓝桥杯官网练习题(谈判)

    题目描述 在很久很久以前 有 n 个部落居住在平原上 依次编号为 1 到 n 第 i 个部落的人数为 ti 有一年发生了灾荒 年轻的政治家小蓝想要说服所有部落一同应对灾荒 他能通过谈判来说服部落进行联合 每次谈判 小蓝只能邀请两个部落参加
  • 测试集数据语义特征t_SNE降维可视化

    在图像分类通用步骤中 第一步是训练数据集 第二步是测试数据集 而无论是训练还是测试 对我们而言都是黑盒子 我们只知道模型从训练数据中学习到了特征 然后应用到测试集数据集中 最终得出测试集数据的分类结果 对于其中的细节 我们就不得而知了 因此
  • 人工智能(python)开发——Linux环境基本知识要点

    1 终端工具 打开方法 1 点击左侧图标 2 点击 搜索计算机 终端图标上方 输入 gnome terminal 终端 3 快捷键 Ctrl shift t 退出方法 1 exit lt 回车 gt 2 ctrl d 3 点击左上角x 2
  • 【电路设计】将AC交流电转换为DC直流电

    文章传送门 前言 一 直流 AC 与交流 DC 的区别 二 转换步骤 降压 整流 滤波 稳压 1 整流 交流电 直流电 2 滤波 滤除脉动直流的交流部分 3 稳压 稳定到需要的电压值 三 简单分辨直流电与交流电 前言 日常生活中用到的都是2
  • ubuntu的tmp目录下自己创建的文件每次重启后自动删除

    ubuntu的tmp目录下自己创建的文件每次重启后自动删除 可以修该 etc default rcS文件中的内容而改变为不自动删除 输入命令 vim etc default rcS 开始编辑 将TMPTIME 0改为TMPTIME 1 保存
  • 快速排序——C++实现

    快速排序采用 分治法 首先选取一个 轴值 假设数据中有k个数小于轴值 那么这k个数放在数组最左边的k个位置上 而不小于k的数放在数组右边的n k个位置上 这就实现了数组的一个 分割 给定分割中的值不必排序 只要求所有结点都被轴值正确分割 快
  • 在Android中简单使用消息通知

    发送通知 消息通知可以在Activity BroadcastReceiver service中创建 但是无论在哪创建 整体步骤基本一样 Android中使用通知渠道来开启通知功能 通知渠道一旦设定就不能再更改 所以创建通知渠道的时候最好进行
  • Tensrfow GAN Discriminator 如何使用hinge loss训练

    hinge loss 核心点 网络的输出要确保是 1 1 范围 之前一直用cross entrype loss这一点没有台注意 所以之前一直没写对 hinge loss 核心代码 def Hinge loss pos neg name Hi
  • ecipse theme

    市场里搜 jeeeyul s Eclipse Themes
  • 【golang】1、用 double check 正确的锁临界区

    文章目录 一 错误的互斥锁示例 二 粗暴的临界区 三 double check 来提升性能 四 总结 如果写到并发的程序 就要考虑加锁 而加锁很容易出现 bug 且极难排查 本文以 golang 语言为例 介绍怎样正确地锁住临界区 一 错误