在多线程应用中,程序员会使用互斥锁(mutex)来同步线程进入可访问共享资源的代码区域的行为。受这些锁保护的代码区域被称为关键代码段(Critical Section)。如果关键代码段中已存在一个线程,那么其他任何线程都不可进入该代码段。
线程应该尽量缩短在关键代码段花费的时间,进而减少其他线程在代码段外闲置等待获得锁的时间。但是又不能盲目地划分出很多的小代码段。
例1
Begin Thread Function ()
Initialize ()
BEGIN CRITICAL SECTION 1
UpdateSharedData1 ()
END CRITICAL SECTION 1
DoFunc1 ()
BEGIN CRITICAL SECTION 2
UpdateSharedData2 ()
END CRITICAL SECTION 2
DoFunc2 ()
End Thread Function ()
上例中,关键代码段被DoFunc1 ()函数分离开来。
如果线程在DoFunc1 ()中花费的时间很长,这样做是值得的。
但是如果线程在DoFunc1 ()中花费的时间很短,那更好的方案是将两个小关键代码段合并为一个关键代码段
例2
Begin Thread Function ()
Initialize ()
BEGIN CRITICAL SECTION 1
UpdateSharedData1 ()
DoFunc1 ()
UpdateSharedData2 ()
END CRITICAL SECTION 1
DoFunc2 ()
End Thread Function ()
例1中,关键代码段被DoFunc1 ()函数分离开来。UpdateSharedData1 ()和UpdateSharedData2 ()分别由两个锁来同步。例2中,将两个小关键代码段合并为一个大的关键代码段,其中包含了与同步无关的函数DoFunc1 ()。
那么哪种选择更好呢?
这要看情况决定。
如果线程在DoFunc1 ()中花费的时间很长,例1的性能更好。因为例2里,线程获得了锁,却在关键代码中浪费了大量的时间到与同步无关的函数上。
如果线程在DoFunc1 ()中花费的时间很短,例2的性能更好。例1为了在关键代码段中减少DoFunc1 ()的时间,承受了同步两个关键代码段的锁争用开销。如果DoFunc1 ()的时间少于1个锁争用开销,那么例1反而损失了性能。
如果线程在UpdateSharedData2 函数上会花费较长时间,例1的性能更好。因为这种情况下,线程无可避免地将在关键代码段中花费很长的时间。例2中,当线程进入UpdateSharedData2函数时,所有其他线程都堵塞着等待进入UpdateSharedData1函数。因此,我们可以采用例1 ,让其他线程先处理完UpdateSharedData1函数。
尽量把锁关联到特定共享数据。你不应该为共享数据的结构中的每个元素创建一个独立的锁,也不应该创建单个锁来保护到整个结构的访问。最佳的锁粒度应该在这两者之间,需要你把握。
针对上面的最后一个情况(如果线程在UpdateSharedData2 函数上会花费较长时间)
- 把UpdateSharedData2 函数访问的数据结构分为两部分,各使用一把互斥锁。然后把UpdateSharedData2 函数分解为两个函数。通过分离关键代码的方式来减少锁争用。
- 分析UpdateSharedData2 函数,如果UpdateSharedData2 函数并不需要对整个执行过程进行保护,那你可以考虑在函数中需要访问共享数据的点插入关键代码段,而不是封闭整个函数调用。
根据获取和释放锁的开销来调整关键代码段的大小。我们可做的事有:
- 整合小关键代码段,以分担锁定开销。
- 将锁争用现象严重的大型关键代码段划分为较小的关键代码段。
- 将锁关联至特定的共享数据,借以最大限度减少锁争用问题。 最佳解决方案可能处于为每个共享数据元素创建一个锁和为所有共享数据创建一个锁两种极端之间。
采用大关键代码段意味着算法本身的并发性非常低,或者线程间的数据划分并不理想。 对于前者,只能更改算法。 对于后者,可尝试为共享数据创建本地拷贝,支持线程异步访问。
如果我们考虑上下文切换的开销,那么我们应该决定使用互斥锁还是自旋锁。自旋锁是一个等待循环,会一直占用CPU,因此没有上下文切换。对于正等待进入小关键代码段的线程来说,使用自旋锁可能比互斥锁有更高的性能。但是,鉴于处于等待状态的线程在旋转等待循环中仍将占用 CPU 资源, 只有当线程在关键代码段中所花费的时间极短,不良影响低于环境切换时,才推荐使用旋转等待循环。
在支持英特尔® 超线程技术(英特尔® HT 技术)的处理器中,会在同一 CPU 核心上创建两路逻辑处理器。 旋转线程和正执行有用任务的线程务必会争夺逻辑处理器资源。与对称多处理器系统相比,旋转线程对采用英特尔超线程技术的系统中多线程应用性能的影响更大。 在这种情况下,应将自旋锁的自旋计数调低,或者不采用自旋锁。