您从一个因多线程不良而几乎/实际上失败的项目中学到了什么?[关闭]


11

您从一个因多线程不良而几乎/实际上失败的项目中学到了什么?

有时,框架会采用某种线程模型,使事情难以正确处理一个数量级。

对于我来说,我还没有从上一次失败中恢复过来,我觉得最好不要在该框架中处理与多线程有关的任何事情。

我发现我擅长处理多线程问题,这些问题具有简单的fork / join,并且数据仅在一个方向上传播(而信号可以在圆形方向上传播)。

我无法处理GUI,其中某些工作只能在严格序列化的线程(“主线程”)上完成,而其他工作只能在除主线程(“工作线程”)之外的任何线程上完成,并且数据和消息必须在N个组件之间完全传播(完整连接的图表)。

在我将该项目移交给另一个项目时,到处都有僵局问题。我听说2-3个月后,其他几位开发人员设法解决了所有僵局问题,以至于可以将其交付给客户。我从未设法找出我所缺少的知识。

关于项目的一些事情:消息ID(描述事件含义的整数值,可以将其发送到另一个对象的消息队列中,而与线程无关,而已)达到数千。唯一字符串(用户消息)也有大约一千。

添加

我从另一个团队得到的最好的类比(与我的过去或现在的项目无关)是“将数据放入数据库”。(“数据库”指的是集中化和原子更新。)在分为多个视图且全部在同一“主线程”上运行且所有非GUI繁重工作都在单个工作线程中完成的GUI中,应用程序的数据应可以将其存储在一个像数据库一样的单一文件中,并让“数据库”处理涉及非平凡数据依赖项的所有“原子更新”。GUI的所有其他部分仅处理屏幕绘图,而没有其他内容。UI部分可能会缓存内容,并且如果设计正确,用户将不会注意到它是否过时了不到一秒钟。此“数据库”也称为“文档” 在文档视图体系结构中。不幸的是-不,我的应用程序实际上将所有数据存储在“视图”中。我不知道为什么会这样。

会员贡献者:

(贡献者不需要使用真实/个人示例。如果您自己认为可信的轶事示例,也欢迎他们提供经验教训。)



我认为能够“在线程中思考”在某种程度上是一种才华,而不是可以学到的东西,因为缺乏更好的措辞。我知道很多开发人员已经在并行系统上工作了很长时间,但是如果数据必须沿多个方向运行,他们就会感到厌烦。
dauphic

Answers:


13

我最喜欢的课程–来之不易!–在多线程程序中,调度程序是讨厌您的狡猾猪。如果事情出了问题,它们将会以意外的方式发生。弄错任何事情,您将追逐奇怪的heisenbug(因为您添加的任何工具都会改变计时,并为您提供不同的运行方式)。

解决此问题的唯一明智的方法是将所有线程处理严格地合并为一小段代码,以使其正常运行,并且对于确保正确持有锁(以及全局固定的获取顺序)也非常保守。 。最简单的方法是在线程之间不共享内存(或其他资源),除非消息必须异步。这样,您就可以采用线程无关的样式编写其他所有内容。(奖金:扩展到集群中的多台计算机要容易得多。)


+1表示“除了必须异步的消息传递外,不要在线程之间共享内存(或其他资源);”
Nemanja Trifunovic

1
唯一方式吗?不可变数据类型呢?
亚伦诺特,2011年

is that in a multithreaded program the scheduler is a sneaky swine that hates you.-不,它没有,它确实按照您的要求做:)
mattnz

@Aaronaught:通过引用传递的全局值即使是不可变的,仍然需要全局GC,并且重新引入了一大堆全局资源。能够使用每线程内存管理是一件很不错的事,因为它使您摆脱了一堆全局锁。
多纳研究员

不是说您不能通过引用传递非基本类型的值,而是需要更高级别的锁定(例如,“所有者”持有引用直到消息返回,这很容易在维护中弄乱)或消息传递引擎中的复杂代码来转移所有权。或者,您将所有内容编组在一起,然后在另一个线程中取消编组,这要慢得多(无论如何,在进入群集时都必须这样做)。追逐追逐和根本不共享内存更加容易。
多纳研究员

6

这是我现在可以想到的一些基本课程(不是从项目失败中获得,而是从实际项目中看到的实际问题中得出):

  • 保留共享资源时,请尝试避免任何阻塞呼叫。常见的死锁模式是线程获取互斥锁,进行回调,在同一互斥锁上进行回调。
  • 用互斥/关键部分保护对任何共享数据结构的访问(或使用无锁的部分-但不要自己动手!)
  • 不要假设原子性-使用原子API(例如InterlockedIncrement)。
  • 有关正在使用的库,对象或API的线程安全性的RTFM。
  • 利用可用的同步原语,例如事件,信号量。(但是在使用它们时要格外注意,您知道自己处于良好状态-我已经看到了许多以错误状态表示事件的示例,这些事件会丢失事件或数据)
  • 假设线程可以同时执行和/或以任何顺序执行,并且上下文可以随时在线程之间切换(除非在做出其他保证的OS下)。

6
  • 您的整个GUI项目应仅从主线程调用。基本上,您不应该在GUI中添加单个(.net)“调用”。多线程应该卡在处理数据访问速度较慢的单独项目中。

我们继承了一个部分,其中GUI项目使用了十几个线程。除了问题,它什么都没有。死锁,赛车问题,跨线程GUI调用...


“项目”是指“组装”吗?我看不到程序集之间的类分布如何会导致线程问题。
尼基,2011年

在我的项目中,它确实是一个程序集。但要点是,这些文件夹中的所有代码都必须从主线程中调用,没有例外。
卡拉

我认为该规则通常不适用。是的,永远不要从另一个线程调用GUI代码。但是如何将类分配到文件夹/项目/程序集是一个独立的决定。
尼基,

1

Java 5和更高版本具有Executors,旨在使处理多线程fork-join风格程序的工作变得更轻松。

使用那些,将消除很多痛苦。

(是的,这是我从一个项目中学到的:))


1
将此答案应用于其他语言-尽可能使用该语言提供的高质量并行处理框架。(但是,只有时间才能证明框架是否真的非常好并且可以使用。)
rwong 2011年

1

我在硬实时嵌入式系统方面有背景。您无法测试是否存在多线程导致的问题。(您有时可以确认存在)。代码必须证明是正确的。因此,围绕所有线程交互的最佳实践。

  • #1规则:吻-如果不需要线程,请不要旋转。尽可能序列化。
  • #2规则:不要打破#1。
  • #3如果您无法通过审查证明它是正确的,那是不正确的。

规则1的+1。我正在开发一个项目,该项目最初将被阻塞直到另一个线程完成为止-本质上是一个方法调用!幸运的是,我们决定不采用这种方法。
Michael K

#3 FTW。花费数小时来研究锁定时序图或您用来证明其良好性的任何方法,总比花几个月的时间思考为什么有时会崩溃要好。

1

我去年参加的关于多线程课程的类比非常有帮助。线程同步就像交通信号一样,保护交叉路口(数据)不被两个汽车(线程)同时使用。许多开发人员犯的错误是,在整个城市的大部分地方都把红色的信号灯点亮,让一辆汽车通过,因为他们认为弄清楚他们需要的确切信号太困难或危险。当流量很小时,这可能会很好地工作,但是随着应用程序的增长,它会导致陷入僵局。

从理论上我已经知道了这一点,但是在那堂课之后,类比的确在我心中震撼了,令我惊讶的是,在那之后我有多频繁地调查一个线程问题并找到一个巨大的队列,或者在写变量的过程中到处都禁用了中断只有两个线程被使用,或者互斥锁被长时间保留,可以完全避免重构。

换句话说,一些最糟糕的线程问题是由过分尝试避免线程问题引起的。


0

再试一次。

至少对我来说,造成差异的是实践。在完成多线程和分布式工作很多次之后,您就可以掌握它了。

我认为调试确实使它变得困难。我可以使用VS调试多线程代码,但是如果必须使用gdb的话,我真的会感到茫然。可能是我的错

正在学习更多的另一件事是无锁数据结构。

我认为,如果您指定框架,则可以真正改善此问题。例如,.NET线程池和后台工作程序实际上与QThread不同。总是有一些平台特定的陷阱。


我有兴趣听取来自任何框架的故事,因为我相信每个框架都需要学习一些东西,尤其是那些我从未接触过的东西。
rwong 2011年

1
调试器在多线程环境中基本上没有用。
Pemdas'5

我已经有多线程执行跟踪器,它可以告诉我问题出在哪里,但不会帮我解决问题。我的问题的症结在于:“根据当前的设计,我无法以这种方式(顺序)将消息X传递给对象Y;必须将其添加到巨型队列中并最终将其处理;但是由于这个原因有没有办法让信息出现在正确的时间,用户-它永远anachronisticly发生,使用户非常,非常,混淆你甚至可能需要添加进度条,取消按钮或错误信息的地方它不应该”没有那些。”
rwong 2011年

0

我了解到,从较低级别的模块到较高级别的模块的回调是一个巨大的弊端,因为它们会导致以相反的顺序获取锁。


回调不是邪恶的……事实上,除了线程中断之外,它们还做其他任何事情,这可能是邪恶的根源。我非常怀疑任何不只是将令牌发送到消息队列的回调。
Pemdas

解决优化问题(如最小化f(x))通常是通过向优化过程提供指向函数f(x)的指针来实现的,该指针在寻找最小值时会“回调”。没有回调怎么办?
quant_dev

1
没有下注,但是回调不是邪恶的。持有锁时调用回调是邪恶的。当您不知道锁中是否有锁或等待时,请勿调用任何内容。这不仅包括回调,还包括虚拟函数,API函数以及其他模块(“较高级别”或“较低级别”)中的函数。
nikie 2011年

@nikie:如果必须在回调过程中保持锁定,则需要将API的其余部分设计为可重入(很难!),或者将持有锁定的事实作为API的记录部分(不幸的是,但有时您可以做的一切)。
多纳研究员

@Donal研究员:如果必须在回调过程中保持锁定,我想说您有设计缺陷。如果真的没有其他方法,那就可以,一定要记录下来!就像您将记录是否在后台线程中调用回调一样。那是界面的一部分。
尼基,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.