IDE版本:PyCharm 2023.3.2
Python演示版本: 3.12.1
2023年下半年,接了一个视觉高速动态实时识别的项目,使用的是YOLOv5s进行动态识别。
项目初期,经过多次多环境高复杂程度测试,结论:识别速度远小于视频流每一帧的速度
所以在开发之前,先预设好开发架构。
此次开发不采用逐帧识别法,开发方案定位是抽帧识别法(成本与效率之间的折中考量)。
本次不讨论如何识别(在后续的博客里会发布,请订阅python栏目)
这次讨论的内容是如何在同一个时间点保证原子性实现异步抽帧
由于实现原理众多且冗长,这次讨论的第一个重要实现原理:
【多个.py文件之间全局变量的调用】
在继续阅读之前,可以先参考一下我之前发的一篇博客
传送门:【Python实战】global关键字的应用和线程并发
建议先实操一下这篇博客,可以更好的帮你理解以下内容。
首先,我们先创建a.py、b.py和c.py三个.py文件。
各个.py文件代码如下:
from time import sleep
# 初始化a_value的值为100
a_value = 100
# 改变a_value值的方法
def change():
# 在此方法中用global声明a_value为全局变量
# 这样改变a_value的值才会生效
global a_value
while True:
# 每一次让a_value自增1
a_value += 1
# 间隔1秒
sleep(1)
from time import sleep
# 初始化b_value的值为200
b_value = 200
# 改变b_value值的方法
def change():
# 在此方法中用global声明b_value为全局变量
# 这样改变b_value的值才会生效
global b_value
while True:
# 每一次让b_value自增1
b_value += 1
# 间隔1秒
sleep(1)
from time import sleep
# 初始化c_value的值为300
c_value = 300
# 改变c_value值的方法
def change():
# 在此方法中用global声明c_value为全局变量
# 这样改变c_value的值才会生效
global c_value
while True:
# 每一次让c_value自增1
c_value += 1
# 间隔1秒
sleep(1)
这三个.py文件,分别声明了三个变量的值和各自的change()方法
a_value = 100
b_value = 200
c_value = 300
每个change()方法都会让各自声明的值在每一次循环中自增1
首先,我们来创建main.py文件
代码如下:
import threading
from concurrent.futures import ThreadPoolExecutor
from time import sleep
# 用import引入a、b和c三个.py文件
import a
import b
import c
# 监视器方法
def monitor():
# 循环计数标记
count = 1
while True:
print(f'第 {count} 次循环')
print(f'a_value: {a.a_value}, b_value: {b.b_value}, c_value: {c.c_value}')
# 设置比以上各个.py文件延迟10ms,以便于观察全局变量的变化
sleep(1.01)
print(f'a_value: {a.a_value}, b_value: {b.b_value}, c_value: {c.c_value}')
sleep(1.01)
print(f'a_value: {a.a_value}, b_value: {b.b_value}, c_value: {c.c_value}')
sleep(1.01)
count += 1
# 声明线程列表,保存线程
thread_list = [threading.Thread(target=a.change),
threading.Thread(target=b.change),
threading.Thread(target=c.change),
threading.Thread(target=monitor)]
# 使用 map 方法提交线程列表,自动并行执行
# 线程完成后,会自动等待所有线程完成
ThreadPoolExecutor().map(lambda thread: thread.run(), thread_list)
在Python中,“import” 和 “from import” 是两种不同的方式用于导入模块或模块中的特定对象(如函数、类、变量等)。
它们之间的主要区别在于导入的范围和使用方式。
这边调用全局变量的导入方式使用的是“import”
为什么使用import来导入?
对于全局变量来说,这两个导入有什么区别?
当然有区别啦!区别大着呢!
比如a.py中的a_value
from a import a_value
这个导入只导入a.py文件中的a_valuie对象
没有导入a.py的整个文件
对于在main文件中
这种方式的导入,相当于把整个a.py文件分模块拆开成各自独立的模块
开辟各自相对独立的内存空间(堆)
目前堆栈结构如下:
通过from import进来的a_value是在main.py中创建一个新的对象(相当于New一个对象)
引用地址和a.py文件中的a_value已经不一样,相对独立
这时,改变main.py中a_value的值,在a.py中的a_value不会产生任何变化
import a
a_value = a.a_value
通过导入整个a.py文件来获取a_value
目前堆栈结构如下:
当你改变了main.py中的 a.a_value 的值
a.py中的a_value的值也会随之改变
import导入整个模块,对应的变量的调用,他的引用(栈),指向同一个块内存(堆)
所以,在main.py中,我使用的是import方式导入整个模块
在main.py中,声明了a、b、c和monitor四条线程。
代码如下:
# 声明线程列表,保存线程
thread_list = [threading.Thread(target=a.change),
threading.Thread(target=b.change),
threading.Thread(target=c.change),
threading.Thread(target=monitor)]
这边为了便于操作,用列表来保存这四条线程
然后让这四条线程加入到主线程中运行
代码如下:
# 使用 map 方法提交线程列表,自动并行执行
# 线程完成后,会自动等待所有线程完成
ThreadPoolExecutor().map(lambda thread: thread.run(), thread_list)
以上用了线程池和map还有lambda语法简化线程列表运行并加入主线程
和下面代码运行的结果一样:
# 遍历所有线程启动
for thread in thread_list:
thread.start()
# 遍历所有线程加入主线程
for thread in thread_list:
thread.join()
这样同时创建了4条异步线程
在monitor方法中,你会看到在每一次循环次数打印的值都会产生变化
结果如下:
第 1 次循环
a_value: 101, b_value: 201, c_value: 301
a_value: 102, b_value: 202, c_value: 302
a_value: 103, b_value: 203, c_value: 303
第 2 次循环
a_value: 104, b_value: 204, c_value: 304
a_value: 105, b_value: 205, c_value: 305
a_value: 106, b_value: 206, c_value: 306
第 3 次循环
a_value: 107, b_value: 207, c_value: 307
a_value: 108, b_value: 208, c_value: 308
a_value: 109, b_value: 209, c_value: 309
这是在a.py、b.py和c.py中改变变量的值,在main.py中对应的值也会产生变化
如果反过来,我在main.py中改变a.a_value、b.b_value和c.c_value
在a.py、b.py和c.py中的各个对应的变量是否也会跟着改变呢?
这边我们修改一下a.py、b.py和c.py中的代码:
from time import sleep
# 初始化a_value的值为100
a_value = 100
# 改变a_value值的方法
def change():
# 在此方法中用global声明a_value为全局变量
# 这样改变a_value的值才会生效
global a_value
while True:
# 间隔比main中改变值延迟10ms,确保值改变后打印值的变化
sleep(1.01)
# 改为每次循环打印a_value的值是否有改变
print(f'a_value: {a_value}')
from time import sleep
# 初始化b_value的值为200
b_value = 200
# 改变b_value值的方法
def change():
# 在此方法中用global声明b_value为全局变量
# 这样改变b_value的值才会生效
global b_value
while True:
# 间隔比main中改变值延迟10ms,确保值改变后打印值的变化
sleep(1.01)
# 改为每次循环打印b_value的值是否有改变
print(f'b_value: {b_value}')
from time import sleep
# 初始化c_value的值为300
c_value = 300
# 改变c_value值的方法
def change():
# 在此方法中用global声明c_value为全局变量
# 这样改变c_value的值才会生效
global c_value
while True:
# 间隔比main中改变值延迟10ms,确保值改变后打印值的变化
sleep(1.01)
# 改为每次循环打印c_value的值是否有改变
print(f'c_value: {c_value}')
# 监视器方法
def monitor():
# 循环计数标记
count = 1
while True:
print(f'第 {count} 次循环')
sleep(1)
# 在main.py中每个对应的变量自增1
a.a_value += 1
b.b_value += 1
c.c_value += 1
sleep(1)
a.a_value += 1
b.b_value += 1
c.c_value += 1
sleep(1)
a.a_value += 1
b.b_value += 1
c.c_value += 1
# 圈数在最后自增1
count += 1
我们来看一下运行结果
运行结果如下:
第 1 次循环
c_value: 301
b_value: 201
a_value: 101
a_value: 102
b_value: 202
c_value: 302
第 2 次循环
c_value: 303
b_value: 203
a_value: 103
a_value: 104
b_value: 204
以以上测试结果来说
无论在main.py中修改各个.py文件import导入模块的变量的值
还是在各个.py文件中修改各自变量的值
这个变量的值,在main.py和各个.py文件中的值都是一致的
线程之间变量保持着内存可见性
这一篇主要说明的是在各个异步线程中
变量的值的变化,怎么保证各个线程的值的内存可见性
这个变量的值的同步,在实现异步抽帧法中,是第一步
后续还会陆续更新如何保证同一个时间点多个变量的原子性和深拷贝
可以订阅我的Python专栏来关注最新技术!
长风破浪会有时,直挂云帆济沧海。