并发安全与锁

news2024/9/21 21:45:08

总述

这篇文章,我想谈一谈自己对于并发变成的理解与学习。主要涉及以下三个部分: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("主进程已经结束")
	//创建一个无缓冲channel
	c := make(chan int)

	go func() {
		defer fmt.Println("子进程已经结束")
		fmt.Println("正在进行")
		num := 666
		fmt.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 main

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

var (
	sharedValue int
	mu          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.WaitGroup

	wg.Add(2)            // 添加两个 goroutine 的等待
	go increment(&wg, 1) // 创建第一个 goroutine,并传递ID 1
	go increment(&wg, 2) // 创建第二个 goroutine,并传递ID 2

	wg.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 main

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

var (
	sharedValue int
	rwMutex     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 = value
	time.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.coloradmin.cn/o/2142682.html

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

相关文章

lnmp - 登录技术方案设计与实现

概述 登录功能是对于每个动态系统来说都是非常基础的功能&#xff0c;用以区别用户身份、和对应的权限和信息&#xff0c;设计出一套安全的登录方案尤为重要&#xff0c;接下来我介绍一下常见的认证机制的登录设计方案。 方案设计 HTTP 是一种无状态的协议&#xff0c;客户端…

iOS - TestFlight使用

做的项目需要给外部人员演示&#xff0c;但是不方便获取对方设备的UDID&#xff0c;于是采用TestFlight 的方式邀请外部测试人员的方式给对方安装测试App&#xff0c;如果方便获取对方设备的UDID&#xff0c;可以使用蒲公英 1.在Xcode中Archive完成后上传App Store Connect之前…

浙大上交联合阿里腾讯,共同构建医学AI领域的顶尖科研+商业团队|个人观点·24-09-17

小罗碎碎念 昨晚锻炼时&#xff0c;我想着是时候对推文的内容做一些改进了——既能通过写推文来锻炼自己写paper的能力&#xff0c;也希望凭借自己一点微弱的影响力&#xff0c;去带动更多的人加入医学AI的队伍中。 这一期推文系统且深度的分析一下&#xff0c;国内哪些学者在医…

Linux基础开发环境(git的使用)

1.账号注册 git 只是一个工具&#xff0c;要想实现便捷的代码管理&#xff0c;就需要借助第三方平台进行操作&#xff0c;当然第三平台也是基于git 开发的 github 与 gitee 代码托管平台有很多&#xff0c;这里我们首选 Github &#xff0c;理由很简单&#xff0c;全球开发者…

算法题之回文子串

回文子串 给你一个字符串 s &#xff0c;请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。 示例 1&#xff1a; 输入&#xff1a;s "abc" 输出&#xff1a;3 解释…

C++ 带约束的Ceres形状拟合

C 带约束的Ceres形状拟合 一、Ceres Solver1.定义问题2. 添加残差AddResidualBlockAutoDiffCostFunction 3. 配置求解器4. 求解5. 检查结果 二、基于Ceres的最佳拟合残差结构体拟合主函数 三、带约束的Ceres拟合残差设计拟合区间限定 四、拟合结果bestminmax 五、完整代码 对Ce…

RocksDB系列一:基本概念

0 引言 RocksDB 是 Facebook 基于 Google 的 LevelDB 代码库于 2012 年创建的高性能持久化键值存储引擎。它针对 SSD 的特定特性进行了优化&#xff0c;目标是大规模&#xff08;分布式&#xff09;应用&#xff0c;并被设计为嵌入在更高层次应用中的库组件。RocksDB应用范围很…

【Python百日进阶-Web开发-音频】Day711 - 光谱表示 librosa.stft 短时傅里叶变换

文章目录 一、光谱表示 Spectral representations1.1 librosa.stft1.1.1 语法与参数1.1.2 示例 一、光谱表示 Spectral representations 1.1 librosa.stft https://librosa.org/doc/latest/generated/librosa.stft.html 1.1.1 语法与参数 librosa.stft(y, *, n_fft2048, ho…

智能机巢+无人机:自动化巡检技术详解

智能机巢与无人机的结合&#xff0c;在自动化巡检领域展现出了巨大的潜力和优势。以下是对这一技术的详细解析&#xff1a; 一、智能机巢概述 智能机巢&#xff0c;也被称为无人机机场或无人机机巢&#xff0c;是专门为无人机提供停靠、充电、维护等服务的智能化设施。它不仅…

加密与安全_优雅存储二要素(AES-256-GCM )

文章目录 什么是二要素如何保护二要素&#xff08;姓名和身份证&#xff09;加密算法分类场景选择算法选择AES - ECB 模式 (不推荐)AES - CBC 模式 (推荐)GCM&#xff08;Galois/Counter Mode&#xff09;AES-256-GCM简介AES-256-GCM工作原理安全优势 应用场景其他模式 和 敏感…

LeetcodeTop100 刷题总结(一)

LeetCode 热题 100&#xff1a;https://leetcode.cn/studyplan/top-100-liked/ 文章目录 一、哈希1. 两数之和49. 字母异位词分组128. 最长连续序列 二、双指针283. 移动零11. 盛水最多的容器15. 三数之和42. 接雨水&#xff08;待完成&#xff09; 三、滑动窗口3. 无重复字符的…

TCADE--基于迁移成分分析和差分进化的多目标多任务优化

TCADE–基于迁移成分分析和差分进化的多目标多任务优化 title&#xff1a; Multitasking multiobjective optimization based on transfer component analysis author&#xff1a; Ziyu Hua, Yulin Li, Hao Sun, Xuemin Ma. journal&#xff1a; Information Sciences (Ins)…

最优化理论与自动驾驶(十):纯跟踪算法原理、公式及代码演示

纯跟踪算法&#xff08;Pure Pursuit Algorithm&#xff09;是一种用于路径跟踪的几何控制算法&#xff0c;广泛应用于自动驾驶、机器人导航等领域。其基本思想是通过选择预定路径上的目标点&#xff08;预瞄点&#xff09;&#xff0c;并控制转向角&#xff0c;使车辆不断逼近…

用于稀疏自适应深度细化的掩码空间传播网络 CVPR2024

目录 Masked Spatial Propagation Network for Sparsity-Adaptive Depth Refinement &#xff08;CVPR 2024&#xff09;用于稀疏自适应深度细化的掩码空间传播网络1 介绍2 算法流程2.1 问题建模2.2 Guidance Network2.3 MSPN 模块 3 实验结果3.1 稀疏度自适应深度细化对比试验…

COMDEL电源CX2500S RF13.56MHZ RF GENERATOR手侧

COMDEL电源CX2500S RF13.56MHZ RF GENERATOR手侧

如何让虚拟机的服务被主机访问

当我们在虚拟机上写了一个服务器&#xff0c;在宿主机访问时&#xff0c;出现无法访问的情况。这可能是虚拟机网络的设置问题。 查看虚拟机防火墙是否关闭 在终端输入&#xff1a; systemctl status firewalld 如果是active就说明防火墙是开启的&#xff0c;需要关闭。 输入…

高级I/O知识分享【epoll || Reactor ET,LT模式】

博客主页&#xff1a;花果山~程序猿-CSDN博客 文章分栏&#xff1a;Linux_花果山~程序猿的博客-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能吧&#xff01;让我们一起努力&#xff0c;一起成长&#xff01; 目录 一&#xff0c;接口 epo…

SpringBoot 消息队列RabbitMQ 消息可靠性 数据持久化 与 LazyQueue

介绍 在默认情况下&#xff0c;RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟 一旦MO宕机&#xff0c;内存中的消息会丢失内存空间有限&#xff0c;当消费者故障或处理过慢时&#xff0c;会导致消息积压&#xff0c;引发MQ阻塞 在消息队列运行的过程中&#xf…

LeetCode 815.公交路线(BFS广搜 + 建图)(中秋快乐啊)

给你一个数组 routes &#xff0c;表示一系列公交线路&#xff0c;其中每个 routes[i] 表示一条公交线路&#xff0c;第 i 辆公交车将会在上面循环行驶。 例如&#xff0c;路线 routes[0] [1, 5, 7] 表示第 0 辆公交车会一直按序列 1 -> 5 -> 7 -> 1 -> 5 -> …

物理感知扩散的 3D 分子生成模型 - PIDiff 评测

PIDiff 是一个针对蛋白质口袋特异性的、物理感知扩散的 3D 分子生成模型&#xff0c;通过考虑蛋白质-配体结合的物理化学原理来生成分子&#xff0c;在原理上&#xff0c;生成的分子可以实现蛋白-小分子的自由能最小。 一、背景介绍 PIDiff 来源于延世大学计算机科学系的 Sang…