870920 Menu

CriticalSection临界区与ScopedLock作用域锁

多线程环境下,对共享数据的保护(线程间的互斥)是件很痛苦的事。使用JUCE类库编程,可采用CriticalSection临界区和预定义的ScopedLock作用域锁对象来解决此问题,方便、有效而简单。

JUCE没有Semaphore(信号量)一说,也没有Mutex(互斥)一说,CriticalSection临界区即相当于互斥。其实现多线程环境下对共享数据和共享资源的保护,通过某个作用域范围内锁对象对临界区对象进行加锁和解锁来实现。临界区对象被锁对象加锁后,锁对象所位于的作用域内的数据不能再被其他线程读写。代码执行到作用域结束位置时,锁对象自动解锁。

锁对象的加锁与解锁基于RAII技术。即:同一时间,不管有多少个线程,最多只能有一个线程拥有已锁定的CriticalSection临界区对象,其他线程要拥有此对象,必须等待临界区对象解锁。而对临界区对象的加锁和解锁,通过作用域锁对象来进行(使用锁对象的原因之一是防止直接使用临界区对象调用enter()自我加锁后,忘记或不便于调用exit()来解锁)。当然也可直接用临界区对象调用enter()和exit()来自我加/解锁,但需注意两点:已经加锁,则不能再次加锁。

另外,一个exit()只匹配一个enter()。拥有已锁定的临界区对象的线程,可以自由读写作用域内的数据而无需担心其他线程的竞争。读写完毕,退出作用域时,临界区对象解锁,供其他线程锁定和读写该作用域内的数据。JUCE类库利用此技术完成多线程环境下的代码并发执行并解决竞争和死锁的经典问题。

由上可知,要实现共享数据和资源的互斥与保护,有三个关键点。一是CriticalSection临界区(互斥)对象,二是指明作用域(一对花括号之间的范围),三是使用作用域锁对象(自动对临界区对象加锁和解锁)。锁对象由几个内置的类型重定义来实现,通常情况下,无需深究类型重定义的原型及其内部细节,直接使用即可。实际编程中,使用最多的是ScopedLock作用域锁。

仅需两步即可实现线程同步时的数据保护:
 类中声明CriticalSection临界区对象,必须是栈对象。即:在作用域之外需提前定义临界区对象。
 类的功能性函数中指明作用域。在作用域的开始处创建一个ScopedLock锁对象,其构造参数为临界区对象。注意:锁对象也必须是栈对象,并且通常声明为const常对象。

完成上述两个步骤后,系统将确保同一时间只能有一个线程可访问作用域内的数据。并且当代码执行到作用域之外时,CriticalSection对象自动解锁,以方便其它线程锁定并读写作用域内的数据。示例:

(网站发表,具体示例代码略)。

关于锁:

JUCE类库中预定义了4个锁对象:一是最常用的ScopedLock作用域锁定,二是ScopedUnlock取消作用域锁定,三是ScopedTryLock作用域内尝试锁定,四是读写Component对象的主线程UI锁MessageManagerLock(参见3.2.2 Thread小节的代码示例和脚注)。

使用作用域锁来锁定CriticalSection对象,可无需直接用CriticalSection对象调用其enter()和exit()等成员函数,比较安全可靠。