想知道一些使用多线程出错的原因以及如何避免或解决错误

使用问题 · 1790 次浏览
Y@404 创建于 2023-08-26 20:48

我在使用多线程的时候,有很小的概率(大概1/1000)会出错,系统负载高时(CPU100%)出错的概率也会增大(可能到1/10),一旦出错,就会导致整个任务中断,而多线程对于我是必须品,单线程的效率是我无法接受的,所以想知道出错的原因,以及有没有什么办法可以避免或解决。

 

示例:

我要解散名称为1-1000的总共1000个文件夹,每个文件夹内有一个和文件夹名称相同文件,处理方式为打包的子程序,只有传入参数文件夹路径,没有传出参数。

使用多线程时,系统会报错,在名称为233的文件夹中没有找到233.xxx这个文件(实际上我确认这个文件已经被解散成功,我在上层目录找到了它,此时233文件夹就是个空文件夹,所以它才没有找到233.xxx这个文件,它早就应该被删除,而不是再次进入多线程中)。

 

因为多线程不支持调试,我也做了一些我自认为有效的测试,测试的结果大致意思如下:

在每个步骤中,以多线程,分发1、2、3、4、5、6、7、8、9这9个数字。

我理想中的结果是这9个数字全部分发,且都只分发一次,分发的顺序无所谓,比如分发的结果是:123789456,这是我希望的结果。

但实际上的结果可能是:1224568889。

有些数字会漏发,比如7,有些数字会多发,比如28,于是就导致了错误。

 

我想知道,我以上的测试及猜测是对的吗?

造成这样错误的原因是什么?还是说多线程本身就这样?

如果出现这样的错误,有没有什么办法可以在动作中解决掉这种错误以让动作继续正确的运行下去?


回复内容
CL 2023-08-26 20:58
#1

估计变量冲突了,在一个线程里修改了变量,另一个线程里读取了变量。 

一般的解决方法是,将变量通过参数传入子程序执行,并且启动新线程之前等15ms左右,待变量传入子程序之后再开启下个线程。

Y@404 回复 CL 2023-08-26 21:06 :

我就是把多线程的内容全部放到子程序处理的,在每个步骤里就一个子程序,然后只有一个传入变量,没有任何传出变量,文件之间也不存在任何冲突,但就是会很小的概率报错,报错概率跟CPU占用还正相关……

CL 回复 Y@404 2023-08-26 21:09 :

我觉得不是很可能,这个测试动作发一下:

Y@404 回复 CL 2023-08-26 21:31 :

动作我重新写一下分享给你

这是我在日志数据里找到的不知道有没有用,这段文本经常出现

2023-08-26 21:24:27,858 [106] WARN Quicker.Domain.Actions.X.BuiltinRunners.EachStepRunner - [多线程]出错:发生一个或多个错误。已释放该信号量。

System.AggregateException: 发生一个或多个错误。 ---> System.ObjectDisposedException: 已释放该信号量。

   在 System.Threading.SemaphoreSlim.CheckDispose()

   在 System.Threading.SemaphoreSlim.Release(Int32 releaseCount)

   在 Quicker.Domain.Actions.X.BuiltinRunners.EachStepRunner.<>c__DisplayClass45_1.L2wBYGHa3ad()

   在 System.Threading.Tasks.Task.Execute()

   --- 内部异常堆栈跟踪的结尾 ---

---> (内部异常 #0) System.ObjectDisposedException: 已释放该信号量。

   在 System.Threading.SemaphoreSlim.CheckDispose()

   在 System.Threading.SemaphoreSlim.Release(Int32 releaseCount)

   在 Quicker.Domain.Actions.X.BuiltinRunners.EachStepRunner.<>c__DisplayClass45_1.L2wBYGHa3ad()

   在 System.Threading.Tasks.Task.Execute()<---

Y@404 回复 CL 2023-08-26 22:20 :

https://getquicker.net/Sharedaction?code=2a2e6978-97fe-46f5-9937-08dba5d3aaf5

这就是解散文件夹,我刚刚又试了几次,很容易出错


Y@404 回复 Y@404 2023-08-26 22:24 :

我仔细看了下文件,两个待解散的文件都已解散到了上级目录,原目录也已经成功删除,按理来说,任务已经完美完成,但偏偏系统又报错,也就是两个已经完成的任务又进入到了任务之中……

Y@404 回复 Y@404 2023-08-26 22:38 :

刚刚又试了下,然后提示NO.5文件找不到,然后我发现,排在第二的NO.2这个文件夹并未解散,而NO.01-NO.35除了它都已经解散,之后的文件夹因为系统报错动作就被结束掉了,所以我才猜测,多线程除了会多分发,还漏分发…

Y@404 回复 CL 2023-08-26 23:00 :

然后就是,这个出错的概率跟CPU负载关系非常大,我的CPU是20线程的,所以我也设置的20线程,按理来说应该是一个线程一个任务不冲突,如果只是处理轻任务,系统负载很低,比如多线程更改文件名,就没有出错过一次,但是只要CPU负载高,就经常性的开始出错。

如果线程数设置的非常低,我刚刚试了下6线程和8线程,即便在CPU高负载下也没有出错,但如果使用6/8线程那效率也太低了……(试过10线程也会报错)

这样一看又似乎是跟CPU的线程调度有很大的关系,我的CPU是6大核+8小核总共20线程,但我也没有证据…

CL 回复 Y@404 2023-08-26 23:08 :

确实有点奇怪,我研究下看看。

CL 2023-08-27 16:14
#2

花了一些时间分析了一下这个问题,目前看来主要是这个问题:

线程启动间隔不够大。

当系统比较忙的时候,前一个循环将“项”写入变量中,线程子程序步骤还未读取的时候,下一个循环就到了,将变量改为了新值。这时候前一个线程和后一个线程都读取到了相同的“项”,就是动作里的“选中文件夹列表_每项”这个变量。产生了重复。


建议:

1)增大线程启动间隔到50ms或以上。

2)减少线程数。线程太多,会造成更多的线程处于等待状态,从而容易引发并行执行顺序的混乱,容易发生这个问题。

3)对文件系统的操作,不建议使用多线程。 如果不是固态硬盘,磁盘IO本身的并行能力差,可能多个线程反而互相影响。

4) 移入回收站耗时时间很长,后面版本增加一个删除空目录的操作类型。

CL 最后更新于 2023-08-27 16:46
Y@404 回复 CL 2023-08-27 20:11 :

我是4.0的固态,直连CPU,磁盘应该不是短板。

我曾经把线程启动间隔设为5000ms,但高负载依旧报错…报错概率和5ms似乎没差…真正有效避免出错还得调低线程数…

然后我今天分别尝试了8、12、16、20线程的操作,发现线程的提升与线程数量的提升完全不符合预期,处理相同的任务,分别花了1770、1580、1498、1424秒,也就是20线程相比8线程线程翻倍还多却只提升了24%…最具性价比的是12线程。

线程数调低之后确实不太容易出错,但就是看着那么多线程空在那里不工作,跟小核有难大核围观的既视感一样一样的…

感谢解答。

Y@404 最后更新于 2023-08-27 20:11
CL 回复 Y@404 2023-08-28 06:58 :

是磁盘IO不是线性的。 可以试试把里面文件和目录操作先禁用,应该就会很快了。

CL 2023-08-28 22:21
#3

https://getquicker.net/Help/Versions 增加了一个为线程创建独立上下文的选项,可以试下。

不获全胜不收兵 2023-08-30 20:00
#4

这里面的词典变量是线程安全的吗?

CL 回复 不获全胜不收兵 2023-08-30 20:53 :

不是的。因为多线程本身使用较少,如果都使用线程安全词典,会有一些浪费。 即使普通词典对象,遇到冲突的几率也比较小。

在必要的情况下,可以使用表达式生成一个线程安全的词典对象 $=new ConcurrentDictionary<string, object>(); 


回复主贴