并发安全与锁
总述
这篇文章,我想谈一谈自己对于并发变成的理解与学习。主要涉及以下三个部分:goroutine,channel以及lock
临界区
首先,要明确下面两组概念
并发和并行
并行:指几个程序每时每刻都同时进行
并发:指在单位时间内同时运行
go的并发模型在内存共享方面有点类型SMP
模型:
相似之处:
- 多处理器利用:
- SMP:SMP 系统利用多个处理器(或核心)来并行处理任务,系统中的所有处理器共享同一内存空间。
- Go: Go 通过 goroutines 和调度器来利用多个处理器,能够在多核处理器上并行执行多个 goroutine。Go 的调度器会将 goroutine 分配到可用的处理器上,从而实现并行计算。
- 共享内存:
- SMP:所有处理器访问同一内存空间,可以直接共享数据。
- Go: goroutines 可以通过共享内存进行通信,虽然 Go 提供了通道(channels)作为主要的同步和通信机制,但共享内存仍然是可能的。Go 的同步机制,如互斥锁(mutex)和原子操作,帮助避免数据竞争和保持一致性。
不同之处:
- 并发模型:
- SMP:SMP 更多地关注于物理硬件层面的多处理器架构和资源共享,不直接涉及编程模型。
- Go: Go 语言提供了一种高层次的并发编程模型,通过 goroutines 和 channels 来简化并发编程。Go 的调度器负责将 goroutines 映射到系统线程和处理器上,程序员可以更高效地编写并发程序,而不需要直接管理线程。
- 调度和管理:
- SMP:在 SMP 系统中,操作系统负责调度和管理线程,确保线程能够在多个处理器上运行。
- Go: Go 运行时提供了一个轻量级的调度器,称为 GOMAXPROCS,管理 goroutines 的执行。Go 的调度器将 goroutines 调度到系统线程上,而不是直接由操作系统的线程调度机制来管理。
- 编程模型:
- SMP:编程模型通常需要考虑线程同步、数据竞争和缓存一致性等底层细节。
- Go: Go 的并发模型通过 goroutines 和通道提供了较高的抽象,程序员不需要直接处理线程和锁的细节,使并发编程更为简洁和安全。
总的来说,虽然 Go 的并发模型与 SMP 在利用多处理器的并行能力上有相似之处,但 Go 的模型更专注于简化并发编程,而 SMP 更加关注底层的处理器和内存管理。
协程 进程 线程
1. 进程(Process)
定义:
进程是计算机中正在运行的程序的实例,具有独立的地址空间、资源和执行上下文。每个进程都有自己的内存空间、文件描述符和其他系统资源。
特点:
- 资源独立:每个进程有独立的内存空间,进程间的通信需要使用 IPC(进程间通信)机制,如管道、共享内存等。
- 开销大:创建和销毁进程的开销相对较大,因为涉及内存分配和资源管理。
2. 线程(Thread)
定义:
线程是进程中的一个执行单元,是程序执行的最小单位。一个进程可以包含多个线程,它们共享进程的地址空间和资源。
特点:
- 共享资源:同一进程中的线程共享进程的内存和资源,因此线程间的通信比进程间的通信更为高效。
- 开销小:线程的创建和销毁比进程要轻量,因为不需要为每个线程分配独立的地址空间。
3. 协程(Coroutine)
定义:
协程是一种用户级的轻量级线程,可以在程序中暂停和恢复执行。协程通常在同一线程中切换,由程序控制,而不是由操作系统调度。
特点:
- 高效:由于协程是由程序员控制的,切换开销相对较小,适合处理高并发场景。
- 共享同一线程:协程通常在同一线程内运行,之间的切换非常快速,适合执行 I/O 密集型任务。
区别总结
特性 | 进程 | 线程 | 协程 |
---|---|---|---|
定义 | 程序的独立实例 | 进程内的执行单元 | 用户级的轻量级线程 |
内存 | 独立的内存空间 | 共享进程内的内存 | 共享线程内的内存 |
开销 | 高 | 较低 | 低 |
调度 | 由操作系统调度 | 由操作系统调度 | 由程序控制 |
通信 | 复杂(需要 IPC) | 简单(共享内存) | 更简单(通过函数调用) |
应用场景 | 适合 CPU 密集型任务 | 适合多任务并发处理 | 适合高并发的 I/O 密集型任务 |
适用场景
- 进程:适用于需要高度隔离的任务,比如服务器、桌面应用等。
- 线程:适合需要共享资源的任务,如 GUI 应用程序的事件处理、网络服务等。
- 协程:适合 I/O 密集型的应用,如网络爬虫、异步处理等,能够有效管理大量并发任务。
Go程的使用
go语言使用的是共享内存(与传统的共享模型不同的是,go语言与大多编程语言一样,允许加锁来保证线程安全)的并发模式,使用go关键字可以启动一个go程(先后顺序是随机的、可互换的),不同的go程之间可以通过通道来传输数据,使用锁或者sync
包中的方法可以控制进程的顺序(有点类似于阶段内随机、总体分阶段进行)。
Goroutine
在go语言中,并发性是一种语言天然支持,简洁而容易实现的。
两种调用
实现一个“go程”,最常见的方法在正常的函数调用之前加上“go”关键字即可:
for i := 0; i < 5; i++ {wg.Add(1)//暂且忽略这一行go work(&wg)}
在go中,无法控制go程执行的先后顺序,也就是说这些go程的先后顺序是随机的。
此外还有一种方法是使用闭包函数的直接调用:
go func() { //使用go 直接将这个函数作为一个goroutine来运行defer fmt.Println("A defer")func() {defer fmt.Println("B defer")runtime.Goexit() //退出这个goroutine//return 退出内层匿名函数fmt.Println("A")}()}()
Go程的常见配套方法
前置defer()
标记结束
func Run() {defer fmt.Println("子进程结束了")for i := 0; i < 10; i++ {fmt.Println("这是子进程的第", i, "个循环")time.Sleep(1 * time.Second)}
}
利用defer的特性,我们可以在go触发进程的函数结束之后,达到某种效果(比如sync.WaitGroup.Done()
)
time.Sleep(?* time.Second)
收尾
goroutine可能的切换点
- I/O,select
- channel
- 等待锁
- 函数调用(有时)
runtime.Gosched()
Channel
channel是一个通道,是用来实现两个进程之间的通信的,本质上是一个队列的数据结构。
channel实现了goroutine两个进程之间的通信
chan2 := make(chan int, 10) make(chan Type, capacity)有缓冲通道
chan1 := make(chan int) //make(chan Type)无缓冲通道
向通道中读写数据:
//向管道中写入数据
channel <- value 发送//从管道中读取数据
<- channel 接收并将其丢弃
x := <- channel 接收并赋值
x , ok := <- channel 接收并赋值,ok为false表示channel已关闭
两种通道的解释
下面两个是刘丹冰老师对于无缓冲通道和有缓冲通道的形象解释
无缓冲通道:
有缓冲通道:
实例:
func test1() {defer fmt.Println("主进程已经结束")//创建一个无缓冲channelc := make(chan int)go func() {defer fmt.Println("子进程已经结束")fmt.Println("正在进行")num := 666fmt.Println("子进程中数据的值为:", num)c <- num}()num := <-c //通过通道将子进程的数据捕捉到主进程fmt.Println("主进程捕捉到的子进程数据: ", num)
}
Lock
锁的使用,包含在sync
包里面,分为互斥锁(Mutex)、读写锁(RWMutex)、等待组(WaitGroup)、一次性锁(Once)和条件变量(Cond)。这是为了解决在Go代码中可能会存在多个goroutine同时操作一个资源(临界区)以及这种情况下发生的竞态问题(数据竞态)。这篇文章只涉及前面两个,后续的在sync包中解释。
互斥锁(Mutex类型)
互斥锁只能被一个goroutine同时持有。如果另一个goroutine试图获取一个已被持有的互斥锁,它将被阻塞,直到持有锁的goroutine释放锁。使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。
案例:
package mainimport ("fmt""sync""time"
)var (sharedValue intmu sync.Mutex // 创建一个互斥锁
)func increment(wg *sync.WaitGroup, id int) {defer wg.Done() // 完成任务时调用 Done()for i := 0; i < 5; i++ { // 为了简化输出,将循环次数减少到5次mu.Lock() // 获取锁fmt.Printf("Go程编号: %d: 将共享变量 sharedValue 的值从 %d 增加\n", id, sharedValue)sharedValue++ // 访问和修改共享变量time.Sleep(100 * time.Millisecond) // 模拟其他工作,增加延迟以便观察输出fmt.Printf("Go程编号: %d: 将共享变量 sharedValue 的值增加到 %d \n", id, sharedValue)mu.Unlock() // 释放锁}
}func main() {var wg sync.WaitGroupwg.Add(2) // 添加两个 goroutine 的等待go increment(&wg, 1) // 创建第一个 goroutine,并传递ID 1go increment(&wg, 2) // 创建第二个 goroutine,并传递ID 2wg.Wait() // 等待所有 goroutine 完成fmt.Printf("最终的共享变量值: %d\n", sharedValue)
}
运行结果:
Go程编号: 2: 将共享变量 sharedValue 的值从 0 增加
Go程编号: 2: 将共享变量 sharedValue 的值增加到 1
Go程编号: 2: 将共享变量 sharedValue 的值从 1 增加
Go程编号: 2: 将共享变量 sharedValue 的值增加到 2
Go程编号: 1: 将共享变量 sharedValue 的值从 2 增加
Go程编号: 1: 将共享变量 sharedValue 的值增加到 3
Go程编号: 1: 将共享变量 sharedValue 的值从 3 增加
Go程编号: 1: 将共享变量 sharedValue 的值增加到 4
Go程编号: 2: 将共享变量 sharedValue 的值从 4 增加
Go程编号: 2: 将共享变量 sharedValue 的值增加到 5
Go程编号: 2: 将共享变量 sharedValue 的值从 5 增加
Go程编号: 2: 将共享变量 sharedValue 的值增加到 6
Go程编号: 1: 将共享变量 sharedValue 的值从 6 增加
Go程编号: 1: 将共享变量 sharedValue 的值增加到 7
Go程编号: 1: 将共享变量 sharedValue 的值从 7 增加
Go程编号: 1: 将共享变量 sharedValue 的值增加到 8
Go程编号: 2: 将共享变量 sharedValue 的值从 8 增加
Go程编号: 2: 将共享变量 sharedValue 的值增加到 9
Go程编号: 1: 将共享变量 sharedValue 的值从 9 增加
Go程编号: 1: 将共享变量 sharedValue 的值增加到 10
最终的共享变量值: 10
这个程序说明了,Lock()
将go程锁住,这时候只有一个进程可以更改变量的值,直到这个进程结束后,才能有其他进程一起竞争这个变量值的使用。
锁的影响范围:
- 在
mu.Lock()
和mu.Unlock()
之间的代码:这些代码块中的所有操作都被保护。只有持有锁的 goroutine 可以执行这些操作。 - 在
mu.Unlock()
之后的代码:锁的释放意味着其他等待的 goroutine 现在可以获取锁并继续执行它们的操作。锁不再影响mu.Unlock()
之后的代码块。
读写互斥锁(RWMutex类型
)
读写锁允许多个goroutine同时读取受保护的数据,但只允许一个goroutine同时写入受保护的数据。
每个进程都可以获得读锁,拿到之后都可以读。但是写锁只有一把,谁拿到谁写。所以很明显,读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
案例如下:
package mainimport ("fmt""sync""time"
)var (sharedValue intrwMutex sync.RWMutex
)// 读操作
func read(id int) {rwMutex.RLock() // 获取读锁fmt.Printf("Goroutine %d: Reading sharedValue: %d\n", id, sharedValue)time.Sleep(100 * time.Millisecond) // 模拟读取操作rwMutex.RUnlock() // 释放读锁
}// 写操作
func write(id int, value int) {rwMutex.Lock() // 获取写锁fmt.Printf("Goroutine %d: Writing sharedValue from %d to %d\n", id, sharedValue, value)sharedValue = valuetime.Sleep(200 * time.Millisecond) // 模拟写操作rwMutex.Unlock() // 释放写锁
}func main() {var wg sync.WaitGroup// 启动多个读操作for i := 1; i <= 5; i++ {wg.Add(1)go func(id int) {defer wg.Done()read(id)}(i)}// 启动多个写操作for i := 1; i <= 3; i++ {wg.Add(1)go func(id int) {defer wg.Done()write(id, id*10)}(i)}// 再启动一些读操作for i := 6; i <= 10; i++ {wg.Add(1)go func(id int) {defer wg.Done()read(id)}(i)}wg.Wait()fmt.Printf("最终共享值为: %d\n", sharedValue)
}
运行结果:
Goroutine 1: Reading sharedValue: 0
Goroutine 2: Reading sharedValue: 0
Goroutine 2: Writing sharedValue from 0 to 20
Goroutine 8: Reading sharedValue: 20
Goroutine 9: Reading sharedValue: 20
Goroutine 4: Reading sharedValue: 20
Goroutine 3: Reading sharedValue: 20
Goroutine 6: Reading sharedValue: 20
Goroutine 7: Reading sharedValue: 20
Goroutine 5: Reading sharedValue: 20
Goroutine 10: Reading sharedValue: 20
Goroutine 1: Writing sharedValue from 20 to 10
Goroutine 3: Writing sharedValue from 10 to 30
最终共享值为: 30
从这里面我们可以看出来,读操作的进程是没有什么先后顺序的,完全随机的(比如89436这几个进程,完全是竞争的关系),而写操作之间也是相互竞争的。但是,我们不难发现,对于读取操作,他们使用的是读锁,因此这个竞争是随机的;但是写锁,很明显是有着先后顺序的(这点从前后值的变化就可以看出),即前面一个写进程结束之后,后面一个写进程才能继续。
后续的三个锁在sync包中有详解。
相关文章:

并发安全与锁
总述 这篇文章,我想谈一谈自己对于并发变成的理解与学习。主要涉及以下三个部分:goroutine,channel以及lock 临界区 首先,要明确下面两组概念 并发和并行 并行:指几个程序每时每刻都同时进行 并发:指…...

细胞分裂检测系统源码分享
细胞分裂检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…...

openssl 生成多域名 多IP 的数字证书
openssl.cnf 文件内容: [req] default_bits 2048 distinguished_name req_distinguished_name copy_extensions copy req_extensions req_ext x509_extensions v3_req prompt no [req_distinguished_name] countryName CN stateOrProvinceName GuangDong l…...

电影评论|基于springBoot的电影评论网站设计与实现(附项目源码+论文+数据库)
私信或留言即免费送开题报告和任务书(可指定任意题目) 目录 一、摘要 二、相关技术 三、系统设计 四、数据库设计 五、核心代码 六、论文参考 七、源码获取: 一、摘要 随着信息技术在管理上越来越深入而广泛的应用,…...

【C++】虚函数
一、什么是虚函数 在类的成员函数前加上virtual关键字,这个函数就是虚函数。 虚函数的所用就是完成多态。多态示例如下: class A {public:virtual void func()//虚函数{cout << "A" << endl;}void ftwo()//普通函数{cout <&…...

esxi虚拟机启用cbt备份(增量备份)
在虚拟机中启用CBT 1.关闭虚拟机。 右键点按虚拟机,Edit Settings-VM Options-Advanced-Configuration Parameters-Edit Configuration- Add parameters,添加ctkEnabled参数,并将其值设置为true。 Add parameters,添加scsi0:0…...

mysql 8.0 时间维度表生成(可运行)
文章目录 mysql 8.0 时间维度表生成实例时间维度表的作用时间维度表生成技术细节使用时间维度表的好处 mysql 8.0 时间维度表生成实例 时间维度表的作用 dim_times(时间维度表)在数据仓库(Data Warehouse)中的作用至关重要。作为…...

打造高效实时数仓,从Hive到OceanBase的经验分享
本文作者:Coolmoon1202,大数据高级工程师,专注于高性能软件架构设计 我们的业务主要围绕出行领域,鉴于初期采用的数据仓库方案面临高延迟、低效率等挑战,我们踏上了探索新数仓解决方案的征途。本文分享了我们在方案筛选…...
15.3 JDBC数据库编程
15.3 JDBC数据库编程 15.3.1 创建数据库和表 创建一个名为webstore的数据库,并向其中添加数据,代码如下: 1.创建数据库 CREATE TABLE products( id int PRIMARY KEY, pname VARCHAR(20) brand VARCHAR(20), price FLOAT(7,2), stock SMALLINT, ) …...

SSH公私钥后门从入门到应急响应
目录 1. SSH公私钥与SSH公私钥后门介绍 1.1 SSH公私钥介绍 1.1.1 公钥和私钥的基本概念 1.1.2 SSH公私钥认证的工作原理(很重要) 1.2 SSH公私钥后门介绍 2. 如何在已拿下控制权限的主机创建后门 2.1 使用 Xshell 生成公钥与私钥 2.2 将公钥上传到被需要被植入后门的服务…...

服务器数据恢复—Linux操作系统环境下网站数据的恢复案例
服务器数据恢复环境: 一台linux操作系统服务器上跑了几十个网站,服务器上只有一块SATA硬盘。 服务器故障: 服务器突然宕机,尝试再次启动失败。将硬盘拆下检测,发现存在坏扇区。找当地一家数据恢复公司处理后ÿ…...

开放式耳机是怎么样的?开放式耳机的优缺点分析?
开放式耳机作为一种独特的耳机类型,因其独特设计和使用体验受到了许多用户的喜爱。了解开放式耳机的优缺点有助于大家更好地选择适合自己需求的耳机。以下是开放式耳机的一些主要优点和缺点分析: 优点 l 舒适度高 开放式耳机的设计通常更加轻盈&#…...

HDMI色块移动——FPGA学习笔记13
一、方块移动原理 二、实验任务 使用FPGA开发板上的HDMI接口在显示器上显示一个不停移动的方块,要求方块移动到边界处时能够改变移动方向。显示分辨率为800*480,刷新速率为90hz。(480p分辨率为800*480,像素时钟频率Vga_clk 800x4…...

MySQL中去除重复
除去相同的行 SELECT DISTINCT 列名 FROM 表名; 示例:查询employees表,显示唯一的部门ID select distinct department_id from employees;...

【C++】vector容器的基本使用
一、vector是什么 vector是STL第一个正式的容器,它的底层其实就是动态数组,插入数据时当容量满了会自动扩容,它和string差不多,不同的之处之一在于vector本身是一个模板,它这个容器中可以存放各种各样的类型的数据&am…...

【强化学习系列】Gym库使用——创建自己的强化学习环境2:拆解官方标准模型源码/规范自定义类+打包自定义环境
目录 一、 官方标准环境的获取与理解 二、根据官方环境源码修改自定义 1.初始化__init__() 2.重置环境 reset() 三、打包环境 1.注册与创建自定义环境 2.环境规范化 在本文的早些时候,曾尝试按照自己的想法搭建自定义的基于gym强化学习环境。 【强化学习系列】Gy…...
PyQt5实现按钮选择文件夹及文件夹
目录 1、选择文件夹并显示 2、选择文件 3、选择多个文件 4、设置保存文件路径 1、选择文件夹并显示 from PyQt5 import QtWidgetsdirectory QtWidgets.QFileDialog.getExistingDirectory(None, "选取文件夹", "./") # 起始路径 print(directory) 2…...

Gin渲染
HTML渲染 【示例1】 首先定义一个存放模板文件的 templates文件夹,然后在其内部按照业务分别定义一个 posts 文件夹和一个 users 文件夹。 posts/index.tmpl {{define "posts/index.tmpl"}} <!DOCTYPE html> <html lang"en">&…...
前端——JS基础
定义变量:let / var 字符串 字符串拼接: 字符串和数字拼:您.... 25 ; 这个25会转成字符串再拼接 字符串和数组拼:10以内的质数有: [2,3,5,7] > 10以内的质数有:2,3,5,7 字符串长度:leng…...

MATLAB入门教程
MATLAB安装教程可参考链接:matlab怎么安装 matlab安装教程-电脑软件-PHP中文网 1.MATLAB的工作环境 (1)命令窗(command window) 是对MATLAB进行操作的主要载体。默认情况下,启动MATLAB时就打开命令窗。MATLAB的所有所数…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...

如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...

企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...
Java线上CPU飙高问题排查全指南
一、引言 在Java应用的线上运行环境中,CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时,通常会导致应用响应缓慢,甚至服务不可用,严重影响用户体验和业务运行。因此,掌握一套科学有效的CPU飙高问题排查方法&…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】,分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...
redis和redission的区别
Redis 和 Redisson 是两个密切相关但又本质不同的技术,它们扮演着完全不同的角色: Redis: 内存数据库/数据结构存储 本质: 它是一个开源的、高性能的、基于内存的 键值存储数据库。它也可以将数据持久化到磁盘。 核心功能: 提供丰…...
c# 局部函数 定义、功能与示例
C# 局部函数:定义、功能与示例 1. 定义与功能 局部函数(Local Function)是嵌套在另一个方法内部的私有方法,仅在包含它的方法内可见。 • 作用:封装仅用于当前方法的逻辑,避免污染类作用域,提升…...
在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7
在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤: 第一步: 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为: // 改为 v…...
Qt Quick Controls模块功能及架构
Qt Quick Controls是Qt Quick的一个附加模块,提供了一套用于构建完整用户界面的UI控件。在Qt 6.0中,这个模块经历了重大重构和改进。 一、主要功能和特点 1. 架构重构 完全重写了底层架构,与Qt Quick更紧密集成 移除了对Qt Widgets的依赖&…...