Python线程入门:多线程并发的基础与实践

线程概念

我们在日常开发中经常会听到使用多线程/多进程的方式完成并发任务。那么什么是进程?什么是线程?进程与线程之间有什么关系?接下来我们通过日常场景简单的了解一下进程与线程。

一个工厂,至少有一个车间,一个车间中最少有一个工人,最终是工人在工作。

一个程序,至少有一个进程,一个进程中最少有一个线程,最终是线程在工作。

在一个车间中有最基本的工作工具,人通过操作工具的方式来完成工作。

一个进程中有最基本的运行代码的资源(内存、cpu...),线程通过进程中提供的资源来运行代码。

通过上述描述,我们来总结一下线程与进程的关系:

  • 线程是计算机可以被cpu调度的最小单元
  • 进程是计算机分配资源的的最小单元,进程可以为线程提供运行资源。

一个进程中可以有多个线程,同一个进程中的线程可以共享当前进程中的资源。

使用线程完成并发任务

我们之前编写的代码都被称为同步代码,排队依次运行。如果前一个任务没有完成,那么之后的任务也无法继续。

import timedef work_1():print('任务1...')time.sleep(2)def work_2():print('任务2...')time.sleep(2)work_1()
work_2()

使用线程的方式优化代码

import time
import threadingdef work_1():print('任务1...')time.sleep(2)def work_2():print('任务2...')time.sleep(2)# 通过Thread方法创建线程对象,并使用target指定线程对象要运行的任务
t1 = threading.Thread(target=work_1)
t2 = threading.Thread(target=work_2)# 运行线程
t1.start()
t2.start()

实现一个简单的单线程同步爬虫

import requestsdef get_image(url):response = requests.get(url).contentfile_name = url.rsplit('/')[-1]with open(file_name, 'wb') as f:f.write(response)print('下载完成...')url_list = ['http://pic.bizhi360.com/bbpic/98/10798.jpg','http://pic.bizhi360.com/bbpic/92/10792.jpg','http://pic.bizhi360.com/bbpic/86/10386.jpg']for url in url_list:get_image(url)

对上述单线程同步爬虫进行优化

import requests
import threadingdef get_image(url):response = requests.get(url).contentfile_name = url.rsplit('/')[-1]with open(file_name, 'wb') as f:f.write(response)print('下载完成...')url_list = ['http://pic.bizhi360.com/bbpic/98/10798.jpg','http://pic.bizhi360.com/bbpic/92/10792.jpg','http://pic.bizhi360.com/bbpic/86/10386.jpg']for url in url_list:t = threading.Thread(target=get_image, args=(url,))t.start()

除了上述使用线程的方式提升运行速度之外,我们还可以使用多进程的方式完成并发任务。

import requests
import multiprocessingdef get_image(url):response = requests.get(url).contentfile_name = url.rsplit('/')[-1]with open(file_name, 'wb') as f:f.write(response)print('下载完成...')url_list = ['http://pic.bizhi360.com/bbpic/98/10798.jpg','http://pic.bizhi360.com/bbpic/92/10792.jpg','http://pic.bizhi360.com/bbpic/86/10386.jpg']if __name__ == "__main__":for url in url_list:p = multiprocessing.Process(target=get_image, args=(url,))p.start()
GIL锁

在刚刚的一些案例中我们发现无论使用多线程还是多进程的方式都可以实现并发任务,那么在什么业务场景下去使用多线程/多进程?在学习本小结内容之前,先给出结论:

  • 如果任务是属于IO密集型任务那么优先使用线程方式
  • 如果任务是属于计算密集型任务那么优先使用进程方式

在Python中存在全局解释器锁,简称为GILGILcpython解释器独有的。主要的功能是:让一个进程在同一时刻只能有一个线程被执行。

例如在一个进程中创建了多个线程,在运行当前程序时在同一时刻只有一个线程被执行,其他线程等待cpu调度。这种情况无法利用多核cpu的优势,如果想要绕开GIL,那么可以使用多进程的方式,创建多个进程,每个进程中只有一个线程。但是请注意,创建多进程消耗的资源比多线程消耗的资源大。

线程方法 - 重点

在正式学习线程方法之前,我们首先回顾一下刚刚学习到的内容。

# threading_test.py
import time
import threadingdef work():time.sleep(2)print('子线程任务...')# 当前只是创建了线程对象,并不会执行线程
t = threading.Thread(target=work)
# 通过start运行线程
t.start()
print('主线程打印信息...')'''
在上述代码中,我们要明确:1.一个py文件被解释器执行时会在操作系统中创建进程。2.在进程中创建线程来执行当前文件中的代码,我们把最初创建的线程称之为主线程。3.当主线程执行到Thread代码时会创建一个新的线程,我们一般称之为子线程。4.当前代码中的主线程与子线程交替执行。5.子线程被执行时主线程不会等待,继续往下执行到没有代码时等待子线程执行完毕再退出。
'''

thread_obj.start()

当前线程准备就绪,等待cpu调度,具体调度时间由cpu决定。

import threadingnum = 0def add():global numfor i in range(100000):num += it = threading.Thread(target=add)
t.start()
print(num)

thread_obj.join()

等待子线程任务执行完毕后再继续向下执行

import threadingnum = 0def add():global numfor i in range(100000):num += it = threading.Thread(target=add)
t.start()
t.join()  # 主线程等待子线程任务结束时解堵塞
print(num)
import threadingnum = 0def add_():global numfor i in range(100000):num += idef sub_():global numfor i in range(100000):num -= it1 = threading.Thread(target=add_)
t2 = threading.Thread(target=sub_)
t1.start()
t1.join()  # 主线程等待子线程任务结束时解堵塞t2.start()
t2.join()
print(num)

thread_obj.setDaemon(bool: attr)

设置守护线程,需要在线程启动之前设置。

如果一个线程为守护线程则主线程执行完毕后不管子线程任务是否结束都自动退出。

当前参数默认为:False

import time
import threadingdef work():for i in range(5):print(i)time.sleep(1)t = threading.Thread(target=work)
t.setDaemon(True)
t.start()
print('主线程即将退出...')

thread_obj.current_thread()

获取当前运行的线程对象的引用

import threadingdef work():name = threading.current_thread().getName()print(name)for i in range(5):t = threading.Thread(target=work)t.setName(f'线程: {i}')t.start()

自定义线程类完成并发爬虫

import requests
import threadingclass ThreadSpider(threading.Thread):def __init__(self, url):super().__init__()  # 子类继承线程类如果需要重写构造函数必须先调用父类的构造函数self.url = urldef run(self):response = requests.get(self.url).contentfile_name = self.url.rsplit('/')[-1]with open(file_name, 'wb') as f:f.write(response)print('下载完成...')url_list = ['http://pic.bizhi360.com/bbpic/98/10798.jpg','http://pic.bizhi360.com/bbpic/92/10792.jpg','http://pic.bizhi360.com/bbpic/86/10386.jpg'
]for url in url_list:ts = ThreadSpider(url)ts.start()
线程安全

在之前的案例中我们使用了多个线程对一个全局变量进行修改的操作,如果多个线程都对一个全局变量进行操作的话会出现资源竞争的问题,会导致计算错误。

import threadingnum = 0def add_():global numfor i in range(100000):num += idef sub_():global numfor i in range(100000):num -= it1 = threading.Thread(target=add_)
t2 = threading.Thread(target=sub_)
t1.start()
t2.start()t1.join()
t2.join()
print(num)

使用线程锁解决上述问题

from threading import Thread, RLocknum = 0
lock_obj = RLock()def add_():global numfor i in range(100000):lock_obj.acquire()  # 申请锁,申请成功会让其他线程等待直到当前线程释放num += ilock_obj.release()  # 释放锁,当锁被释放后其他线程才能被cpu挂起执行def sub_():global numfor i in range(100000):lock_obj.acquire()num -= ilock_obj.release()t1 = Thread(target=add_)
t2 = Thread(target=sub_)
t1.start()
t2.start()t1.join()
t2.join()
print(num)

在上述案例中,我们在代码中手动申请锁与释放锁,RLock方法支持上下文管理协议,可以使用with语句帮助我们申请和释放锁。

from threading import Thread, RLocknum = 0
lock_obj = RLock()def add_():global numfor i in range(100000):with lock_obj:  # 使用上下文管理锁的申请与释放num += idef sub_():global numfor i in range(100000):with lock_obj:num -= it1 = Thread(target=add_)
t2 = Thread(target=sub_)
t1.start()
t2.start()t1.join()
t2.join()
print(num)

在多线程编程中,不是任何地方都需要加锁处理。有些内置类型已经具有了加锁的机制,例如:列表。

import threadingdata_list = list()def add_list():for _ in range(100000):data_list.append(i)print(len(data_list))for i in range(2):t = threading.Thread(target=add_list)t.start()

在线程中一般使用两种锁机制:LockRLock

Lock

同步锁:同步锁一般很少使用,不支持锁嵌套

from threading import Thread, Locknum = 0
lock_obj = Lock()def add_num():global numfor i in range(10000000):lock_obj.acquire()num += 1lock_obj.release()print(num)for _ in range(2):t = Thread(target=add_num)t.start()

RLock

递归锁:支持锁嵌套

from threading import Thread, RLocknum = 0
lock_obj = RLock()def add_num():global numfor i in range(100000):lock_obj.acquire()lock_obj.acquire()num += 1lock_obj.release()lock_obj.release()print(num)for _ in range(2):t = Thread(target=add_num)t.start()
死锁

在使用锁的过程中我们发现如果在同步锁中使用了嵌套则程序会卡死,这种情况我们称之为死锁。

死锁:由于资源竞争造成的一种堵塞现象。

# coding=utf-8
import threading
import timemutexA = threading.Lock()
mutexB = threading.Lock()class MyThread1(threading.Thread):def run(self):# 对mutexA上锁mutexA.acquire()# mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁print(self.name + '----do1---up----')time.sleep(1)# 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了mutexB.acquire()print(self.name + '----do1---down----')mutexB.release()# 对mutexA解锁mutexA.release()class MyThread2(threading.Thread):def run(self):# 对mutexB上锁mutexB.acquire()# mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁print(self.name + '----do2---up----')time.sleep(1)# 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了mutexA.acquire()print(self.name + '----do2---down----')mutexA.release()# 对mutexB解锁mutexB.release()if __name__ == '__main__':t1 = MyThread1()t2 = MyThread2()t1.start()t2.start()
线程池

创建线程对象的期间会损耗时间,尤其是在需要开辟大量线程对象的时候会发生性能下降的情况。那么我们能否让程序创建一定数量的线程对象,并且在执行完某一个任务后不会被解释器销毁,下一个任务重复使用之前所创建的线程对象。

像这种需要创建大量线程对象的场景推荐使用线程池。

线程池的创建

# 模拟网页请求
import time
from concurrent.futures import ThreadPoolExecutordef get_html(time_attr):time.sleep(time_attr)print(f'get page {time_attr} success')return time_attr# 创建线程池对象
executor = ThreadPoolExecutor(max_workers=2)# 通过submit提交需要执行的函数到线程池中,并且submit是立即返回对象不会堵塞
task_1 = executor.submit(get_html, 3)
task_2 = executor.submit(get_html, 2)# done方法用于判定某个任务是否完成
print('task_1完成情况:', task_1.done())# 可以使用cancel取消任务 但是运行中的任务无法取消,可以将线程数量修改成1
# print('task_2任务取消:', task_2.cancel())# result方法可以获取任务的返回值 当前获取为阻塞
print('task_1返回结果:', task_1.result())

as_completed

获取已经执行成功的task的返回值

# 模拟网页请求
import time
from concurrent.futures import ThreadPoolExecutor, as_completeddef get_html(time_attr):time.sleep(time_attr)print(f'get page {time_attr} success')return time_attr# 创建线程池对象
executor = ThreadPoolExecutor(max_workers=2)# 批量提交任务并获取已经执行成功的task的返回值
time_attr_list = [3, 2, 5, 4]
all_task = [executor.submit(get_html, time_attr) for time_attr in time_attr_list]
for future in as_completed(all_task):data = future.result()print(f'get page {data}')  # 线程任务只要执行完就能获取到返回值

map

获取已经执行成功的task的返回值 - 代码更精简

# 模拟网页请求
import time
from concurrent.futures import ThreadPoolExecutor, as_completeddef get_html(time_attr):time.sleep(time_attr)print(f'get page {time_attr} success')return time_attr# 创建线程池对象
executor = ThreadPoolExecutor(max_workers=2)# 批量提交任务并获取已经执行成功的task的返回值
time_attr_list = [3, 2, 5, 4]# 通过executor对象中的map获取已经完成的任务的返回值
for data in executor.map(get_html, time_attr_list):print(f'get page {data}')  # 当前打印的返回值顺序与列表顺序一致# all_task = [executor.submit(get_html, time_attr) for time_attr in time_attr_list]
# for future in as_completed(all_task):
#     data = future.result()
#     print(f'get page {data}')  # 线程任务只要执行完就能获取到返回值

wait

等待指定任务完成后主线程解堵塞

import time
from concurrent.futures import ThreadPoolExecutor, waitdef get_html(time_attr):time.sleep(time_attr)print(f'get page {time_attr} success')return time_attr# 创建线程池对象
executor = ThreadPoolExecutor(max_workers=2)# 批量提交任务并获取已经执行成功的task的返回值
time_attr_list = [3, 2, 5, 4]all_task = [executor.submit(get_html, time_attr) for time_attr in time_attr_list]
wait(all_task)
print('主线程执行...')

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

本地缓存~

前言 Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中取而代之,基于LRU算法实现,支持多种缓存过期策略。 以下摘抄于https://github.com/ben-manes/caffeine/wiki/Benchmarks-zh-CN 基准测试通过使用Java microbenchmark ha…

Unity Shader Graph 2D - 角色身体电流覆盖效果

在游戏中,通常会有游戏角色受到“电击”的效果,此时游戏角色身体上会覆盖有电流,该效果能表明游戏角色的当前状态,让玩家能够获得更直观更好的体验。 那么如何实现呢 首先创建一个ShaderGraph文件,命名为Current,再创建对应的材质球M_Current。 基础的资源显示 老规矩,…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.9 广播陷阱:形状不匹配的深层隐患

2.9 广播陷阱:形状不匹配的深层隐患 目录 #mermaid-svg-F0AgBChfSCGzOqa7 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-F0AgBChfSCGzOqa7 .error-icon{fill:#552222;}#mermaid-svg-F0AgBChfSCGzOqa7 …

解锁豆瓣高清海报(二) 使用 OpenCV 拼接和压缩

解锁豆瓣高清海报(二): 使用 OpenCV 拼接和压缩 脚本地址: 项目地址: Gazer PixelWeaver.py pixel_squeezer_cv2.py 前瞻 继上一篇“解锁豆瓣高清海报(一) 深度爬虫与requests进阶之路”成功爬取豆瓣电影海报之后,本文将介绍如何使用 OpenCV 对这些海报进行智…

vue入门到实战 二

目录 2.1 计算属性computed 2.1.1什么是计算属性 2.1.2 只有getter方法的计算属性 2.1.3 定义有getter和setter方法的计算属性 2.1.4 计算属性和methods的对比 2.2 监听器属性watch 2.2.1 watch属性的用法 2.2.2 computed属性和watch属性的对比 2.1 计算属性computed…

【DeepSeek】本地快速搭建DeepSeek

博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持! 博主链接 博客内容主要围绕: 5G/6G协议讲解 高级C语言讲解 Rust语言讲解 文章目录 本地快速搭建DeepSeek一、安装及配置ollama二、DeepSeek模型…

Spring WebFlux揭秘:下一代响应式编程框架,与Spring MVC有何不同?

Spring WebFlux和Spring MVC都是Spring家族里的成员,它们都能帮助我们开发Web应用,但工作方式有所不同。 可以把Spring MVC想象成一个服务员,每次有客人(请求)来,它就会专门找一个服务员(线程&a…

基于微信小程序的实习记录系统设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…

MySQL5.5升级到MySQL5.7

【卸载原来的MySQL】 cmd打开命令提示符窗口(管理员身份)net stop mysql(先停止MySQL服务) 3.卸载 切换到原来5.5版本的bin目录,输入mysqld remove卸载服务 测试mysql -V查看Mysql版本还是5.5 查看了环境变量里的…

TensorFlow 简单的二分类神经网络的训练和应用流程

展示了一个简单的二分类神经网络的训练和应用流程。主要步骤包括: 1. 数据准备与预处理 2. 构建模型 3. 编译模型 4. 训练模型 5. 评估模型 6. 模型应用与部署 加载和应用已训练的模型 1. 数据准备与预处理 在本例中,数据准备是通过两个 Numpy 数…

使用朴素贝叶斯对散点数据进行分类

本文将通过一个具体的例子,展示如何使用 Python 和 scikit-learn 库中的 GaussianNB 模型,对二维散点数据进行分类,并可视化分类结果。 1. 数据准备 假设我们有两个类别的二维散点数据,每个类别包含若干个点。我们将这些点分别存…

AI视频编码器(3.2) 《Swin Transformer V2: Scaling Up Capacity and Resolution》

arxiv链接自监督训练用到了SimMIM 论文链接。我觉得,SimMIM与MAE的区别在于,前者只是一个1-layer的prediction head,而后者是多层transformer结构的decoder。可参考Swin Transformer V2(CVPR 2022)论文与代码解读。总结 图中展示了三个创新,从左到右有三处红色结构,分别…

前端进阶:深度剖析预解析机制

一、预解析是什么? 在前端开发中,我们常常会遇到一些看似不符合常规逻辑的代码执行现象,比如为什么在变量声明之前访问它,得到的结果是undefined,而不是报错?为什么函数在声明之前就可以被调用&#xff1f…

Baklib赋能企业提升内容中台构建效率的全新路径解析

内容概要 在当今数字化转型的大潮中,企业面临着前所未有的挑战与机遇。为了顺应市场的发展趋势,提高运营能力,搭建高效的内容中台已成为企业迫在眉睫的任务。内容中台不仅仅是一个技术架构的集合,它更是企业实现数据共享、资源整…

计算机网络——流量控制

流量控制的基本方法是确保发送方不会以超过接收方处理能力的速度发送数据包。 通常的做法是接收方会向发送方提供某种反馈,如: (1)停止&等待 在任何时候只有一个数据包在传输,发送方发送一个数据包,…

游戏引擎 Unity - Unity 设置为简体中文、Unity 创建项目

Unity Unity 首次发布于 2005 年,属于 Unity Technologies Unity 使用的开发技术有:C# Unity 的适用平台:PC、主机、移动设备、VR / AR、Web 等 Unity 的适用领域:开发中等画质中小型项目 Unity 适合初学者或需要快速上手的开…

MySQL基础-多表查询

多表查询-多表关系 多表查询-概述 例如执行下行sql语句就会出现笛卡尔积: select *from emp,dept; --消除笛卡尔积 select * from emp,dept where emp.dept_id dept.id; 多表查询-查询分类 多表查询-连接查询-内连接 --内连接演示 --1.查询每一个员工的姓名,及关…

[权限提升] Wdinwos 提权 维持 — 系统错误配置提权 - Trusted Service Paths 提权

关注这个专栏的其他相关笔记:[内网安全] 内网渗透 - 学习手册-CSDN博客 0x01:Trusted Service Paths 提权原理 Windows 的服务通常都是以 System 权限运行的,所以系统在解析服务的可执行文件路径中的空格的时候也会以 System 权限进行解析&a…

【01】共识机制

BTF共识 拜占庭将军问题 拜占庭将军问题是一个共识问题 起源 Leslie Lamport在论文《The Byzantine Generals Problem》提出拜占庭将军问题。 核心描述 军中可能有叛徒,却要保证进攻一致,由此引申到计算领域,发展成了一种容错理论。随着…

本地部署DeepSeek教程(Mac版本)

第一步、下载 Ollama 官网地址:Ollama 点击 Download 下载 我这里是 macOS 环境 以 macOS 环境为主 下载完成后是一个压缩包,双击解压之后移到应用程序: 打开后会提示你到命令行中运行一下命令,附上截图: 若遇…