凡是多线程编程,几乎就离不开同步和锁定这个话题。在深入探讨之前,首先我们了解下同步和锁定是怎么回事:
同步是为了在多线程中串行化对公共资源的访问而采取的一种策略,相关的对象在 Delphi 中在 syncobjs 和sysutils 单元。一般来说,多线程读取在整个应用程序生存周期内,始终保持不变的公共资源是安全,就象一个广播,一群人都可以听到相关的信息,而不用担心出现任何问题。但如果我们要同时写一个公共的资源,那么以一个多重读独占写类型(TMultiReadExclusiveWriteSynchronizer,多读单写,也叫共享读独占写)的同步对象来说明其同步策略:
1、读取数据
(1)、BeginRead:检查是否正在进行写入操作,如果进行写入操作,则等待写入操作完成,以保证读取的数据是完整的,如果不等待,显然很可能会读到不完整的数据。如果没有,则进入共享读的状态。
(2)、线程读取数据内容,在此绝不能对公共资源进行写入。这是一个逻辑上的强制规则,但在编码实现却没有必然的约束,这就造成有些时候,错误的写入,会造成数据混乱。
(3)、EndRead:读取完成后,退出共享读状态。
2、写入数据
(1)、BeginWrite:检查是否有其它线程处于读或写的状态,如果处于该状态,则需要等待其它线程退出读写状态。无论其它线程是读还是在写,都不能进入写入状态,以避免破坏数据的完整性。一旦一个线程锁定了公共资源,其它线程就不能访问公共资源,必需同步等待这个线程对资源的锁定解除。
(2)、获得锁的线程修改公共资源内容。
(3)、EndWrite:写入完成后,退出独占写状态,锁被释放。
(4)、其它线程在获得执行时间片时,进行自己的读或写尝试。
理论上来说,多重读独占写的这种同步对象是最理想的锁,但由于其要求程序员在编码时必需注意到读和写两种不同的锁的区别,并保证相应的操作是没有问题的。而由于各种语法糖的引入,使程序员在开发过程中,有时候很难准确的满足这一要求。
临界(TCriticalSection)是另一个类型的同步对象,它与上面的多读单写类型的同步对象的区别是它不区分读和写操作,统一按照最糟糕的情况来考虑,也就是说,无论是读还是写,都同时只能有一个线程在执行,其它线程只能等待它执行完成才能获取执行机会。其同步策略可以描述为:
1、请求进入临界(Enter);
2、执行读或写代码;
3、离开临界(Leave);
事件(TEvent)是另一个重要的同步对象,它只是一个信号。这个信号一旦被激活(SetEvent),那么直到重置(ResetEvent)前,每一个得到机会执行的线程都会退出等待状态。但事件有一个自动重置标志位,它在一个线程接收到信号退出等待状态后,就会自动重置为无信号状态,从而保证同时只有一个线程进入执行状态。事件实际上是一种通知,一般来说,其执行策略可以描述为:
1、需要等待某些数据就绪的线程进入等待状态(WaitFor);
2、数据就绪后,触发信号(SetEvent)通知等待线程数据就绪;
3、等待中的线程处理数据,完成后重新进入等待状态;
还有多个同步对象类型,如互斥(TMutex)我们常用它来保证程序只运行一个实例,还有信号量(TSemaphore)、自旋锁(TSpinLock)、计数器(TCountdownEvent)等等,限于篇幅和时间,我们不再展开说明。
更多的关于同步对象和锁的信息,可以参考 MSDN 相关的文章:点此访问