Go并发安全,锁和原子操作

一. 并发安全

        有时候在Go代码中可能存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。

        1.1 互斥锁

        互斥锁是一种常见的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。使用互斥锁来修复上面的代码:

        使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁。当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒策略是随机的。

        1.2 读写互斥锁

        互斥锁是完全互斥的,当一个goroutine获取到锁时,其它的goroutine得等待锁的释放。但是实际情况下,是读多写少的情况,如果使用互斥锁效率会很低。实际当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的。这种场景下使用读写锁效率会更高一点。读写锁在Go语言中使用Sync包中的RWMutex类型。

        读写锁分为两种:读锁和写锁。当一个goroutine获取到读锁,其它协程也可以获取该读锁,但是获取写锁会等待。当一个goroutine获取到写锁,其它goroutine无论是获取读锁还是写锁都会等待。

        示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	x      int
	wg     sync.WaitGroup
	rwlock sync.RWMutex

	lock sync.Mutex
)

func write() {
	//加互斥锁
	//lock.Lock()
	//加写锁
	rwlock.Lock()
	x = x + 1
	time.Sleep(10 * time.Millisecond) //加锁读操作耗时10毫秒
	//lock.Unlock()                     //释放互斥锁
	rwlock.Unlock() //释放写锁
	wg.Done()
}

func read() {
	//加互斥锁
	//lock.Lock()
	rwlock.RLock() //加读锁
	time.Sleep(time.Millisecond)
	//lock.Unlock() //释放互斥锁
	rwlock.RUnlock() //释放读锁
	wg.Done()
}

func main() {
	start := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}

	wg.Wait()
	end := time.Now()
	fmt.Println(end.Sub(start))

}

        注意:读写锁适用于读多写少的情况,如果读和写操作差别不大,读写锁优势发挥不出来。

二. Sync包

        2.1 sync.WaitGroup

        我们直到当主协程结束,不论子协程是否执行完都会结束执行。但是在代码中生硬的使用time.Sleep肯定是不合适的,Go语言可以使用sync.WaitGroup来实现并发任务的同步。

        sync.WaitGroup内部维护着计数器,计数器的值可以增加和减少。例如:当我们启动了N个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器为0时,表示所有并发任务已完成。

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func test() {
	defer wg.Done()
	fmt.Println("hello world")
}

func main() {
	wg.Add(1)
	go test()
	fmt.Println("main goroutine done!")
	wg.Wait()
}

        需要注意:sync.WaitGroup是一个结构体,传递的时候,为了防止拷贝的开销,最好传递指针。

        2.2 sync.Once

        在编程的很多情况下我们需要确保某些操作在高并发场景下只执行一次,例如:只加载一次配置文件,只关闭一次通道等。

        Go语言中的Sync包提供了一个针对只执行一次场景的解决方案——sync.Once

         注意:Do方法中,如果要执行的函数f需要传递参数,就需要搭配闭包来使用。因为函数f没有参数,闭包获取外面的变量。

  • 加载配置文件示例

        延时一个开销很大的初始化操作,到真正用到它的时候执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序启动耗时,而且很有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。

        多个goroutine并发调Icon函数时不是并发安全的,现代编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由的重排访问内存顺序。loadIcon函数可能会被重排成以下结果:

func loadIcond() {
	icons = make(map[string]image.Image)
	icons["left"] = loadIcon("left.png")
	icons["right"] = loadIcon("right.png")
	icons["up"] = loadIcon("up.png")
	icons["down"] = loadIcon("down.png")
}

        在这种情况下即使判断了icons不是nil也不意味着变量初始化完成了。因为可能一个协程执行到 icons = make(map[string]image.Image),另外一个协程执行到判断 icons == nil,此时该判断为false,但是icons还没有被赋值。

        考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做会引发性能问题。

        使用sync.Once改造示例代码

         sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。(有点像C/C++里的单例模式),这样设计就能保证初始化操作的时候是并发安全的,并且初始化操作也不会被执行多次。

        2.3 sync.Map

        Go语言内置的的map不是并发安全的。

        像上面这种情况就需要为map加锁来保证并发的安全性,Go语言的Sync包提供了一个开箱即用的并发安全的map——sync.Map,可以直接使用,不需要像内置map一样使用make函数初始化才能使用。同时sync.Map内置了诸如Store,Load,LoadOrStore,Delete,Range等函数。

三. 原子操作(atomic包) 

        代码中的加锁操作因为涉及内核态的上下文切换会比较耗时,代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法他在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作有内置标准库sync/atomic提供。

        可以通过标准库文档查看sync/atomic库中提供了哪些方法。Go语言标准库文档中文版 

  • 示例

        比较原子操作和互斥锁 

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

var x int64
var mt sync.Mutex
var mg sync.WaitGroup

// 不是并发安全的
func add() {
	x++
	mg.Done()
}

// 加锁版,开销比较大
func mutexAdd() {
	mt.Lock()
	x++
	mt.Unlock()
	mg.Done()
}

// 原子操作版
func atomicAdd() {
	atomic.AddInt64(&x, 1)
	mg.Done()
}

func main() {
	start := time.Now()
	for i := 0; i < 1000; i++ {
		//go add() 不是并发安全的
		go mutexAdd()
		mg.Add(1)
	}
	mg.Wait()
	end := time.Now()
	fmt.Println(x)
	fmt.Println(end.Sub(start))

	start = time.Now()
	for i := 0; i < 1000; i++ {
		go atomicAdd()
		mg.Add(1)
	}
	mg.Wait()
	end = time.Now()
	fmt.Println(x)
	fmt.Println(end.Sub(start))

}

         atomic包提供了底层的原子级的操作,对于同步算法的实现很有用。这些函数必须谨慎并保证正确的使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/573396.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【日志】CSDN-AI助手升级日志

CSDN-AI助手升级日志 2023/04/05上线 支持点赞、收藏回访 关注回访&#xff08;对方至少有一条博客的记录&#xff09; 评论回访 私信检测到群发消息自动三连 OR 通过私信指令三连触发 bug优化 优化检测模式&#xff0c;防止出现多触发情况 为了防止操作额度不够&#xff0c…

Spring日志

Spring日志的作用: 1.定位和发现问题 2.系统监控 3.数据采集 4.日志审计 打印日志步骤: 1.定义日志对象2.打印日志 RestController public class LoggerController {private static Logger logger LoggerFactory.getLogger(LoggerController.class);PostConstructpublic v…

Dos慢速攻击

这里写自定义目录标题 Dos慢速攻击 Dos慢速攻击 测试结果为“Exit status&#xff1a; No open connections left"&#xff0c;代表无此漏洞。 如果测试结束后connected数量较多&#xff0c;closed数量很少或0&#xff0c;说明之前建立的慢速攻击测试连接没有关闭&#…

最佳三款员工电脑行为监控软件评选

企业对员工生产力和数据安全的关注不断增加&#xff0c;员工电脑行为监控软件成为了许多企业不可或缺的工具。 这些软件不仅可以帮助企业管理者实时监测员工的电脑使用情况&#xff0c;还可以防止数据泄露和滥用公司资源。 然而&#xff0c;市面上有数不胜数的员工电脑行为监控…

【前端缓存】localStorage是同步还是异步的?为什么?

写在开头 点赞 收藏 学会 首先明确一点&#xff0c;localStorage是同步的 一、首先为什么会有这样的问题 localStorage 是 Web Storage API 的一部分&#xff0c;它提供了一种存储键值对的机制。localStorage 的数据是持久存储在用户的硬盘上的&#xff0c;而不是内存。这意…

海外盲盒APP开发:探索海外盲盒市场的商机

随着娱乐消费的流行&#xff0c;盲盒在我国可以说是非常火热&#xff0c;消费群体和市场规模逐年增加。在盲盒热潮下&#xff0c;不少潮玩企业也纷纷加入到了盲盒赛道中&#xff0c;市场竞争非常激烈&#xff01; 此外&#xff0c;我国盲盒出海也成为了一个大趋势。盲盒不仅在…

如何用 Llama 3 免费本地 AI 分析数据和可视化?

帮助你消除调用大模型 API 带来的数据安全烦恼。 模型 今天我们来探讨一个有趣的话题 —— 如何使用 Llama 3 免费地进行数据分析和可视化。 Meta 团队在 2024 年 4 月发布了两款 Llama 3 新模型&#xff0c;一款是 8B&#xff0c;即 80 亿参数&#xff1b;另一款则是 70B&…

鸿蒙arkui 也支持热重载了 我不允许你不会

历史背景 因为鸿蒙出的ark ui 很多人说很像flutter&#xff0c;但是能不能做到跟flutter一样支持热重载呢 。答案是可以的 我们喜就教大家如何操作。 构建工程 选择这个空的模版 next finish 点击file 找到setting 找到 Tools Actions on Save 我们把 Perform hor reload 勾上…

说说你对二分查找的理解?如何实现?应用场景?

一、是什么 在计算机科学中&#xff0c;二分查找算法&#xff0c;也称折半搜索算法&#xff0c;是一种在有序数组中查找某一特定元素的搜索算法 想要应用二分查找法&#xff0c;则这一堆数应有如下特性&#xff1a; 存储在数组中有序排序 搜索过程从数组的中间元素开始&…

招商画册不会制作?这个教程收藏好

在制作招商画册过程中可能对于一些小型企业或刚刚起步的企业来说&#xff0c;招商画册的制作也不是一个难以克服的难题。本文将为您提供一些制作招商画册的技巧和建议。在制作招商画册前肯定是需要选择一款合适的在线制作工具&#xff0c;如FLBOOK 这个平台本就有海量的模板和设…

vue快速入门(四十四)自定义组件

注释很详细&#xff0c;直接上代码 上一篇 新增内容 全局注册自定义组件并应用局部注册自定义组件并应用 此篇使用了axios模块没有安装导入的先看这一篇 axios模块下载与导入 源码 main.js import Vue from vue import App from ./App.vue//全局引入axios // 引入axios impor…

超市火灾烟雾蔓延及人员疏散的matlab模拟仿真,带GUI界面

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 出口在人员的视野范围内时&#xff0c;该元胞选择朝向引导点的方向运动。出口不在人员的视野范围内时&#xff0c;作随机运动&#xff0c;8个方向的运动概率相等。…

网工交换基础——Qinq技术

一、Qinq的概述 QinQ&#xff08;802.1Q-in-802.1Q&#xff09;&#xff0c;也叫做VLAN Stacking或Double VLAN&#xff0c;由IEEE 802.1ad标准定义&#xff0c;是一项扩展VLAN空间的技术&#xff0c;通过在802.1Q标签报文的基础上再增加一层802.1Q的Tag来达到扩展VLAN空间的目…

Linux驱动开发——(六)按键中断实验

目录 一、简介 二、修改设备树 2.1 添加pinctrl节点 2.2 添加KEY设备节点 2.3 检查是否被其他外设使用 三、代码 3.1 驱动代码 3.2 测试代码 3.3 平台测试 一、简介 以I.MX6U-MINI为例&#xff0c;实现KEY0按下后&#xff0c;设备识别到并将数据发送到平台。 二、修…

40-数组 _ 数组越界

40-1 数组的下标是有范围限制的 数组的下标规定是从0开始的&#xff0c;如果数组有n个元素&#xff0c;最后一个元素的下标就是n-1。所以数组的下标如果小于0&#xff0c;或者大于n-1&#xff0c;就是数组越界访问了&#xff0c;超出了数组合法空间的访问。 C语言本身不做数组…

VUE2版本的仿微信通讯录侧滑列表

<template><!-- Vue模板部分 --><div><div v-for"(group, index) in groupedArray" :key"index" ref"indexcatch"><h2>{{ letter[index] }}</h2><ul><li v-for"item in group" :key&quo…

python基础--文件操作

目标 文件操作的作用文件的基本操作 打开读写关闭 文件备份文件和文件夹的操作 一. 文件操作的作用 思考&#xff1a;什么是文件&#xff1f; 思考&#xff1a;文件操作包含什么&#xff1f; 答&#xff1a;打开、关闭、读、写、复制… 思考&#xff1a;文件操作的的作用…

聚类分析字符串数组

聚类分析字符串数组 对多个字符串进行聚类分析旨在根据它们之间的相似度将这些字符串划分成若干个类别&#xff0c;使得同一类别内的字符串彼此相似度高&#xff0c;而不同类别间的字符串相似度低 小结 数据要清洗。清洗的足够准确&#xff0c;可能不需要用聚类分析了数据要…

45. 【Android教程】内容提供者 - Content Provider

本节学习最后一个 Android 组件——内容提供者。顾名思义&#xff0c;它可以用来给其他的 App 提供各种内容&#xff0c;比如 Android 自带的短信、联系人、日历等等都是一个普通的 App&#xff0c;当你需要这些内容的时候&#xff0c;就可以向它们的 Content Provider 发起请求…

C/C++ 入门(7)vector类(STL)

个人主页&#xff1a;仍有未知等待探索-CSDN博客 专题分栏&#xff1a;C 请多多指教&#xff01; 目录 一、标准库中的vector 1、了解 2、vector常用接口 二、vector的实现 1、框架 2、构造、析构函数 3、操作函数 三 、问题 1、由于赋值而引起的浅拷贝 2、因为类没…