问题如何确保我的应用程序是线程安全的?他们的常见做法,测试方法,要避免的事情,要找的东西是什么?
背景我目前正在开发一个服务器应用程序,它在不同的线程中执行许多后台任务,并使用Indy与客户端进行通信(使用另一组自动生成的线程进行通信)。由于应用程序应该是高度可用的,程序崩溃是一件非常糟糕的事情,我想确保应用程序是线程安全的。无论如何,我不时发现一段代码抛出一个以前从未发生过的异常,在大多数情况下我发现它是某种同步错误,我忘了正确地同步我的对象。因此,我的问题涉及最佳实践,测试线程安全等等。
mghie:谢谢你的回答!我或许应该更准确一点。为了清楚起见,我了解多线程的原理,我在整个程序中使用同步(监视器),我知道如何区分线程问题和其他实现问题。但尽管如此,我仍然忘记不时添加适当的同步。举个例子,我在代码中使用了RTL排序功能。看起来像FKeyList.Sort (CompareKeysFunc);
事实证明,我必须在排序时同步FKeyList。在最初编写那么简单的代码行时,我才想起它。这是我想谈的这些问题。一个人容易忘记添加同步代码的地方有哪些?您如何确保在所有重要位置添加同步代码?
答案 0 :(得分:16)
您无法真正测试线程安全性。您所能做的就是证明您的代码不是线程安全的,但如果您知道如何做到这一点,那么您已经知道如何在您的程序中修复该特定错误。这是您不知道问题的错误,您将如何编写测试?除了线程之外,问题比其他问题更难找到,因为调试行为已经可以改变程序的行为。从一台机器到另一台机器,从一台机器到另一台机器的运行情况会有所不同。 CPU和CPU内核的数量,并行运行的程序的数量和种类,程序中发生的事件的确切顺序和时间 - 所有这些以及更多将影响程序行为。 [我实际上想把这个月亮的阶段和类似的东西添加到这个列表中,但是你得到了我的意思。]
我的建议是停止将此视为一个实现问题,并开始将其视为程序设计问题。您需要学习和阅读有关多线程的所有内容,无论它是否为Delphi编写。最后,您需要了解基本原理并在编程中正确应用它们。像关键部分,互斥体,条件和线程这样的原语是操作系统提供的东西,并且大多数语言只将它们包装在它们的库中(这忽略了像Erlang提供的绿色线程之类的东西,但从一开始就是一个很好的观点。 )。
我想从Wikipedia article on threads开始,逐步完成链接的文章。我已经开始阅读Aaron Cohen和Mike Woodring的书"Win32 Multithreaded Programming" - 它已经绝版了,但也许你可以找到类似的东西。
修改:让我简要介绍一下您编辑过的问题。所有对非数据的数据的访问都需要正确同步才能保证线程安全,并且对列表进行排序不是只读操作。显然,人们需要在列表的所有访问中添加同步。
但是,随着系统中越来越多的内核,常量锁定将限制可以完成的工作量,因此最好寻找一种不同的方式来设计程序。一个想法是在程序中引入尽可能多的只读数据 - 不再需要锁定,因为所有访问都是只读的。
我发现接口在设计多线程程序时非常有价值。可以实现接口以仅具有对内部数据进行只读访问的方法,如果您坚持使用它们,则可以确定不会发生许多潜在的编程错误。您可以在线程之间自由共享它们,并且线程安全引用计数将确保在最后一次引用它们超出范围或被赋予其他值时正确释放实现对象。
您要做的是创建从TInterfacedObject下降的对象。它们实现了一个或多个接口,这些接口都只提供对对象内部的只读访问,但它们也可以提供改变对象状态的公共方法。创建对象时,保留对象类型的变量和接口指针变量。这样生命周期管理很容易,因为当发生异常时,对象将被自动删除。您可以使用指向对象的变量来调用正确设置对象所需的所有方法。这会改变内部状态,但由于这只发生在活动线程中,因此不存在冲突的可能性。正确设置对象后,将接口指针返回到调用代码,由于除了通过接口指针之外无法访问对象,因此可以确保只能执行只读访问。通过使用此技术,您可以完全删除对象内部的锁定。
如果您需要更改对象的状态怎么办?你没有,你通过从界面复制数据创建一个新的,然后改变新对象的内部状态。最后,将引用指针返回给新对象。
通过使用此功能,您只需要在获取或设置此类接口的位置进行锁定。通过使用原子交换功能,甚至可以在不锁定的情况下完成。请参阅Primoz Gabrijelcic的this blog post,了解设置接口指针的类似用例。
答案 1 :(得分:6)
简单:不要使用共享数据。每次访问共享数据时,都有可能遇到问题(如果忘记同步访问)。更糟糕的是,每次访问共享数据时,都有可能阻止其他线程,这会损害您的并行化。
我知道这个建议并不总是适用。尽管如此,如果你尽可能地追随它,它并没有受到伤害。
编辑:对Smasher评论的回应更长。不适合评论:(你完全正确。这就是为什么我喜欢在readonly线程中保留主数据的卷影副本。我在结构中添加了一个版本控制(一个4对齐的DWORD)并在(受锁保护的)数据写入器中增加此版本。数据读取器将比较全局版本和私有版本(可以在不锁定的情况下完成),并且只有它们不同时才会锁定结构,将其复制到本地存储,更新本地版本并解锁。然后它将访问结构的本地副本。如果阅读是访问结构的主要方式,那么效果很好。
答案 2 :(得分:2)
我将第二个mghie的建议:设计线程安全。尽可能在任何地方阅读。
要了解它的实现方式,请查看有关实时操作系统内核内部的书籍。一个很好的例子是Jean J. Labrosse的MicroC/OS-II: The Real Time Kernel,其中包含完整的带注释的源代码到工作内核,并讨论为什么事情按照它们的方式完成。
修改:根据改进的问题重点关注使用RTL功能......
多个线程可以看到的任何对象都是潜在的同步问题。线程安全对象在每个方法的实现中遵循一致的模式,在方法的持续时间内锁定对象状态的“足够”,或者可能缩小到“足够长”。当然,对象状态的任何部分的任何读 - 修改 - 写序列必须相对于其他线程以原子方式完成。
艺术在于弄清楚如何在没有死锁或创建执行瓶颈的情况下完成有用的工作。
至于发现这些问题,测试将不是任何保证。可以修复测试中出现的问题。但是为线程安全编写单元测试或回归测试是非常困难的...所以面对现有代码的一部分,你可能的求助是不断的代码审查,直到线程安全的实践成为第二天性。
答案 3 :(得分:2)
正如人们提到的那样,我想你知道,一般来说,确定你的代码是线程安全的是不可能的(我相信这是不可能的,但我必须追查这个定理)。 当然,你想让事情变得更容易。
我尝试做的是:
答案 4 :(得分:2)
在考虑线程安全性和实现它的可能性时,我只想添加两个我认为有用的讨论链接:
答案 5 :(得分:1)
我的简单答案与这些答案相结合:
因此,它通常很容易陷入这种习惯/习惯,但需要一些时间来适应:
使用函数编程语言(如F#)编程逻辑(而不是UI),甚至使用Scheme或Haskell。函数式编程也促进了线程安全实践,同时它还警告我们始终在函数式编程中编写纯度代码。 如果使用F#,那么使用可变或不可变对象(如变量)也有明显的区别。
由于方法(或简单的函数)是F#和Haskell中的一等公民,因此您编写的代码也会对不太可变的状态更加严格。
同样使用通常可以在这些函数式语言中找到的惰性评估样式,您可以确保您的程序是安全的,不会受到影响,并且您还会意识到如果您的代码需要效果,则必须明确定义它。考虑到IF副作用,那么您的代码就可以利用代码中的组件和多核编程中的可组合性。
答案 6 :(得分:0)
M2C - Java Concurrency in Practice非常好。