100、Python并发编程:保护临界资源的最简单方式,加锁

引言

前面的文章中已经提到了并发编程中能够带来性能提升的同时,也带来了一些问题,比如对共享资源/临界资源的竞争,可能会导致状态的不一致。最终的结果是虽然性能提升了,但是结果却是错误的……

所以,并发编程中一个更加核心的板块是同步机制。通过同步机制的引入,可以确保临界资源的安全性。

并发编程中,一个最简单的同步机制,就是锁。本文就来简单介绍一下Python中对锁的支持。

本文的主要内容有:

1、为什么要使用锁

2、锁的底层原理

3、Python中的互斥锁

4、Python中的可重入锁

为什么要使用锁

在并发编程中,多个线程可能会同时访问和修改同一个共享资源,也就是临界资源。如果没有对共享资源进行保护,可能会导致数据不一致、数据竞争以及其他难以调试的并发错误。

例如,当多个线程同时读取和修改一个全局变量时,可能会出现不可预知的结果,破坏数据的完整性。

以实际代码为例:

from threading import Thread
import time

lines = 0


def code(name, n):
    global lines
    print(f'{name}开始写代码')
    for i in range(n):
        lines += 1
    print(f'{name}代码编写完成')


if __name__ == '__main__':
    frontend_coder = Thread(target=code, args=('前端', 1000000))
    backend_coder = Thread(target=code, args=('后端', 1000000))
    start_time = time.time()
    frontend_coder.start()
    backend_coder.start()
    frontend_coder.join()
    backend_coder.join()
    print(f'总共写了{lines}行代码, 共计耗时{time.time() - start_time}秒')

使用Python3.11执行的结果:

eae80d1e1a80f5961ba1ed6d3e96932f.jpeg

使用Python3.13移除了GIL的解释器执行的结果:

7c11e75ffd72959b64492fd10bae8b40.jpeg

通过对比,有以下内容需要说明:

1、Python中由于GIL的存在,在一定程度上能够保证线程的安全。但是,GIL只是保证的同一个时刻只有一个线程执行字节码指令,并没有承诺线程安全,所以,在涉及到字节码指令行数的设定、IO操作等,还是会导致线程安全性的问题,也就是GIL并不能保证线程安全。

2、Python3.13中,我们使用了移除了GIL的实验特性,很容易就触发了线程安全性的问题,本来应该对全局变量累加到2000000的,结果却是一个错误的数字。

3、需要特别说明的是,在使用了移除GIL的实验特性中,程序执行的性能反而有所下降。因为,该演示代码计算量比较小,反而线程上下文频繁切换的开销,占据了大量的CPU时间。

从上面代码的演示中,可以看到,应该尽量少用全局变量在多线程中进行共享。如果确实需要,应该考虑使用相关同步机制来保证线程安全性。

锁的底层原理

锁是一种同步机制,通常在操作系统的内核层面实现,通常使用原子操作来确保锁的获取和释放。这点应该很好理解,使用锁本身就是要保证临界区执行的原子性、隔离性。如果锁本身不能保证原子性,则锁本身也是存在线程安全问题的,又怎么能够解决线程安全的问题呢。

当一个线程尝试获取一个锁时,如果锁是可用的,线程将成功获得它并进入临界区。如果锁不可用,则线程将被阻塞,直至锁被释放。

所以,从本质上来说,所有的同步机制,都是将对临界区的执行从并行化转变化串行化,从而保证线程安全。所以,临界区应当尽量小,否则,并发编程不仅没有实现并行提效,反而变成了完全串行执行。

锁的底层实现中,一般会涉及到如下几个关键概念,需要说明的是这几个概念与编程语言无关,更多的是操作系统内核层面的设计:

1、原子操作:要确保锁的操作是不可中断的。

2、上下文切换:当线程被阻塞时,通常应当切换执行其他线程。

3、自旋锁:由于上下文切换存在相应的性能开销,有些场景中,需要实现短期低开销的锁,这种锁不会导致上下文切换,而是通过持续检查锁直至锁可用,适合临界区非常短的场景。

在Python多线程编程中,对于锁的支持,主要涉及到两种锁:互斥锁和可重入锁,接下来分别来看下这两种锁的使用。

Python中的互斥锁

互斥锁,是一种最简单的锁,在threading模块中,直接使用Lock即可完成互斥锁的创建。

以下是互斥锁的简单说明:

9eefbd1bc6064fe625cf39c072fc28ea.jpeg

可以看到,关于互斥锁主要有3个常用的方法:
1、acquire()方法:用于申请获取锁,如果锁已经被别的线程获取并没有被释放的话,则当前现成阻塞。

2、release()方法:用于释放该锁对象。

3、locked()方法:用于测试锁对象当前的状态。

下面通过代码验证下互斥锁的使用。

from threading import Thread, Lock
import time

lines = 0

lock = Lock()


def code(name, n):
    global lines
    global lock
    print(f'{name}开始写代码')
    for i in range(n):
        lock.acquire()
        lines += 1
        lock.release()
    print(f'{name}代码编写完成')


if __name__ == '__main__':
    frontend_coder = Thread(target=code, args=('前端', 1000000))
    backend_coder = Thread(target=code, args=('后端', 1000000))
    start_time = time.time()
    frontend_coder.start()
    backend_coder.start()
    frontend_coder.join()
    backend_coder.join()
    print(f'总共写了{lines}行代码, 共计耗时{time.time() - start_time}秒')

通过Python3.13t执行的结果:

477f0cb6d56998836e69270a483313a6.jpeg

可以看到,加上锁之后,执行的更加慢了……

需要说明的是,互斥锁同一个线程中,只能申请一次,释放之后才能再次申请,否则就会导致死锁。所以,在某些场景中,互斥锁就不能满足需求了,这时就需要可重入锁了。

Python中的可重入锁

针对一些复杂场景,同一个现成需要多次获取锁,这时候,互斥锁的多次申请会导致死锁,所以,引出了可重入锁——RLock。

可重入锁的可能会在以下场景中被用到:

1、递归函数

递归函数中,函数体可能会多次进入同一个临界区,使用互斥锁会导致死锁,而使用可重入锁可以避免这种情况。

2、多个方法需要共用锁

在复杂的类设计中,多个方法可能需要同一个锁,如果这些方法在同一个线程中互相调用,使用互斥锁也会导致死锁。

3、拦截器或者装饰器模式

在某些设计模式中,比如拦截器或者装饰器模式中,方法调用可能会导致同一个线程多次请求同一个锁。可重入锁可以使得这些模式在多线程环境中依然能够平稳运行。

总的来说,可重入锁的优势在于当同一个线程多次进入同一临界区时不会导致死锁,非常适合复杂的用户场景,了解这些适用场景,有助于更好地选择适合的锁类型,从而确保逻辑正确和线程安全。

以下是Python中对可重入锁的支持:

c817b8609b8ea340bacb6b3cc1f48193.jpeg

可重入锁内部维持了一个owner属性和count属性,用于保存当前锁的所属线程,以及重入次数。关于可重入锁的实现,可以通过以下几点简化的描述来有个整体上的理解:

1、拥有者线程标识:用于记录当前持有该锁的线程,只有持有该锁的线程才能递归地再次获取该锁。

2、计数器管理:记录当前线程获取该锁的次数,每获取一次,计数器+1,每释放一次,计数器-1。计数器归零之后,锁才被真正释放,从而允许其他线程获取。

3、底层基础锁Lock:使用一个基本的互斥锁来实现实际的锁定和解锁操作,控制锁资源的管理。

简单来说,RLock = Lock + Owner + Counter。

下面,通过代码示例来演示RLock的使用:

from threading import Thread, RLock
import random


class Project:
    def __init__(self):
        self.lock = RLock()
        self.lines = 0

    def code(self):
        for i in range(5):
            with self.lock:
                print('普通模式,一行一行写代码')
                self.lines += 1
                if random.randint(0, 10) % 3 == 0:
                    print('进入心流模式,生产力提升')
                    self.flow()

    def flow(self):
        with self.lock:
            self.lines += 100


if __name__ == '__main__':
    proj = Project()
    coders = []
    for _ in range(3):
        coder = Thread(target=proj.code)
        coders.append(coder)
        coder.start()
    for coder in coders:
        coder.join()
    print(f'总共写了{proj.lines}行代码')

执行结果:

e428bc0da1a583b4cf0579996e060357.jpeg

下面对Lock和RLock的使用做一个简单的总结:

1、Lock、RLock的使用,需要注意acquire()和release()方法,要注意成对的调用,否则会导致死锁。

2、由于锁对象实现了__enter__()方法和__exit__()方法,所以,可以通过with语法块进行上下文的管理,从而简化锁对象的申请和释放。

3、通常场景中,可以使用简单的互斥锁来满足常规的需求。如果涉及到复杂的场景,可能在同一个线程中多次申请同一个锁,则需要使用可重入锁。

3、可重入锁是基于普通的互斥锁来实现的,通过维持一个互斥锁对象来保证基本的锁资源管理,通过维持一个owner属性来管理锁的持有者线程标识,通过维持一个counter计数器属性来标识当前持有者进程重入的次数。

总结

本文简单介绍了使用锁的必要性、锁的底层实现原理,以及Python中关于互斥锁、可重入锁的支持,并通过具体的代码示例,演示了两种的锁的基本使用。

以上就是本文的全部内容,感谢您的拨冗阅读。

7f4daf97a0cfcd6e9691dbcf45cf8dfd.jpeg

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

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

相关文章

PHP电商供应链ERP管理系统小程序源码

🚀电商供应链大揭秘!ERP管理系统如何重塑你的商业版图✨ 🔍 什么是电商供应链ERP管理系统? 电商供应链ERP管理系统是一款基于FastAdminThinkPHP开发的系统。该系统可满足电商企业管理自身进销存,帮助中小型电商企业管…

python: Parent-child form operations using ttkbootstrap

# encoding: utf-8 # 版權所有 2024 ©塗聚文有限公司 # 許可資訊查看:言語成了邀功的功臣,還需要行爲每日來值班嗎? # 描述: 主、子表單 窗體傳值 Parent-child form operations # Author : geovindu,Geovin Du 塗聚文. …

跳表原理笔记

课程地址 跳表是一种基于随机化的有序数据结构,它提出是为了赋予有序单链表以 O(logn) 的快速查找和插入的能力 创建 首先在头部创建一个 sentinel 节点,然后在 L1 层采用“抛硬币”的方式来决定 L0 层的指针是否增长到 L1 层 例如上图中,L…

医院信息化与智能化系统(17)

医院信息化与智能化系统(17) 这里只描述对应过程,和可能遇到的问题及解决办法以及对应的参考链接,并不会直接每一步详细配置 如果你想通过文字描述或代码画流程图,可以试试PlantUML,告诉GPT你的文件结构,让他给你对应…

selenium操作已开启的浏览器,方便调试

一、谷歌浏览器配置: 在所安装的谷歌下面,执行下面命令,打开谷歌浏览器,用来selenium的操作: 注意事项:端口需要不被占用,--user-data-dir"D:\workspace\chrome-data"这个路径需要有…

深度强化学习:从理论到应用

目录 1.引言 2.什么是强化学习? 3.深度学习和强化学习的结合 4.深度强化学习的主要方法 5.深度强化学习的应用领域 6.深度强化学习的挑战与未来 7.总结 1.引言 深度强化学习(Deep Reinforcement Learning,DRL)是近年来人工…

[Linux] 进程控制之创建和终止

🪐🪐🪐欢迎来到程序员餐厅💫💫💫 主厨:邪王真眼 主厨的主页:Chef‘s blog 所属专栏:青果大战linux 总有光环在陨落,总有新星在闪烁 每日吐槽 不得不说&a…

介绍一下rand函数生成随机数(c基础)

适合对象 c语言初学者 总结语言用色&#xff0c;个人强调用红色&#xff0c;注意为易错点&#xff0c;若有问题请告诉我谢谢。(建议通过目录观看)。一定要自己动手打代码。 rand函数 是生成随机数的函数&#xff0c;但实则是伪随机数。(即是同一个值) 格式 #include<st…

vue3入门知识(一)

vue3简介 性能的提升 打包大小减少41%初次渲染快55%&#xff0c;更新渲染快133%内存减少54% 源码的升级 使用Proxy代替defineProperty实现响应式重写虚拟DOM的实现和Tree-Shaking 新的特性 1. Composition API&#xff08;组合API&#xff09; setupref与reactivecomput…

FET113i-S核心板已支持RISC-V,打造国产化降本的更优解 -飞凌嵌入式

FET113i-S核心板是飞凌嵌入式基于全志T113-i处理器设计的国产工业级核心板&#xff0c;凭借卓越的稳定性和超高性价比&#xff0c;FET113i-S核心板得到了客户朋友们的广泛关注。作为一款拥有A7核RISC-V核DSP核的多核异构架构芯片&#xff0c;全志科技于近期释放了T113-i的RISC-…

C语言进阶:二.数据的存储(2)

❤个人主页❤&#xff1a;折枝寄北-CSDN博客 ❤学习专栏❤&#xff1a; C语言专栏&#xff1a;https://blog.csdn.net/2303_80170533/category_12794764.html?spm1001.2014.3001.5482https://blog.csdn.net/2303_80170533/category_12794764.html?spm1001.2014.3001.5482 在…

城市智慧公厕解决方案,建设城市智能化公厕

在城市的飞速发展进程中&#xff0c;公厕作为城市基础设施的重要一环&#xff0c;其智能化建设已成为提升城市品质与居民生活舒适度的关键举措。以下是关于城市智慧公厕的几点解决方案。 一、智能设施配备 首先&#xff0c;要引入智能化的卫生设备。例如&#xff0c;安装自动感…

【STL栈和队列】:高效数据结构的应用秘籍

前言&#xff1a; C 标准模板库&#xff08;STL&#xff09;为我们提供了多种容器&#xff0c;其中 stack&#xff08;栈&#xff09;和 queue&#xff08;队列&#xff09;是非常常用的两种容器。 根据之前C语言实现的栈和队列&#xff0c;&#xff08;如有遗忘&#xff0c;…

vue data变量之间相互赋值或进行数据联动

摘要&#xff1a; 使用vue时开发会用到data中是数据是相互驱动&#xff0c;经常会想到watch,computed&#xff0c;总结一下&#xff01; 直接赋值&#xff1a; 在 data 函数中定义的变量可以直接在方法中进行赋值。 export default {data() {return {a: 1,b: 2};},methods: {u…

HTML 基础标签——分组标签 <div>、<span> 和基础语义容器

文章目录 1. `<div>` 标签特点用途示例2. `<span>` 标签特点用途示例3. `<fieldset>` 标签特点用途示例4. `<section>` 标签特点用途示例5. `<article>` 标签特点用途示例总结HTML中的分组(容器)标签用于结构化内容,将页面元素组织成逻辑区域…

Java开发配置文件的详情教程配置文件类型

学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……&#xff09; 2、学会Oracle数据库入门到入土用法(创作中……&#xff09; 3、手把手教你开发炫酷的vbs脚本制作(完善中……&#xff09; 4、牛逼哄哄的 IDEA编程利器技巧(编写中……&#xff09; 5、面经吐血整理的 面试技…

全面解析:网络协议及其应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 全面解析&#xff1a;网络协议及其应用 全面解析&#xff1a;网络协议及其应用 全面解析&#xff1a;网络协议及其应用 网络协议…

软件压力测试有多重要?北京软件测试公司有哪些?

软件压力测试是一种基本的质量保证行为&#xff0c;它是每个重要软件测试工作的一部分。压力测试是给软件不断加压&#xff0c;强制其在极限的情况下运行&#xff0c;观察它可以运行到何种程度&#xff0c;从而发现性能缺陷。 在数字化时代&#xff0c;用户对软件性能的要求越…

聊一聊Qt中的Slider和ProgressBar

目录 QAbstractSilder 主要属性 设置值 信号 其他功能 API QSlider 主要功能 控制刻度 信号 用户交互 键盘操作 API QProgressBar API QScrollBar 详细描述 QDial API 一个示例 Slider和ProgressBar从某种程度上都是反应了自己对目标控件的进度状态。在Qt中…

源鲁杯 2024 web(部分)

[Round 1] Disal F12查看: f1ag_is_here.php 又F12可以发现图片提到了robots 访问robots.txt 得到flag.php<?php show_source(__FILE__); include("flag_is_so_beautiful.php"); $a$_POST[a]; $keypreg_match(/[a-zA-Z]{6}/,$a); $b$_REQUEST[b];if($a>99999…