并发安全与锁

总述

这篇文章,我想谈一谈自己对于并发变成的理解与学习。主要涉及以下三个部分:goroutine,channel以及lock

临界区

首先,要明确下面两组概念

并发和并行

并行:指几个程序每时每刻都同时进行

并发:指在单位时间内同时运行

go的并发模型在内存共享方面有点类型SMP模型:

相似之处:

  1. 多处理器利用
    • SMP:SMP 系统利用多个处理器(或核心)来并行处理任务,系统中的所有处理器共享同一内存空间。
    • Go: Go 通过 goroutines 和调度器来利用多个处理器,能够在多核处理器上并行执行多个 goroutine。Go 的调度器会将 goroutine 分配到可用的处理器上,从而实现并行计算。
  2. 共享内存
    • SMP:所有处理器访问同一内存空间,可以直接共享数据。
    • Go: goroutines 可以通过共享内存进行通信,虽然 Go 提供了通道(channels)作为主要的同步和通信机制,但共享内存仍然是可能的。Go 的同步机制,如互斥锁(mutex)和原子操作,帮助避免数据竞争和保持一致性。

不同之处:

  1. 并发模型
    • SMP:SMP 更多地关注于物理硬件层面的多处理器架构和资源共享,不直接涉及编程模型。
    • Go: Go 语言提供了一种高层次的并发编程模型,通过 goroutines 和 channels 来简化并发编程。Go 的调度器负责将 goroutines 映射到系统线程和处理器上,程序员可以更高效地编写并发程序,而不需要直接管理线程。
  2. 调度和管理
    • SMP:在 SMP 系统中,操作系统负责调度和管理线程,确保线程能够在多个处理器上运行。
    • Go: Go 运行时提供了一个轻量级的调度器,称为 GOMAXPROCS,管理 goroutines 的执行。Go 的调度器将 goroutines 调度到系统线程上,而不是直接由操作系统的线程调度机制来管理。
  3. 编程模型
    • 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包中有详解。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

JVM 一个对象是否已经死亡?

目录 前言 引用计数法 可达性分析法 引用 finalize() 方法区回收 前言 虚拟机中垃圾回收器是掌握对象生死的判官, 只要是垃圾回收器认为需要被回收的, 那么这个对象基本可以宣告"死亡". 但是也不是所有的对象, 都需要被回收, 因此, 我们在学习垃圾回收的时候…

如何用MATLAB计算多边形的几何中心

在MATLAB中&#xff0c;计算多边形的几何中心&#xff08;又称质心或重心&#xff09;可以通过以下步骤实现。假设你有一个多边形&#xff0c;其顶点按照顺时针或逆时针顺序排列在一个矩阵中。具体步骤如下&#xff1a; 定义多边形顶点&#xff1a;首先&#xff0c;你需要将多边…

珠宝首饰检测系统源码分享

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

【时时三省】(C语言基础)指针进阶 例题8

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 第一个打印2 a6不管它是多大 前面是&#xff1d;s 都得变成两个字节 所以打印2 第二个打印5 sizeof里面的表达式是不参与运算的 所以打印5 上面所有例题总结…

36.贪心算法3

1.坏了的计算器&#xff08;medium&#xff09; . - 力扣&#xff08;LeetCode&#xff09; 题目解析 算法原理 代码 class Solution {public int brokenCalc(int startValue, int target) {// 正难则反 贪⼼int ret 0;while (target > startValue) {if (target % 2 0…

gcc/g++的使用:

目录 (1). 程序的翻译过程 预处理&#xff1a; gcc -E 源文件 编译&#xff1a; gcc -S 源文件 汇编&#xff1a;gcc -c 源文件 连接&#xff1a; (2) 语言的自举(也叫 编译器的自举)&#xff1a; (3). 查看可执行程序在连接时依赖的库: ldd 可执行程序的名字 。 (4). …

指针 (六)

OK&#xff0c;书接上回&#xff0c;咱们继续&#xff1a; 一 . 函数指针变量 &#xff08;1&#xff09;函数指针变量的创建 首先我们得明白&#xff0c;什么是函数指针变量呢&#xff1f;从我们之前学习过的整型指针&#xff0c;数组指针的相关知识当中&#xff0c;通过类…

OpenAI API key not working in my React App

题意&#xff1a;OpenAI API 密钥在我的 React 应用中不起作用 问题背景&#xff1a; I am trying to create a chatbot in my react app, and Im not able to generate an LLM powered response. Ive been studying documentation and checking out tutorials but am unable …

基于STM32F407ZGT6——看门狗

独立看门狗 独立看门狗的时钟由独立的RC 振荡器LSI 提供&#xff0c;即使主时钟发生故障它仍然有效&#xff0c;非常独立。 LSI 的频率一般在30~60KHZ 之间&#xff0c;根据温度和工作场合会有一定的漂移&#xff0c; 所以独立看门狗的定时时间并不一定非常精确&#xff0c;只适…

国学盛典 致敬先贤 《老子与道德经》纪录片研讨会在北京善品堂国学馆圆满落幕

9月10日&#xff0c;《老子与道德经》纪录片研讨会在北京善品堂国学馆圆满落幕。中国著名表演艺术家、曾饰演央视86版电视剧《西游记》中“孙悟空”的六小龄童先生与两百余人传统文化传播者、践行者、爱好者齐聚一堂&#xff0c;共同交流。本次会议由中国文化促进会福文化工作委…

【自动驾驶】决策规划算法 | 数学基础(三)直角坐标与自然坐标转换Ⅱ

写在前面&#xff1a; &#x1f31f; 欢迎光临 清流君 的博客小天地&#xff0c;这里是我分享技术与心得的温馨角落。&#x1f4dd; 个人主页&#xff1a;清流君_CSDN博客&#xff0c;期待与您一同探索 移动机器人 领域的无限可能。 &#x1f50d; 本文系 清流君 原创之作&…

GoPlantUML,go代码到类图

前言 GoPlantUML 是一个开源工具&#xff0c;旨在简化从 Go 源代码生成 PlantUML 图的过程。使用 GoPlantUML&#xff0c;开发人员可以毫不费力地可视化其 Go 项目中的结构和关系&#xff0c;从而有助于代码理解和文档编写。通过解析 Go 源代码并生成 PlantUML 图&#xff0c;…

软件安全、逆向分析、加密与解密--crackme2详解

本次使用到的软件有&#xff1a;PEiD、IDA、X32dbg 刚学逆向不久&#xff0c;可能有些地方会有错误&#xff0c;欢迎各位大佬指导 执行 运行程序 点击About 点击确定&#xff0c;输入如图数据 点击try Now 点击确定&#xff0c;回到主界面 点击Exit&#xff0c;退出 查壳&a…

猫头虎分享:Python库 Pandas 的简介、安装、用法详解入门教程

&#x1f42f;猫头虎分享&#xff1a;Python库 Pandas 的简介、安装、用法详解入门教程 摘要 今天猫头虎带大家一起来探讨Python数据分析神器——Pandas的完整入门教程&#xff01;本篇博客将深入介绍Pandas的功能&#xff0c;从安装到基础用法&#xff0c;再到常见问题解决&a…

Python 课程14-TensorFlow

前言 TensorFlow 是由 Google 开发的一个开源深度学习框架&#xff0c;广泛应用于机器学习和人工智能领域。它具有强大的计算能力&#xff0c;能够运行在 CPU、GPU 甚至 TPU 上&#xff0c;适用于从小型模型到大规模生产系统的各种应用场景。通过 TensorFlow&#xff0c;你可以…

FinOps三人行:共话FinOps云成本管理与AI的未来在线分享(文字+视频)

前言&#xff1a; 在数字化浪潮的推动下&#xff0c;云成本管理&#xff08;Cloud Financial Management&#xff0c;简称FinOps&#xff09;正逐渐成为企业关注的焦点。在2024年9月4日&#xff0c;一场关于云成本管理与人工智能&#xff08;AI&#xff09;未来的深入讨论在线…

体感魂斗罗-开篇

文章目录 前言新的目标Flag 前言 黑神话悟空大火&#xff0c;9月14&#xff0c;周鸿祎在抖音平台分享了360团队用两天的业余时间将《黑神话&#xff1a;悟空》爆改为体感游戏的过程&#xff0c;通过身体动作来控制游戏中的角色&#xff0c;实现更加自然和直观的操作方式。 把…

2025年最新大数据毕业设计选题-基于Spark分析相关

选题思路 回忆学过的知识(Python、Java、Hadoop、Hive、Sqoop、Spark、算法等等。。。) 结合学过的知识确定大的方向 a. 确定技术方向&#xff0c;比如基于Hadoop、基于Hive、基于Spark 等等。。。 b. 确定业务方向&#xff0c;比如民宿分析、电商行为分析、天气分析等等。。。…

2025年最新大数据毕业设计选题-基于Hive分析相关

选题思路 回忆学过的知识(Python、Java、Hadoop、Hive、Sqoop、Spark、算法等等。。。) 结合学过的知识确定大的方向 a. 确定技术方向&#xff0c;比如基于Hadoop、基于Hive、基于Spark 等等。。。 b. 确定业务方向&#xff0c;比如民宿分析、电商行为分析、天气分析等等。。。…

【bug】通过lora方式微调sdxl inpainting踩坑

报错内容 ValueError: Attempting to unscale FP16 gradients. 报错位置 if accelerator.sync_gradients:params_to_clip (itertools.chain(unet_lora_parameters, text_lora_parameters_one, text_lora_parameters_two)if args.train_text_encoderelse unet_lora_parameters…