【多线程程序设计中的常见问题及解决途径】posix多线程程序设计

  摘要:基于线程的多任务,是一种充分、合理利用计算机资源,提高工作效率的重要手段。多线程编程技术避免了某项任务长期占用CPU时间,既提高了程序的性能,又增强了程序的功能,实现了一些传统语言难以实现的功能。多线程的编程技术越来越广泛应用。本文总结了多线程程序设计中一些常见问题以及它们的特征和解决途径。
  关键词:多线程;应用程序;数据竞争;存储
  中图分类号:TP311 文献标识码:A 文章编号:1007-9599 (2012) 16-0000-02
  多核处理器的出现和普及为提高计算机的性能提供了一个新的计算平台,在多核体系结构上获得性能的提升,需要应用多线程技术,以充分利用单个芯片上的多个计算核心,提高程序整体的计算吞吐量。基于线程的多任务,即同时执行多个线程,是一种充分、合理利用计算机资源,提高工作效率的重要手段。
  然而,在实际的多线程程序设计上,却经常出现各种问题。因为很多问题的出现最终都源于采用了错误的任务分解方式,所以在设计的后期很难进行修正,本文总结了多线程程序设计中一些常见问题以及它们的特征和解决途径,便于程序员开发。
  1.线程的基本概念
  线程是程序中的一个实体,是被系统调度和分配的基本单元。线程只占用很少的系统资源,但可与同属一个进程的其他线程共享所属进程的全部资源,同一个进程中的多个线程之间可以并发执行,从而更好地改善了系统资源的利用。
  2.常见问题及解决途径
  2.1 线程过多
  多线程是为不同的程序比较合理地安排运行时间,更加充分的利用系统资源。但是并不是线程越多就越好,这当中存在着一个线程数和程序性能的平衡,过多的线程可能会严重影响程序的性能。这种影响主要有以下两个方面:首先,将给定的工作量划分给过多的线程会造成每个线程的工作量过少,因此可能导致线程启动和终止时的开销比程序实际工作的开销还要多;其次,过多并发线程的存在将导致共享有限硬件资源的开销增大。
  最好的解决方法是依据实际情况,使用尽可能少的线程,这样可以最大限度地减少操作系统资源的使用,并可提高性能。具体操作方法是使用线程池。每个传入的请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理。一旦池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用,这种重用使应用程序可以避免为每个线程创建新进程的开销。
  2.2 数据竞争和死锁
  通常有几种情况会引发数据竞争:一种情况是:对共享数据的非同步访问。例如一个线程可能在其时间片执行全部代码,也有可能只执行其中一部分代码,因此程序结果将以不确定的方式依赖两个或多个线程的相对时间特性。除此之外,还有一类数据竞争,是在编程时构造出来的:一个数据整体由跨越几个单元的部分组成,在读取或者写入这个数据整体时要保证对这个整体进行原子的读和写,否则,读取或者写入就是已被混淆的数据。另外,不仅对共享的非同步访问会引起数据竞争,即使是同步访问,如果同步的层次比较低,也可能出现数据竞争。
  解决数据竞争的有很多同步策略,其中最简单的就是“锁”。具体方法是,每一个要访问共享数据片的任务在访问数据前必须申请得到锁,然后执行计算,最后要释放锁,以便其他任务可以对这些数据执行别的操作。
  在使用锁时要避免的问题是死锁,它往往造成组装失败。产生死锁的原因是因为锁不具有组合性。程序中,不能保证由两部分以锁为基础、能正确运行的代码合并得到的程序能够正确运行。而现代软件开发的基础正是将小程序库合并为更大程序的组装能力,因此,要做到考察组件的实现能力,才能再此基础上组装大的软件。
  要避免死锁,最好的方法是复制原本需要互斥的访问资源,这样每一个线程都拥有一个资源的私有副本。每个线程访问自己副本,从而可以避免使用锁。如果无法被复制,那么可以按照一定上网顺序获取资源(锁),保存一致的锁获取顺序可避免死锁的出现。如果没有明显的锁获取顺序,那么还有一种解决方法,就是按照地址对锁进行排序。这种方法要求线程指导所有要访问的地址,然后再访问这些锁。
  2.3 竞争激烈的锁
  即使使用锁来避免数据竞争,如果各个线程对锁的竞争很激烈,那么也会引发性能问题。如果线程试图获取锁的速度比线程执行对应临界段的速度快,那么这些线程也会出现“护航”的情形,等待获取锁,从而使程序性能下降。而对于公平竞争的锁机制来说,护航的情形更加严重,因为如果一个线程进入睡眠,那么其后的所有线程都必须等待它再次被唤醒。
  可以通过资源复制的方式来避免使用锁,这一方法在前面已经讨论过。当某个资源无法被复制,解决的方式是将该资源分割成若干个部分,然后用彼此独立的锁分别保护分割以后的部分。对于多个线程同时向一个散列表中插入数据的情形,为了避免数据竞争,每个线程在访问散列表的时候都需要对散列表加锁,具体操作是通过散列函数,将关键字映射到一个子表,并且是每一个子表对应一个独立的锁。对于给定的关键字,线程可以使用散列函数将关键字映射到一个子表,然后线程取得这个子表的锁,再对这个子表进行操作。只要有足够数量的子表和足够好的散列函数,线程之间就不会出现竞争同一个子表的锁的情况。
  2.4 非阻塞算法
  要解决锁带来的问题,一种方法是不使用锁,消除竞争,这种思路设计的算法称为非阻塞算法。非阻塞算法的本质特征就是停止一个线程的执行不会阻碍系统中其他执行实体的运行。
  非阻塞算法被广泛用于操作系统级别,进行诸如线程和线程调度等任务。虽然他们的实现比较复杂,但对于基于锁定的备选算法,他们有许多优点:可以避免优先级倒置和死锁等危险,竞争比较简单,协调发生在更细的粒度级别,允许更高程度的并行机制等。
  非阻塞算法是当前的一个研究热点,其显著的优势在于避免了锁带来的问题,其主要缺点在于,与等价的锁比较而言,非阻塞算法要复杂得多,而且要证明算法的正确性也极为困难。使用非阻塞算法应该注意以下两点:   (1)直接使用原子增、原子减一般来说是安全的。
  (2)对链状数据结构构造非阻塞算法应当使用文献中公认正确地算法,并且要理解存储空间回收问题。
  2.5 面向高性能的数据组织
  如果考虑按照程序的存储单元按照访问类型进行局部性分割,将能提高程序性能,以下是四种局部性定义:
  (1)线程私有:这种存储单元被某一个线程单独占有,其他线程都无法访问。只要cache能够容下这部分存储单元,硬件线程会将其保留在cache中,这样对线程私有存储数据的访问就非常快,且不会消耗总线带宽。
  (2)线程共享只读:这种存储单元被多个线程共享,但是这些线程并不对存储单元进行些操作。例如一个查找表,因为只会对这个表进行读操作,而不会进行写操作,所以硬件线程将其保存在自己的cache 中。
  (3)互斥访问:这种存储单元可以被读取和写入,受到一个锁的保护。当一个线程获得锁,这些存储单元将被载入到cache中。当这个线程释放了锁,这些存储单元将会被写回存储器,或者写到下一个获取锁的硬件线程的cache 中。
  (4)非规访问(wild west):这种存储单元可以有同步的线程进行读写,根据锁实现机制不同,这些存储单元可能会自带锁对象,因为就其本质来说,非同步线程访问锁的同时就实现了同步。非规访问是否包含锁对象取决于锁对象包含实质内容,还是包含指向实际锁的指针。
  在以上四种存储单元中,较好的两类是线程私有的存储单元和线程共享只读的存储单元,因为他们对总线带宽占用很少,并且不需要同步。
  3 结束语
  多核处理器的出现和普及为提高计算机的性能提供了一个新的计算平台,在多核平台上可以更加方便容易的实现个人低成本并行计算。因此研究基于多核架构的多线程编程技术显得极为重要,有助于转变编程思维和程序设计方法,适应多核时代下得多线程并行编程趋势。该方法的研究与讨论还将促进高校计算机教学的改革和课程体系建设以及先进课程体系建设,让高校计算机教学跟随多核的计算机发展趋势,培养紧密跟随计算机发展趋势的计算机人才。
  参考文献:
  [1]阿克特.多核程序设计技术-通过软件多线程程序提升技术[M].北京:电子工业出版社,2007.
  [2]马安腾 visual c++程序设计导学[M].清华大学出版社,2002.
  [3]张俊,刘松鹤.多核多处理器低功耗设计技术研究[J].计算机科学 2007
  [4]Leopold Lee . J2ME手机编程基础[M].清华大学出版社,2003.

推荐访问:常见问题 程序设计 多线程 途径