Python中的并发编程(2)线程的实现

Python中线程的实现

1. 线程

在Python中,threading 库提供了线程的接口。我们通过threading 中提供的接口创建、启动、同步线程。

例1. 使用线程旋转指针

想象一个场景:程序执行了一个耗时较长的操作,如复制一个大文件,我们希望这个过程中程序显示一个动画,表示程序正常运行没有卡死。

简化一下:启动一个函数,执行 3 秒。在这3秒内,在终端持续显示指针旋转的动画。下面用线程来实现这个操作。

注:本例代码主要来自《流畅的Python》(第二版) 19.4.1

首先我们定义旋转函数spin和阻塞函数slow
spin函数每隔0.1s依次打印\|/-,看起来就像是指针转动:
1

import itertools
import time
def spin(msg: str) -> None:  for char in itertools.cycle(r'\|/-'): status = f'\r{char} {msg}' print(status, end='', flush=True)time.sleep(0.1)blanks = ' ' * len(status)print(f'\r{blanks}\r', end='')if __name__ == '__main__':spin("thinking...")

slow函数用来模拟一个耗时的操作。这里我们直接调用time.sleep(3) 等待3秒,然后返回一个结果。

# 阻塞3秒,并返回42
def slow() -> int:time.sleep(3) return 42

调用time.sleep() 阻塞所在的线程,但是释放 GIL,其他 Python 线程可以继续运行。

现在,我们要用线程实现并发。看起来就像是slowspin同时进行。
下面对spin函数做了一些修改,通过threading.Event信号量来同步线程。

import itertools
import time
from threading import Thread, Event# 旋转
def spin(msg: str, done: Event) -> None:  # done用于同步线程for char in itertools.cycle(r'\|/-'): status = f'\r{char} {msg}' print(status, end='', flush=True)if done.wait(.1): #等待/阻塞 。除非有其他线程set了这个事件,则返回True;或者经过指定的时间(0.1s)后,返回 False。breakblanks = ' ' * len(status)print(f'\r{blanks}\r', end='')# 阻塞3秒,并返回42
def slow() -> int:time.sleep(3) return 42

使用线程来并发执行两个函数。
下面我们只手动启动了一个spinner线程,因为程序本身就有一个主线程。

def supervisor() -> int: done = Event()  # 信号量,用于线程同步spinner = Thread(target=spin, args=('thinking!', done)) # 使用Thread创建线程实例spinner。print(f'spinner object: {spinner}') spinner.start() # 启动spinner线程result = slow()  # 调用slow,阻塞 main 线程。同时,次线程spinner运行旋转指针动画done.set() # 设置done为真,唤醒等待done的线程。结束spinner中的循环。spinner.join() # 等待spinner 线程结束。-貌似这里加不加都不影响。return resultdef main() -> None:result = supervisor() print(f'Answer: {result}')if __name__ == '__main__':main()

在这里插入图片描述

程序的执行顺序,主要步骤都发生在supervisor函数中,我们跳过main从supervisor开始看。
由于GIL的存在,同一时刻只有一个线程在执行。所以下面是一个顺序执行的过程。
执行过程大致如下:
在这里插入图片描述

主线程:创建spinner线程,启动spinner线程
spinner线程:输出字符,然后遇到done.wait(.1) 阻塞自己。
主线程:调用slow函数,遇到time.sleep(3) 阻塞
spinner线程:done.wait(.1) 超过了0.1秒返回False,继续输出字符。重复进行阻塞0.1秒、输出字符。
3秒后…
主线程:slow执行完毕,返回结果42。主线程继续执行done.set(),这会唤醒等待done的线程spinner。
spinner线程:运行到done.wait(.1),由于主线程执行了done.set()使得这里的结果为True,所以执行break,结束循环。执行循环下面的print语句后spinner线程结束。
主线程:返回结果。

例2.计算因子

第二个例子我们看一个(失败的)并行计算的例子:
我们希望用n个线程并行计算n个数各自的因子。

注:本例代码来自《Effective Python》(第二版) 第53章

基准方法
逐个计算。

import time# 计算number的因子
def factorize(number):for i in range(1, number + 1):if number % i == 0:yield inumbers = [2139079, 1214759, 1516637, 1852285, 14256346, 12456533]
start = time.time()for number in numbers:list(factorize(number))end = time.time()
delta = end - start
print(f'串行方法花费了 {delta:.3f} 秒')

多线程方式
可以像例1中使用Thread函数实现线程:

def get_factor(number):factors = list(factorize(number))return factorsstart = time.time()
threads = []
for number in numbers:thread = Thread(target=get_factor, args=(number,))thread.start() # 启动threads.append(thread)# 等待所有线程完成
for thread in threads:thread.join() # 等待完成end = time.time()
delta = end - start
print(f'Thread方法花费了 {delta:.3f} 秒')

实现线程的另一种方式是继承Thread类并实现run方法:

from threading import Thread# 继承Thread,需要实现run方法,在run方法中执行要做的事情
class FactorizeThread(Thread):def __init__(self, number):super().__init__()self.number = numberdef run(self):self.factors = list(factorize(self.number))start = time.time()threads = []
for number in numbers:thread = FactorizeThread(number)thread.start() # 启动threads.append(thread)# 等待所有线程完成
for thread in threads:thread.join() # 等待完成end = time.time()
delta = end - start
print(f'Thread方法花费了 {delta:.3f} 秒')

运行结果:
2

你会发现这个多线程的版本并没有变快,这并不意外。
介绍线程时说过,因为GIL的存在,多线程无法同时执行,甚至因为创建和切换线程产生额外的开销导致耗时增加。

小结
在GIL的限制下,Python线程对于并行计算没有用处,但是对于等待(IO、网络、后台任务)是有用处的。下一节我们会看一些Python线程的实际案例。

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

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

相关文章

视频封面提取:精准截图,如何从指定时长中提取某一帧图片

在视频制作和分享过程中,一个有吸引力的封面或截图往往能吸引更多的观众点击观看。有时候要在特定的时间段内从视频中提取一帧作为封面或截图。如果每个视频都手动提取的话就会耗费很长时间,那么如何智化能批量提取呢?现在一起来看下云炫AI智…

SpringAOP专栏一《使用教程篇》

在b站跟着黑马程序员学习SpringAOP时记的笔记。 面向切面编程AOP是 Spring的两大核心之一。 AOP概述 AOP是什么 AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是…

【ArcGIS微课1000例】0078:创建点、线、面数据的最小几何边界

本实例为专栏系统文章:讲述在ArcMap10.6中创建点数据最小几何边界(范围),配套案例数据,持续同步更新! 文章目录 一、工具介绍二、实战演练三、注意事项一、工具介绍 创建包含若干面的要素类,用以表示封闭单个输入要素或成组的输入要素指定的最小边界几何。 工具位于:数…

Mask augmentation for segmentation

For instance and semantic 语义的 segmentation tasks, you need to augment both the input image and one or more output masks. You import the required libraries.You define an augmentation pipeline.You read images and masks from the disk.You pass an image and…

JVM GUI可视化监控及诊断工具

工具既述 使用命令行工具或组合能帮您获取目标Java应用性能相关的基础信息,但它们存在下列局限: 无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。要…

【论文解读】:大模型免微调的上下文对齐方法

本文通过对alignmenttuning的深入研究揭示了其“表面性质”,即通过监督微调和强化学习调整LLMs的方式可能仅仅影响模型的语言风格,而对模型解码性能的影响相对较小。具体来说,通过分析基础LLMs和alignment-tuned版本在令牌分布上的差异&#…

Vue3项目调用腾讯地图服务(地址解析 地址转坐标)及使用axios的跨域问题

一,需求 根据传入的文本地址 将其转换为坐标 显示地图点位在腾讯地图上 二,使用axios发送请求 import axios from axios; //引入axiosaxios({url:https://apis.map.qq.com/ws/geocoder/v1,method:get//参数 地址和key值}).then((data)>{console.log(data)});但是使用完报跨…

JVM 运行时参数

面试题 JVM的参数,你知道的说一下 (百度) 说说你知道的几种主要的JVM参数(京东) JVM调优调的哪些参数?在哪里写这些参数? (亚信) 内存调优参数都有什么?&am…

​LeetCode解法汇总1466. 重新规划路线

目录链接: 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目: https://github.com/September26/java-algorithms 原题链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 描述: n 座城市&…

第一节:安装

Node.js 命令行的TypeScript编译器可以使用Node.js包来安装。 安装 npm install -g typescript 编译 tsc helloworld.ts 由于无法直接运行TypeScript ,所以会将 ts文件 转换成 js文件, 生成一个 js 的文件; 也可以安装 ts-node ,他是封…

redis-学习笔记(list)

因为 list 可以头插头删, 尾插尾删, 所以其实更像 C 中的 deque (双端队列) ---- 知道就好, 别乱说, 具体底层编码是啥, 俺也不知道(没注意过) 可以通过组合, 把 list 当作队列 / 栈来用 list 的几种底层编码: ziplist(压缩列表) , linkedlist(链表) , quicklist ziplist 就是将…

AI助力智慧农业,基于SSD模型开发构建田间作物场景下庄稼作物、杂草检测识别系统

智慧农业随着数字化信息化浪潮的演变有了新的定义,在前面的系列博文中,我们从一些现实世界里面的所见所想所感进行了很多对应的实践,感兴趣的话可以自行移步阅读即可:《自建数据集,基于YOLOv7开发构建农田场景下杂草检…