问题如何确保我的应用程序是线程安全的?他们的任何常见做法,测试方法,要避免的事情,要寻找的东西是什么?
背景我目前正在开发一个服务器应用程序,它在不同的线程中执行许多后台任务,并使用Indy与客户端进行通信(使用另一组自动生成的线程进行通信).由于应用程序应该是高度可用的,程序崩溃是一件非常糟糕的事情,我想确保应用程序是线程安全的.无论如何,我不时发现一段代码抛出一个以前从未发生过的异常,在大多数情况下我发现它是某种同步错误,我忘了正确地同步我的对象.因此,我的问题涉及最佳实践,线程安全测试和类似的事情.
mghie:谢谢你的回答!我或许应该更准确一点.为了清楚起见,我了解多线程的原理,我在整个程序中使用同步(监视器),我知道如何将线程问题与其他实现问题区分开来.但尽管如此,我仍然忘记不时添加适当的同步.举个例子,我在代码中使用了RTL排序功能.看起来像
FKeyList.Sort (CompareKeysFunc);
事实证明,我必须在排序时同步FKeyList.在最初编写那么简单的代码行时,我才想起它.这是我想谈的这些问题.一个人容易忘记添加同步代码的地方有哪些?您如何确保在所有重要位置添加同步代码?
You can't really test for thread-safeness. All you can do is show that your code isn't thread-safe, but if you know how to do that you already know what to do in your program to fix that particular bug. It's the bugs you don't know that are the problem, and how would you write tests for those? Apart from that threading problems are much harder to find than other problems, as the act of debugging can already alter the behaviour of the program. Things will differ from one program run to the next, from one machine to the other. Number of CPUs and CPU cores, number and kind of programs running in parallel, exact order and timing of stuff happening in the program - all of this and much more will have influence on the program behaviour. [I actually wanted to add the phase of the moon and stuff like that to this list, but you get my meaning.]
我的建议是不要将此视为一个实现问题,并开始将其视为程序设计问题.您需要学习和阅读有关多线程的所有内容,无论它是否为Delphi编写.最后,您需要了解基本原理并在编程中正确应用它们.像关键部分,互斥体,条件和线程这样的原语是操作系统提供的东西,并且大多数语言只将它们包装在它们的库中(这忽略了像Erlang提供的绿色线程之类的东西,但从一开始就是一个很好的观点. ).
我要说的是关于线程的维基百科文章,并通过链接文章开始工作.我已经开始使用Aaron Cohen和Mike Woodring的"Win32多线程编程"一书- 它已经绝版了,但也许你可以找到类似的东西.
编辑:让我简要介绍一下您编辑过的问题.对非数据库的所有访问都需要正确同步才能保证线程安全,并且对列表进行排序不是只读操作.显然,人们需要在列表的所有访问周围添加同步.
但是,随着系统中越来越多的内核,常量锁定将限制可以完成的工作量,因此最好寻找一种不同的方式来设计程序.一个想法是在程序中引入尽可能多的只读数据 - 不再需要锁定,因为所有访问都是只读的.
我发现接口在设计多线程程序时非常有价值.可以实现接口以仅具有对内部数据进行只读访问的方法,如果您坚持使用它们,则可以确定不会发生许多潜在的编程错误.您可以在线程之间自由共享它们,并且线程安全引用计数将确保在最后一次引用它们超出范围或被赋予其他值时正确释放实现对象.
你要做的是创建从TInterfacedObject下降的对象.它们实现了一个或多个接口,这些接口都只提供对对象内部的只读访问,但它们也可以提供改变对象状态的公共方法.创建对象时,保留对象类型的变量和接口指针变量.这样生命周期管理很容易,因为当发生异常时,对象将被自动删除.您可以使用指向对象的变量来调用正确设置对象所需的所有方法.这会改变内部状态,但由于这只发生在活动线程中,因此不存在冲突的可能性.正确设置对象后,将接口指针返回到调用代码,并且由于之后无法访问该对象,除非通过接口指针,您可以确保只能执行只读访问.通过使用此技术,您可以完全删除对象内部的锁定.
如果您需要更改对象的状态怎么办?你没有,你通过从界面复制数据创建一个新的,然后改变新对象的内部状态.最后,将引用指针返回给新对象.
通过使用它,您只需要在获取或设置此类接口的地方进行锁定.通过使用原子交换功能,甚至可以在不锁定的情况下完成.请参阅Primoz Gabrijelcic的博客文章,了解设置接口指针的类似用例.
简单:不要使用共享数据.每次访问共享数据时,都有可能遇到问题(如果忘记同步访问).更糟糕的是,每次访问共享数据时都有可能阻止其他线程,这会损害您的并行化.
我知道这个建议并不总是适用.尽管如此,如果你尽可能地追随它,它也不会受到伤害.
编辑:对Smasher评论的回应更长.不适合评论:(
你是完全正确的.这就是为什么我喜欢在readonly线程中保留主数据的卷影副本.我在结构中添加了一个版本控制(一个4对齐的DWORD)并在(受锁保护的)数据写入器中增加此版本.数据读取器将比较全局版本和私有版本(可以在不锁定的情况下完成),并且只有它们不同时才会锁定结构,将其复制到本地存储,更新本地版本并解锁.然后它将访问结构的本地副本.如果阅读是访问结构的主要方式,那么效果很好.