Python并发编程之多线程

 前言

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

线程介绍

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

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

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

开启线程 - threading模块

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

from threading import Thread


def 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 Thread

server = 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: break
            conn.send(data.upper())
        except Exception as e:
            print(e)
            break
    conn.close()


while True:
    conn, addr = server.accept()
    t = Thread(target=task, args=(conn, ))	# 来一个连接请求,就开一个线程处理这个连接
    t.start()								# 这种并发方式有缺陷:容易内存溢出(线程个数有限)

join方法

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

from threading import Thread
import time

class MyThread(Thread):
    def __init__(self,name):
        super().__init__()
        self.name = name

    def 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 Thread

money = 100
def foo():
    global money
    money = 666
    print('子',money)

if __name__ == '__main__':
    t = Thread(target=foo)
    t.start()
    print('主',money)
    
    
# 程序运行结果
子 666
主 666

守护线程

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

from threading import Thread
import time

def foo():
    print('太监逍遥自在!')
    time.sleep(3)
    print('老子寿终正寝')


if __name__ == '__main__':
    t = Thread(target=foo)
    t.daemon = True
    t.start()
    print('吾主驾崩')
    
# 运行结果,表示太监没有寿终正寝
太监逍遥自在!
吾主驾崩

线程互斥锁

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

from threading import Thread, Lock
import time

money = 100
mutex = Lock()  # 实例化得到锁

# 线程需要执行的任务
def task():
    global money
    mutex.acquire()  # 线程获取锁
    tmp = money
    time.sleep(0.01)
    money = tmp - 1
    mutex.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 n
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=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也没用。

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

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

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

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

相关文章

基于查询模板的知识图谱问答系统

目录 前言1 知识图谱问答系统的两个核心问题1.1 问句的表示与语义理解1.2 知识库的映射和匹配 2 问答基本流程2.1 模板生成2.2 模板实例化2.3 查询排序和结果获取 3 模板自动生成3.1 quint方法3.2 对齐任务 4 基于查询模板的知识图谱问答系统优缺点4.1 系统的优点4.2 系统的缺点…

安全的接口访问策略

渗透测试 一、Token与签名 一般客户端和服务端的设计过程中,大部分分为有状态和无状态接口。 一般用户登录状态下,判断用户是否有权限或者能否请求接口,都是根据用户登录成功后,服务端授予的token进行控制的。 但并不是说有了tok…

机器学习:Softmax介绍及代码实现

Softmax原理 Softmax函数用于将分类结果归一化,形成一个概率分布。作用类似于二分类中的Sigmoid函数。 对于一个k维向量z,我们想把这个结果转换为一个k个类别的概率分布p(z)。softmax可以用于实现上述结果,具体计算公式为: 对于…

VMWare虚拟机安装

VMWare虚拟机安装 0.Linux运行平台介绍1. VMWare 虚拟软件安装检查虚拟网卡是否安装 创建VMWare虚拟机对创建虚拟机的内容进行设置挂在要安装的CentOS的ISO文件 0.Linux运行平台介绍 Linux的运行平台一共有两种,其中包括物理机平台和虚拟机平台,在学习阶段当中建议使用虚拟机 …

海康威视球机摄像头运动目标检测、跟踪与轨迹预测

一、总体方案设计 运动目标检测与跟踪方案设计涉及视频流的实时拍摄、目标检测、轨迹预测以及云台控制。以下是四个步骤的详细设计: 1.室内场景视频流拍摄 使用海康威视球机摄像头进行室内视频流的实时拍摄。确保摄像头能覆盖整个室内空间,以便捕捉所…

考研数据结构笔记(7)

循环链表、静态链表、顺序表和链表的比较 循环链表循环单链表循环双链表 静态链表什么是静态链表如何定义一个静态链表?简述基本操作的实现 顺序表和链表的比较逻辑结构物理结构/存储结构数据的运算/基本运算创建销毁增加、删除查找 循环链表 循环单链表 循环双链表…

【浙大版《C语言程序设计实验与习题指导(第4版)》】实验7-1-6 求一批整数中出现最多的个位数字(附测试点)

定一批整数,分析每个整数的每一位数字,求出现次数最多的个位数字。例如给定3个整数1234、2345、3456,其中出现最多次数的数字是3和4,均出现了3次。 输入格式: 输入在第1行中给出正整数N(≤1000&#xff0…

Game辅助推广购卡系统全新一键安装版-已激活

(购买本专栏可免费下载栏目内所有资源不受限制,持续发布中,需要注意的是,本专栏为批量下载专用,并无法保证某款源码或者插件绝对可用,介意不要购买) 资源简介 运行环境 PHP5.6~7.0+MYSQL5.6 本程序可配合(伯乐发卡)基础版使用; 界面炫酷大气!程序内核为yunucm…

1.CVAT建项目步骤

文章目录 1. 创建project2. 创建task2.1. label 标签详解2.2.高级配置 Advanced configuration 3. 分配任务4. 注释者规范 CVAT的标注最小单位是Task,每个Task为一个标注任务。 1. 创建project 假设你并不熟悉cvat的标注流程,这里以图像2D目标检测为例进…

13. 串口接收模块的项目应用案例

1. 使用串口来控制LED灯工作状态 使用串口发送指令到FPGA开发板,来控制第7课中第4个实验的开发板上的LED灯的工作状态。 LED灯的工作状态:让LED灯按指定的亮灭模式亮灭,亮灭模式未知,由用户指定,8个变化状态为一个循…

《CSS 简易速速上手小册》第7章:CSS 预处理器与框架(2024 最新版)

文章目录 7.1 Sass:更高效的 CSS 编写7.1.1 基础知识7.1.2 重点案例:主题颜色和字体管理7.1.3 拓展案例 1:响应式辅助类7.1.4 拓展案例 2:深色模式支持 7.2 Bootstrap:快速原型设计和开发7.2.1 基础知识7.2.2 重点案例…

微信小程序的了解和使用

微信小程序 微信小程序的项目组成 pages 文件夹 用于存放所有的小程序页面 logs 文件夹 用于存放所有的日志文件 utils 文件夹 用于存放工具性质的模块 js app.js 小程序的入口文件 app.json 小程序的全局配置文件 app.wxss 全局样式文件 project.config.json 项目配置文…

解放双手!ChatGPT助力编写JAVA框架!

摘要 本文介绍了使用 ChatGPT逐步创建 一个简单的Java框架,包括构思、交流、深入优化、逐步完善和性能测试等步骤。 亲爱的Javaer们,在平时编码的过程中,你是否曾想过编写一个Java框架去为开发提效?但是要么编写框架时感觉无从下…

4核8g服务器能支持多少人访问?2024新版测评

腾讯云轻量4核8G12M轻量应用服务器支持多少人同时在线?通用型-4核8G-180G-2000G,2000GB月流量,系统盘为180GB SSD盘,12M公网带宽,下载速度峰值为1536KB/s,即1.5M/秒,假设网站内页平均大小为60KB…

程序员如何 “高效学习”?

开篇先说说我吧,马上人生要步入30岁的阶段,有些迷茫,更多的是焦虑,因为行业的特殊性导致我无时无刻不对 “青春饭” 的理论所担忧。担忧归担忧,生活还要继续,我们都知道这行全靠 “学习” 二字,…

树莓派编程基础与硬件控制

1.编程语言 Python 是一种泛用型的编程语言,可以用于大量场景的程序开发中。根据基于谷歌搜 索指数的 PYPL(程序语言流行指数)统计,Python 是 2019 年 2 月全球范围内最为流行 的编程语言 相比传统的 C、Java 等编程语言&#x…

【Linux】Linux下的基本指令

Linux下的基本指令 Linux 的操作特点:纯命令行ls 指令文件 pwd命令Linux的目录结构绝对路径 / 相对路径,我该怎么选择? cd指令touch指令mkdir指令(重要)rmdir指令rm 指令(重要)man指令&#xff…

静态时序分析:建立时间分析

静态时序分析https://blog.csdn.net/weixin_45791458/category_12567571.html?spm1001.2014.3001.5482 在静态时序分析中,建立时间检查约束了触发器时钟引脚(时钟路径)和输入数据引脚(数据路径)之间的时序关系&#x…

HSM加密机原理:密钥管理和加密操作从软件层面转移到物理设备中 DUKPT 安全行业基础8

HSM加密机原理 硬件安全模块(HSM)是一种物理设备,设计用于安全地管理、处理和存储加密密钥和数字证书。HSM广泛应用于需要高安全性的场景,如金融服务、数据保护、企业安全以及政府和军事领域。HSM提供了一种比软件存储密钥更安全…

【前端高频面试题--Vue基础篇】

🚀 作者 :“码上有前” 🚀 文章简介 :前端高频面试题 🚀 欢迎小伙伴们 点赞👍、收藏⭐、留言💬前端高频面试题--Vue基础篇 Vue基本原理双向绑定与MVVM模型Vue的优点计算属性与监听属性计算属性监…