编程经典案例:当线程遇到For循环,一个不可思议的Bug就出现了!
wptr33 2025-07-21 18:18 54 浏览
我们公司有个项目,需要视觉定位,大致就是在产品上会有一个“十字”形状的Mark标记,然后通过视觉相机连续拍照,然后将拍到的图片进行视觉算法运算,最终得出Mark标记的位置,然后根据其位置对设备进行位置纠正。但是,想不到就这么一个小功能却出现了一个Bug,找了很久才发现原因!
说这个Bug之前,我需要先说下这个Bug是怎么被我写出来的!
甲方那有一个设备,设备上有一个可以前后移动的轴,下称M轴,轴上又装了一个可以左右移动的轴,类似于贴片机的构造吧。我们用来视觉定位的相机就是装在那个可以左右移动的轴上面的,下面简称P轴。
Mark标记处于P轴的初始位置,而且Mark的左右视野范围被设定成不可超过P轴的初始位置,因此,要用Mark进行定位,只需要M轴前后移动,相机进行拍照即可,最后再根据Mark的位置调整P轴的位置。
最开始,我选择的是一张一张得拍,并且我在封装相机SDK的时候,取图我只封装了一个取单张图片的接口。
后来发现算法速度跟不上M轴移动的速度,当相机拍了一张照片,然后程序再对照片进行视觉运算,中间会产生一定时间的间隙,当程序算完以后,M轴已经移动很远了,拍到下一张图片的时候,信息就会丢失一部分空间画面。
如果Mark恰巧处于这个空间之内,那么很大概率就拍不到Mark标记了!
所以,我又在相机SDK里面重新封装了一个接口,用来批量取图,然后返回图片地址列表,大致的逻辑就是根据视觉相机设置的帧率乘以M轴移动的总时间,换算出相机应该拍照的总照片数,假设相机帧率是10帧每秒,M轴总移动时间是3秒,那么应该取出的图片就是10*3=30张。
这么做的目的就是在M轴移动的时候,纯拍照片,然后将拍到的图片保存在缓存目录里面,其他什么都不做,这样能让视觉相机使用最大帧率去取图,从而覆盖M轴行程内的所有空间。最后,循环所有已经拍到的图片,总有一张或者几张图片里面存在Mark标记!最后,再一次性返回所有图片的保存地址。
虽然这个方案很好,但是在实际运行过程中却出现了一个小的Bug,那就是Mark的检测结果和实际展示图片对不上!并且,还不是每次都会出现这样的Bug,有时候就是好的。
症状就是,很多时候显示的图片里面明明没有Mark标记,但是算法却能识别出Mark标记来,或者有Mark标记,但是Mark标记被检测出来的上下位置却不一样,可偏偏结果还是正确的!
最开始,我以为是算法有问题,后来这个问题很快被排除了。
那会不会是在循环图片时,把图片索引搞错了呢?我仔细看了下我循环图片,对图片进行视觉运算的那段代码,也没发现问题。
本来,这个问题其实很好解决的,只要下个断点调试一下就能发现问题出在哪,可偏偏软件已经部署在了甲方那里,问题只能通过我们公司放在甲方那边的同事跟我复述,我通过在代码里面加运行日志来分析问题出现的原因。
因为日志总归是人加的,它只能被加在我可能认为会出问题的地方,一些我不认为会出现问题的地方我是不会加的。
但偏偏有一个地方我就忽略了!
前面说了,我在视觉相机的SDK里面重新封装了一个批量取图的接口,而这个接口的逻辑是通过一个触发拍照命令,输入指定数量的图片和取图时间来取图的,取出来的图是放在一个指针数组里面的,我需要循环将指针转换成图片对象,然后再保存到缓存目录里。
因为图片数量太多,为了保证转换效率,所以,我在循环指针数组的时候,使用了线程池来转换图片,问题就出在这!
我外层使用的是一个普通的循环,因为我不光要取图,还需要保存图片,而图片名称的某一个规则就是循环的当前索引,如“20250624_0_.jpg”,其中“_0_”中的0就是循环索引i的值。
因为使用的是线程池来执行取图逻辑,那么在当前线程里面直接取i是有问题的!因为i的作用域是在循环内部,但是,循环内部如果执行的是一个线程,那么每一个线程所获得到的i和这个线程的作用域不一样,那么就会导致这个线程取到的i实际并不是顺序的!
这么说不理解的话,我举一个极端的例子:
假设我们需要拍30张图片,这30张图片被存在了一个指针数组里,我们通过For循环去循环指针数组,每循环一次,在线程池里面加一个线程,用来将指针转换成图片,而此时,我们PC的线程数已经被其他程序占用,因此,这30个线程都处于等待状态,直到最后一次循环,此时i为30,循环完成后,此时前面被占用的线程瞬间被释放,前面等待的这30个线程将全部执行,此时,这30个线程里面取到的所有i值将都是30!
而此时保存图片,所有图片名称都是一样的,因为图片保存走的是覆盖流程,最后程序执行下来,只会保存住一张图片!
如果还不懂,请看示例:
这就是导致前面所有问题的根源!因为我在执行批量取图之后,我需要重新循环图片地址列表去读取图片,然后对图片进行视觉运算以及显示。
结合上面所说,如果最后30张图片所有的i都是30的话,那么最终保存的图片数量就只有一张了,此时,图片数量就是1。
此时,我如果使用For循环这么写:
For(int I = 0;i<1;i++)
这么一来,这个循环索引0所获取到的图片实际上是 “20250624_30_.jpg”,此时,如果视觉算法通过了,我会将图片索引保存,此时保存的所谓的图片索引就是0。
显然,索引为0的图片并不存在,因此,在图片显示的时候,这时候画面上就只有Mark标记框,而没有实际图片。
那假设批量取图数量是30,但是因为线程的原因,最终保存在文件夹里的图片只有15张会出现什么结果呢?
很显然,这时候循环索引和所有图片的实际索引就都错位了!
我也在批量取图后的那个循环读取图片的For循环里面也加过日志,但是恰巧当时这个问题没出现,我以为程序已经没问题了,所以就将日志给去除了,后来再出问题时,就没往这方面想了。
至于为什么不保存图片地址而保存图片循环的索引,这个是业务逻辑决定的。
当然,这么写会出问题我是知道的,但是,您也知道,有时候写代码的事很难说,一个不注意写出一个Bug其实很难发现的。
直到我查遍所有代码,把所有可能性都一一排除以后,最后瞄了一眼我写的相机SDK,一眼就发现了问题所在!
总结
在For循环中使用线程,并且在线程里面使用For循环的索引导致的错误,其实这是一个很经典的线程安全案例,我在过去接触到的很多年轻的程序员在使用线程的时候基本上都会遇到这个问题。
如何避免这个问题呢?其实很简单,就是强制将For循环的作用域限制到当层循环之内,写法就是:
前后对比下,我只是将直接使用i的方式改成了先将i这个循环索引先赋值给变量index,然后再去使用,这样,index这个变量的作用域就会被强制锁定在当前循环内,不管最终线程什么时候执行,index的值不会变!
相关推荐
- Python字符串终极指南!单引号、双引号、三引号区别全解析
-
导语:Python中字符串(str)是最核心的数据类型!无论你是输出"HelloWorld"还是处理用户数据,都离不开它。今天彻底讲清字符串的三大定义方式及其核心区别,新手必看!...
- python 字符串的定义和表示_python字符串的用法
-
在Python中,字符串是一序列字符的集合。定义一个字符串可以使用单引号或双引号括起来的字符序列。...
- 简单的python-熟悉字符串相关的操作
-
str.py:#-*-coding:utf-8-*-#测试函数deff():#字符串使用单引号定义s1='test'print(s...
- Python初学者:3招搞定长字符串逐行读取,代码超简单
-
刚学Python的小伙伴,是不是遇到过这种尴尬情况?拿到一段老长的多行字符串——比如从文档里复制的日志、一段带换行的文章,想一行一行处理,如果直接打印全堆在一起,手动切又怕漏行,咋整啊?别慌!今天就给...
- Python 字符串_python字符串型怎么表达
-
除了数字,Python还可以操作字符串。字符串的形式是单引号('......')双引号(''.........'')或三个单引号(''&...
- 贴身口语第二关:请求帮忙、道歉、指路、接受礼物
-
02-@askforhelp请求协助1.F:Excuseme.Canyouhelpme?M:Yes,whatcanIdoforyou?...
- NBA赛季盘点之九大装逼&炫技时刻:“歪嘴战神”希罗领衔
-
欢迎大家来到直播吧NBA赛季盘点,历经许多波折,2019-20赛季耗时整整一年才圆满收官。魔幻的一年里有太多的时刻值得我们去铭记,赛场上更是不乏球员们炫技与宣泄情绪的装逼时刻,本期盘点就让我们来回顾一...
- 一手TTS-2语音合成模型安装教程及实际使用
-
语音合成正从云端调用走向本地部署,TTS-2模型作为开源语音生成方案之一,正在被越来越多开发者尝试落地。本篇文章从环境配置到推理调用,详尽拆解TTS-2的安装流程与使用技巧,为语音产品开发者提供...
- 网友晒出身边的巨人 普通人站一旁秒变“霍比特人”
-
当巨人遇到霍比特人,结果就是“最萌身高差”。近日网友们晒出了身边的巨人,和他们站在一起,普通人都变成了“霍比特人”。CanYouTellWho'sRelated?TheDutchGiant...
- 分手后我们还能做朋友吗?_分手后我们还能做朋友吗
-
Fewrelationshipquestionsareaspolarizingaswhetherornotyoushouldstayfriendswithanex.A...
- 如何用C语言实现Shellcode Loader
-
0x01前言之前github找了一个基于go的loader,生成后文件大小6M多,而且细节不够了解,一旦被杀,都不知道改哪里,想来还是要自己写一个loader...
- 微星Z490如何装Windows10系统以及怎么设 BIOS
-
小晨儿今天给大家讲一下msi微星Z490重怎样装系统以及怎么设置BIOS。一、安装前的准备工作1、一、安装前的准备工作1、备份硬盘所有重要的文件(注:GPT分区转化MBR分区时数据会丢失)2...
- 超实用!互联网软件开发人员不可不知的 Git 常用操作命令
-
在互联网软件开发的协作场景中,Git是不可或缺的版本控制工具。掌握其核心命令,能让代码管理效率大幅提升。本文精选Git高频实用命令,结合场景化说明,助你快速上手。仓库初始化与克隆...
- AI项目的持续集成持续部署实践_ai 项目
-
在独立开发AI工具的过程中,笔者逐步实践了一套高效的软件项目持续集成与持续部署(CI/CD)流程。这套流程以Git、GitHub和Vercel为核心,实现了从代码提交到生产环境上线的全链路自动化。这篇...
- 总结几个常用的Git命令的使用方法
-
1、Git的使用越来越广泛现在很多的公司或者机构都在使用Git进行项目和代码的托管,Git有它自身的优势,很多人也喜欢使用Git。...
- 一周热门
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)