翻译:taowen(taowen.bitapf.org)
原文:《indy in depth》 concurrency
在多线程的环境,资源必须得到保护,使得它们不会因为一次允许多于一个线程访问而受损。
并发和线程是相互纠缠的问题,选择先学哪个也许很难。本文将先讲讲并发,它将为后面学习线程准备一些该先了解一下的知识。
术语
并发
并发是这样一个状态——许多task同时启动。当并发被实现得恰恰当当时,它可能被认为是“harmony”。而实现得糟糕时,就成了”chaos“。
在大部分情况中,所说的task指的都是线程。然而,task也可以是进程或者纤程。
两者之间的分界通常很清楚,而使用合适的技术才是关键
contention
确切的说何为contention?contention就是当多于一个task尝试着同时访问那独独一个资源时的情况。
如果你是在大家庭长大的孩子,可能这个比喻能很好的解释它意思。想想家里要是有六个小孩,妈妈把一块小匹萨放在桌上作为晚餐,会发生什么样的情况。那就是contention的含义。
无论何时,只要多个并发的task需要用读/写的方式访问数据,对数据的访问都必须得到控制从而保护它的完整性。如果访问没有得到控制,两个或者更多的task可能会“崩溃”。当其中的一个尝试着要读取变量时,另外一个可能要同时对它进行写入。如果一个task正在写,而另外一个正在读,那个读的task可能读取了部分写入的数据从而获得的是损坏了的数据。一般这样的操作是不会立即导致异常的,而只会在这之后给程序带来错误。
contention问题经常是在低流量的implementation中不会出现,因而在开发阶段经常是一点问题都没有。所以在开发的阶段应该采用合适的技术和压力测试。否则就有一些像玩russian roulette,问题在开发阶段仅仅是偶尔出现但是在部署阶段变成了频繁出现。
资源保护
资源保护是用来阻止由contention带来的问题的解决办法。资源保护的目的是一次仅让一个task访问指定的资源。
解决contention
无论何时,只要多个线程需要以读/写的方式访问数据,对数据的访问都必须得到控制从而保护它的完整性。这可能对于不熟悉线程操作的程序员来说intimidating。然而,大部分服务器不需要全局数据。这些程序一般在启动过程中初始化之后只需要读取数据。只要没有写操作,线程可以没有任何副作用的读
取全局数据。
下面讲的是解决contention最常用的办法。
只读
最简单的办法是只读。任何简单类型(整数,字符串,内存)以只读的方式访问不需要任何保护。这也可以扩展到诸如tlists等许多复杂类型。只要它们不以读/写方式访问任何全局或者成员变量,类型在只读方式时都是安全的。
此外,资源可以在任何可能的读操作之前被改写。这允许了在读取它的task启动之前,先初始化资源。
atomic操作
有一种方法是说如果操作是atomic,资源不要被保护。atomic操作是这样一种操作,它太小了以至于不能被计算机处理器分划开来。因为它尺寸小,从而它不会受到contentiion的影响因为它将由自身执行而且在执行过程中不会有task的切换。一般情况下,atomic操作是被编译为一条汇编指令的源代码。
典型的任务诸如读取或者写入一个整型或者布尔型变量被认为是atomic操作,因为它们被编译为一条move指令。然而我推荐你绝对不要依赖原子操作,因为某些情况下甚至写入一个整型或者布尔型变量都能包括多于一个的动作,这要看数据首先是从哪儿读来的。此外,这还依赖于编译器内部的奥秘,而这可能会在不告知你的情况下做出改变。依赖源代码级的atomic操作将产生未来会有问题的代码而且可能在多处理器的机器或者别的操作系统上行为非常不同。
我曾经见过一个铁打不动的atomic操作。然而一个非常prominent的未来事件证明了我的观点,那就是.net。你的代码首先编译为il,然而再编译为机器码,可能还是在不同厂商的不同平台上,你还能确信你的代码最终还是atomic操作吗?
选择最终还是要看你自己,当然有许多声音围绕着对atomic操作的偏爱和反对。在大部分情况下,依赖原子操作仅仅节省了几毫秒,以及几字节的代码。我强烈推荐不要去使用atomic操作,因为它们带来的好处如此至少而liabilities如此之巨大。把所有操作都当作非atomic操作来对待。
操作系统的支持
许多操作系统对非常基本的线程安全的操作提供了支持。
windows支持一套称为interlocked function的函数。这些函数的用处非常有限,而且仅仅包括简单的对整数的操作,诸如步增,步减,加,swap以及swap-compare。
函数的数目和windows的版本有关,而且可能在低版本的windows上发生死锁。在大部分应用程序中,它们提供的性能上的好处非常少。
因为综合这些,用处有限,不断变化的支持,可怜的性能优势因素,建议你用indy的线程安全的同等物来替代。
windows还包括对特殊的ipc(进程间通信)对象的支持,这些对象在delphi有经过包装的类。这些对象和ipc一样对线程操作极端有用。
显式保护
显式保护包括每个task都知道一个资源受到了保护而且在访问这个资源之前采取了显式的防御步骤。一般这样的代码式在被多个task并发执行的函数之中,或者被封装到了一个被许多不同位置调用的函数之中作为一个线程安全的封装。
显示保护一般要利用资源保护对象。简单来说,资源保护对象把对资源的访问限制为了一次一task。资源保护对象并没有实际限制对资源的访问,如果它做到了,它可能必须要知道每一个和所有的资源类型的细节。它就像红绿灯,而代码要遵守它并给它提供输入。每个资源保护对象用不同的机理,不同的输入,以及不同程度的额外负担实现了不同种类的红绿灯。这使得能够选择不同的资源保护对象来更好的适应不同类型的资源以及不同的场合。
资源保护对象以不同形式存在,下面就来逐个介绍。
critical section
critical section可以用来控制对全局资源的访问。critical section是轻量级的并且在vcl中实现于tcriticalsection之中。简单来说,critical section使得多线程程序中的一个线程能够暂时阻塞所有其他线程尝试使用同一个critical section。critical section就像红绿灯,只当前面的路没有任何车辆时才变绿。critical section可以用来确保一次只有一个线程正在执行那一块代码。因此,受到critical section保护的那块代码应当尽可能的小因为如果使用不当的话它们可能严重影响性能。所以,每块代码都应当使用它们自己的tcriticalsection,而不是重用全程序共享的tcriticalsection。
要进入critical section,使用enter方法,而leave方法是用来退出critical section的。tcriticalsection还分别有acquire和release方法来做与enter和leave完全一样的事情。
假设有一个服务器需要记录有关登陆了的客户端的信息,并且要在主线程中显示这些信息。一个可能的选择是使用synchronize。然而使用这个方法在同时有许多客户登陆时会对连接线程产生性能上的负面影响。取决于服务器的需要,一个更好的选项可能是记录下信息并且让主线程用timer来读取这些信息。下面的代码是一个使用了critical section的这种技术的例子。
var
glogcs: tcriticalsection;
guserlog: tstringlist;
procedure tformmain.idtcpserver1connect(athread: tidpeerthread);
var
s: string;
begin
// username
s := readln;
glogcs.enter; try
guserlog.add('user logged in: ' + s);
finally glogcs.leave; end;
end;
procedure tformmain.timer1timer(sender: tobject);
begin
glogcs.enter; try
listbox1.items.addstrings(guserlog);
guserlog.clear;
finally glogcs.leave; end;
end;
initialization
glogcs := tcriticalsection.create;
guserlog := tstringlist.create;
finalization
freeandnil(guserlog);
freeandnil(glogcs);