Python并发编程之多线程

 前言

本文介绍并发编程中另一个重要的知识 - 线程。

线程介绍

我们知道一个程序的运行过程是一个进程,在操作系统中每个进程都有一个地址空间,而且每个进程默认有一个控制线程,打个比方,在一个车间中有很多原材料通过流水线加工产品,而线程就是这个车间中的流水线,而这个车间就是进程,原材料就是内存中的数据,每个车间至少有一条流水线。

因此,进程只是将资源(原材料)集中到一起是资源单位,线程才是CPU具体的执行单位,一个进程中可以存在多个线程,这多个线程会共享该进程内的所有资源,因此同一个进程内开启多线程会产生资源争抢。

为了单核实现并发已经有了多进程为何还要有多线程呢?进程相当于一个一个的车间,创建一个进程就需要创建一个车间,而线程是车间中的流水线,创建一个线程只是在车间中创建一条流水线无需申请另外的内存空间,创建开销相对进程来说小很多。

开启线程 - threading模块

开启线程和开启进程的方式基本一致,只是使用的模块不同,并且在开多线程时无需在if __name__ == '__main__':下开设,但是约定俗成还是建议在if __name__ == '__main__':分支下开设线程。

from threading import Threaddef task():print('i am sub_thread')# 方式1:直接使用Thread实例化线程对象
if __name__ == '__main__':t = Thread(target=task)t.start()print('i am main-thread')# 方式2:继承Therad,自定自己的线程类,重写run方法
class MyThread(Thread):def run(self):task()if __name__ == '__main__':t = MyThread()t.start()print('i am main-thread')

有了开启多线程的方式,针对之前学习的socket就可以实现TCP服务端的并发:

import socket
from threading import Threadserver = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)# 线程的任务:通信循环
def task(conn):while 1:try:data = conn.recv(1024)if not data: breakconn.send(data.upper())except Exception as e:print(e)breakconn.close()while True:conn, addr = server.accept()t = Thread(target=task, args=(conn, ))	# 来一个连接请求,就开一个线程处理这个连接t.start()								# 这种并发方式有缺陷:容易内存溢出(线程个数有限)

join方法

多线程中也有join方法,主线程等待子线程执行结束才执行主线程

from threading import Thread
import timeclass MyThread(Thread):def __init__(self,name):super().__init__()self.name = namedef run(self):print(f'{self.name} is run')time.sleep(2)print(f'{self.name} is over')if __name__ == '__main__':t = MyThread('tank')t.start()t.join()print('主')

多线程共享数据

我们知道多个进程之间的数据是不会共享的,但是线程是开设在进程内,一个进程内如果有多个线程,那么这多个线程就会共享该进程内的所有资源数据,如下

from threading import Threadmoney = 100
def foo():global moneymoney = 666print('子',money)if __name__ == '__main__':t = Thread(target=foo)t.start()print('主',money)# 程序运行结果
子 666
主 666

守护线程

主线程运行结束后不会立刻结束,会等待进程中的其他子线程全部运行完毕后才会结束,因为主线程结束就意味着所在的进程结束,进程结束的话内存空间就会被回收,那么其他子线程就无法工作了。因此如果子线程不是守护线程,主线程结束后会等待子线程运行完毕,但是如果子线程属于守护线程,那么主线程结束,守护线程也结束。

from threading import Thread
import timedef foo():print('太监逍遥自在!')time.sleep(3)print('老子寿终正寝')if __name__ == '__main__':t = Thread(target=foo)t.daemon = Truet.start()print('吾主驾崩')# 运行结果,表示太监没有寿终正寝
太监逍遥自在!
吾主驾崩

线程互斥锁

多线程共享统一进程内的资源,因此就会发生数据争抢,造成数据错乱,为了防止这一现象的发生,可以进行加锁处理,让多个线程进行抢锁,比如:

from threading import Thread, Lock
import timemoney = 100
mutex = Lock()  # 实例化得到锁# 线程需要执行的任务
def task():global moneymutex.acquire()  # 线程获取锁tmp = moneytime.sleep(0.01)money = tmp - 1mutex.release()  # 任务执行完成后释放锁,由其他线程继续争抢if __name__ == '__main__':t_list = []for i in range(100):t = Thread(target=task)t.start()t_list.append(t)for t in t_list:t.join()  # 等待子线程完成后才运行主线程print(money)

死锁和递归锁

我们在说进程加锁的时候有提到过,锁不能轻易使用,容易出现死锁现象,不管是进程加锁还是线程加锁都容易出现死锁,所谓死锁就是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁:

from threading import Thread,Lock
import time
mutexA = Lock()
mutexB = Lock()class MyThread(Thread):def run(self):self.func1()self.func2()def func1(self):mutexA.acquire()print(f'{self.name}抢到A锁')# self.name获取当前线程的名字mutexB.acquire()print(f'{self.name}抢到B锁')mutexB.release()mutexA.release()def func2(self):mutexB.acquire()print(f'{self.name}抢到B锁')time.sleep(2)mutexA.acquire()print(f'{self.name}抢到A锁')mutexA.release()mutexB.release()if __name__ == '__main__':for i in range(10):t = MyThread()t.start()# 运行结果
Thread-1抢到A锁
Thread-1抢到B锁
Thread-1抢到B锁
Thread-2抢到A锁
....这里阻塞住了,出现死锁现象

可以对上述执行结果进行分析:

1.共有10个线程开启,开启后会自动执行run() 2.首先执行func1功能 3.执行func1,10个线程中会有一个首先抢到A锁,另外的9个线程需要等A锁释放才能争抢A锁 4.抢到A锁的线程会很顺利的抢到B锁,然后依次释放B锁和B锁 5.A锁释放完毕后,第一个线程就可以执行到func2,再次抢到B锁,与此同时其他的9个线程执行func1时,又会有一个线程抢到A锁 6.第二个线程拿着A锁想要抢到B锁,而此时正在执行func2的线程拿着B锁想要抢到A锁 7.由此产生了死锁现象

为了解决死锁的问题,我们可以使用递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

导入RLock
将两把锁变为一把锁:mutexA = Lock()mutexB = Lock()# 换成mutexA = mutexB = RLock()

GIL

python解释器有多个版本,比如Cpython/Jpython/Pypython,经常使用的版本就是Cpython,GIL(全局解释器锁)就是Cpython解释器中的一把互斥锁,GIL的作用就是用来阻止同一进程下多个线程的同时执行。

GIL存在的原因是Cpython的内存管理不是线程安全的。程序的代码需要交给解释器解释执行,由于进程中所有线程是共享该进程内的资源,这就有了竞争,而垃圾回收机制也是当前进程中的一个线程,这个线程会和当前进程内的其他线程争抢数据,为了保证数据安全,进程内同一时间只有一个线程在运行就有了GIL。

可能有小伙伴会问,python既然有了GIL保证同一时间只有一个线程运行,为什么还有互斥锁呢?首先需要知道加锁的目的是为了保护数据,同一时间只能有一个线程修改共享的数据,而保护不同的数据应该加不同的锁,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock。

锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁。

可能又有小伙伴问了,既然加锁会让运行变成串行,那么我在start之后立即使用join,不用加锁也是串行的效果,为什么还要用锁呢?在start之后立刻使用jion,肯定会将多个任务的执行变成串行,是安全的,但问题是start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的,单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.比如下述代码用来验证join方式的效率,可以发现最后执行时间非常久。

from threading import current_thread,Thread,Lock
import os,time
def task():time.sleep(3)print('%s start to run' %current_thread().getName())global ntemp=ntime.sleep(0.5)n=temp-1if __name__ == '__main__':n=100lock=Lock()start_time=time.time()for i in range(100):t=Thread(target=task)t.start()t.join()stop_time=time.time()print('主:%s n:%s' %(stop_time-start_time,n))'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 # 耗时会非常久
'''

python多线程的应用

我们知道由于GIL的原因,Cpython无法利用计算机的多核优势,是不是就意味着Cpython没有用了呢?想要回答这个问题需要明确CPU到底是用来做计算的还是用来做IO操作的,多个CPU意味着可以有多个核并行完成计算,因此多核可以提升计算速度,但是每个CPU一旦遇到IO阻塞仍然需要等待,此时多核CPU也没什么用。

举例来说,一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。 如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活, 反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高。

因此对于计算密集型的程序来说肯定是CPU越多越好,但是对于IO密集型的程序来讲再多的CPU也没用。

 最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你! 

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

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

相关文章

MYSQL分区NOW()不支持

传说同事写个复杂的SQL代码,跑一次需要7-10秒, 复杂如上,我也懒得去分析 IF IF IF是怎么回事了! 发现此表是分区表,后面要求加上了分区时间,以便利用到分区裁剪技术. 因为需求是查近10天来到期还款的人和金额.就是今天应该还款的人, 一般还款周期是7天. 给个10天的范围挺可以的…

第3集《佛说四十二章经》

和尚尼慈悲、诸位法师、诸位同学,阿弥陀佛! 请大家打开讲议第四面,三、随文释义。 前面讲到本经的修学纲要是顿渐兼收,理事无碍。本经的修学有两个主题: (一)顿教法门: 顿教法门是一种智慧的观照。修学…

腾讯云4核8G服务器多少钱一年?

腾讯云4核8G服务器S5和轻量应用服务器优惠价格表,轻量应用服务器和CVM云服务器均有活动,云服务器CVM标准型S5实例4核8G配置价格15个月1437.3元,5年6490.44元,标准型SA2服务器1444.8元一年,轻量应用服务器4核8G12M带宽一…

C++:理解拷贝在变量,指针,引用以及构造函数里的意义

变量,指针,引用 //拷贝与拷贝构造函数 //拷贝(copy):拷贝数据,拷贝内存 //始终是在拷贝值,但是指针存储的是内存的地址,变量存储的是数据的值 //特别注意,在引用里面的拷…

新年加载中特效 —— 后期需要添加备注和消化

代码来源&#xff1a;链接: https://www.bilibili.com/video/BV1qA4m1573V/?spm_id_from333.880.my_history.page.click&vd_sourceb91967c499b23106586d7aa35af46413 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8&…

数据库管理-第14期 Oracle Vector DB AI-01(20240210)

数据库管理149期 2024-02-10 数据库管理-第149期 Oracle Vector DB & AI-01&#xff08;20240210&#xff09;1 机器学习2 向量3 向量嵌入4 向量检索5 向量数据库5 专用向量数据库的问题总结 数据库管理-第149期 Oracle Vector DB & AI-01&#xff08;20240210&#xf…

ChatGPT高效提问—prompt常见用法(续篇十一)

ChatGPT高效提问—prompt常见用法(续篇十一) 1.1 增加角色 ​ 在prompt里可以适当增加角色,来满足一些特殊场景的需求。先来看一个不带角色的简单示例。 输入prompt: ​ ChatGPT输出: ​ 如上所示,问题比较难,ChatGPT的答案也确实晦涩难懂。试想一下,如果将这个解释将…

猫头虎分享已解决Bug || 日志文件过大(Log File Oversize):LogFileOverflow, ExcessiveLoggingError

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

Java:Arrays类、Lambda表达式、JDK新特性(方法引用) --黑马笔记

一、Arrays类 1.1 Arrays基本使用 Arrays是操作数组的工具类&#xff0c;它可以很方便的对数组中的元素进行遍历、拷贝、排序等操作。 下面我们用代码来演示一下&#xff1a;遍历、拷贝、排序等操作。需要用到的方法如下&#xff1a; public class ArraysTest1 {public stat…

【机器学习】数据清洗之识别异常点

&#x1f388;个人主页&#xff1a;甜美的江 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;机器学习 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步…

IDEA中Git的使用小技巧-Toolbar(工具栏)的设置

目录 1 前言 2 步骤 2.1 打开设置 2.2 找到Menus and Toolbars 2.3 Menus and Toolbars界面的介绍 2.4 选择工具 2.5 查看 1 前言 工具栏的合理运用&#xff0c;能够极大程度上为我们省时省力 &#xff0c;接下来我将以Git工具的添加&#xff0c;介绍如何定制我们IDEA…

C++学习笔记 | 基于Qt框架开发实时成绩显示排序系统1

目标&#xff1a;旨在开发一个用户友好的软件工具&#xff0c;用于协助用户基于输入对象的成绩数据进行排序。该工具的特色在于&#xff0c;新输入的数据将以红色高亮显示&#xff0c;从而直观地展现出排序过程中数据变化的每一个步骤。 结果展示&#xff1a; 本程序是一个基于…

游戏服务器哪家强?国内几款主流云服务器测评

游戏服务器租用多少钱一年&#xff1f;1个月游戏服务器费用多少&#xff1f;阿里云游戏服务器26元1个月、腾讯云游戏服务器32元&#xff0c;华为云26元&#xff0c;游戏服务器配置从4核16G、4核32G、8核32G、16核64G等配置可选&#xff0c;游戏专业服务器公网带宽10M、12M、15M…

决策树之scikit-learn

实例 from sklearn.datasets import load_iris from sklearn import tree import matplotlib.pyplot as plt# Load iris dataset iris load_iris() X, y iris.data, iris.target# Fit the classifier clf tree.DecisionTreeClassifier() clf clf.fit(X, y)# Plot the deci…

python 基础知识点(蓝桥杯python科目个人复习计划37)

今日复习内容&#xff1a;DFS--回溯 1.介绍 回溯&#xff1a;就是DFS是一种&#xff0c;在搜索尝试过程中寻找问题的解&#xff0c;当发现已不满足求解条件时&#xff0c;就“回溯”返回&#xff0c;尝试别的路径。 回溯更强调&#xff1a;此路不通&#xff0c;另寻他路&…

linux系统下vscode portable版本的c++/Cmake环境搭建001

linux系统下vscode portable版本的Cmake环境搭建 vscode portable 安装安装基本工具安装 build-essential安装 CMake final script code安装插件CMake Tools & cmakeC/C Extension Pack Testsettings,jsonCMakeLists.txt调试和运行工具 CG 目的&#xff1a;希望在获得一个新…

自定义Function MyRandom函数获得随机数

《VBA信息获取与处理》教程(版权10178984)是我推出第六套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。这部教程给大家讲解的内容有&#xff1a;跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互…

手把手教你开发Python桌面应用-PyQt6图书管理系统-图书信息表格数据显示及搜索实现

锋哥原创的PyQt6图书管理系统视频教程&#xff1a; PyQt6图书管理系统视频教程 Python桌面开发 Python入门级项目实战 (无废话版) 火爆连载更新中~_哔哩哔哩_bilibiliPyQt6图书管理系统视频教程 Python桌面开发 Python入门级项目实战 (无废话版) 火爆连载更新中~共计24条视频&…

C++构造和折构函数详解,超详细!

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 大家龙年好呀&#xff0c;今天我们来学习一下C构造函数和折构函数。 文章目录 1.构造函数 1.1构造函数的概念 1.2构造函数的思想 1.3构造函数的特点 1.4构造函数的作用 1.5构造函数的操作 1.6构造函数…

k8s -ingress

概念 Ingress 公开了从集群外部到集群内服务的 HTTP 和 HTTPS 路由&#xff0c;ingress能代理集群为内部的网络&#xff0c;将集群外部的HTTP/HTTPS网络请求转发至不同的service&#xff0c;其本质就是创建一个NodePort类型的svc,和一个nginx 组成 k8s中的ingress 其实是指…