使用线程化 API 提供的同步例程,而非手工编写同步例程

使用线程化 API 提供的同步例程,而非手工编写同步例程 (PDF 202KB)

摘要

应用编程人员有时候手工编写同步例程而非使用线程 API 提供的结构,以便减少同步开销,或提供不同于现有结构所提供的功能。 遗憾的是,使用手工编写的同步例程可能对性能、性能调谐或多线程应用的调试造成负面影响。

本文是“英特尔多线程应用开发指南”系列的一部分,该系列介绍了针对英特尔® 平台开发高效多线程应用的指导原则。

背景

通常,编程人员喜欢手工编写同步例程,以避免有时候由线程 API 提供的同步例程产生的相关开销。 编程人员自己编写同步例程的另外一个原因是,线程 API 提供的功能与实际需求不能完全匹配。 遗憾的是,与使用线程 API 例程相比,手工编写同步例程存在严重的缺点。

其中一点是不能确保针对不同的硬件架构与操作系统提供出色的性能。 下面以 C 语言手工编写的自旋锁为例,来帮助说明这些问题:

 

#include 


	void acquire_lock (int *lock)

	{

	while (_InterlockedCompareExchange (lock, TRUE, FALSE) == TRUE);

	}


	void release_lock (int *lock)

	{

	*lock = FALSE;

	}

	

 

编译器内部函数 _InterlockedCompareExchange 是一种互锁的内存操作,可确保在函数执行期间其它线程不能修改指定的内存位置。 该函数首先将第一个参数中的地址对应的内存内容与第三个参数中的值进行比较,如果匹配,则将第二个参数中的值存储到第一个参数中指定的内存地址。 在指定地址的内存内容中发现的初始值被内部函数返回。 在本例中,acquire_lock例程不停自旋,直到内存位置锁中的内容处于解锁状态(FALSE),此时(通过将锁的内容设置为TRUE)获得锁并例程返回。release_lock 例程将内存位置锁的内容重新设为 FALSE ,以便释放锁。

尽管乍一看该锁的实施似乎非常简单高效,但是它存在以下几个问题:

  • 如果许多线程在同一个内存位置自旋,在锁被释放时,该点就会出现过多的高速缓存无效和过多的内存流量,结果导致随着线程数量增加扩展能力变差。
  • 该代码使用的原子内存基元可能不适用于所有处理器架构,因而限制了可移植性。
  • 紧密的自旋循环可能导致某些处理器架构特性的性能变差,例如英特尔 ® 超线程技术。
  • while 循环对于操作系统来说好像是在执行有用的计算,但是它能对操作系统调度的公平性产生负面影响。

尽管有技术能够解决所有这些问题,但它们通常使代码变得极其复杂,以至于难以验证其正确性。 此外,很难做到代码调谐的同时保持可移植性。 这些问题最好留给线程 API 的作者,因为他们有更多的时间对同步结构进行验证和调谐,以实现出色的可移植性和可扩展性。

手工编写同步例程的另一个严重缺点是,它通常会降低编程工具在线程化环境中的准确性。 例如,英特尔 ® Parallel Studio 工具必须能够识别同步结构,以便提供有关线程化应用程序的性能(使用英特尔 ® Parallel Amplifier)和正确性(使用英特尔 ® Parallel Inspector)的精确信息。

线程工具在设计上通常会考虑发现和区别所支持线程API 提供的同步结构的功能。 如果没有使用标准的同步 API 来实现,这些工具将难以发现和理解同步,如上述示例所示。

有时候,编程人员以工具专用指令、编译指示或 API 调用的形式提供工具支持提示,以便发现和区别手工编写的同步例程。 尽管为特定工具所支持,但与使用线程 API 同步例程相比,这样的提示可能导致应用程序分析准确性降低。出现性能问题的原因可能难以检测,或者线程更正工具可能会报告严重的竞争状态或失去同步。

建议

如果可能,尽量避免使用手工编写的同步例程。 相反,使用您青睐的线程 API 提供的例程,例如面向英特尔 ® 线程构建块的queuing_mutexspin_mutexomp_set_lock/omp_unset_lock,或面向 OpenMP* 的critical/end critical 指令,或面向 Pthreads* 的 pthread_mutex_lock/pthread_mutex_unlock 。 学习线程 API 同步例程,以便找到一个适合您应用的例程。

如果线程 API 中没有能够提供所需功能的同步例程,可考虑针对程序使用对同步要求不高或要求其它同步的不同算法。 此外,专业的编程人员可以通过简单的 API 同步结构构建一个自定义同步结构,而非从零开始。 如果因为性能原因而必须使用手工编写的同步例程,可以考虑使用预处理指令,以便能够轻松使用与线程 API 功能相当的同步例程替换手工编写的同步例程。

使用指南

编程人员如果通过简单的 API同步结构创建自定义同步结构,应避免在共享位置使用自旋循环,从而避免性能不可扩展。 如果代码必须具备可移植性,还应避免使用原子内存基元。 线程性能和更正工具的准确性可能受到影响,因为这些工具可能无法推论出自定义同步结构的功能,即使构建该结构所使用的简单同步结构能够被正确识别。

更多资源

有关编译器优化的更完整信息,请参阅优化通知