每个线程是CPU使用的一个基本单元;包括线程ID、PC、寄存器组和堆栈。线程与同一进程的其他线程共享代码段、数据段和其他操作系统资源,如打开文件和信号。
创建多线程的动机总体上来说就是为了提高速度。以Web服务器为例,当有多个客户机向同一个Web服务器发起请求。在线程流行之前的做法是,服务器(单线程进程)收到请求时会创建另一个进程处理请求,但进程创建很耗时间和资源,如果Web服务器进程时多线程的,那么就可以创建一个线程处理请求。创建线程的代价比创建进程小得多。
多线程编程的四大类优点:
并行性和并发性的区别。
Amdahl定律。
一般而言,多核系统编程有五个方面的挑战:
通常有两种类型的并行:数据并行(data parallelism)注重将数据分布于多个计算核上,在各个核执行相同的操作。任务并行(task parallelism)设计将任务(线程)分配到多个计算核。实践中很少严格遵循数据或任务并行,大多数情况下,应用程序混合使用这两个策略。
有两种不同的方法来提供线程支持:用户层的用户线程(user thread)或内核层的内核线程(kernel thread)。用户线程位于内核之上,它的管理无需内核支持;而内核线程由操作系统来支持和管理。用户线程核内核线程必然存在某种关系。下面是三种常用的建立这种关系的方法:多对一模型、一对一模型和多对多模型。
映射多个用户级线程到一个内核线程。线程管理由用户空间的线程库来完成。如果一个线程执行阻塞系统调用,那么整个进程将会阻塞。因为任意时间只有一个线程可以访问内核,所以多个线程不能并行运行在多处理核系统上。
映射每个用户线程到一个内核线程。此模型在一个线程执行阻塞系统调用时,能够允许另一个线程继续执行,比多对一模型并发性更好。但创建一个用户线程就要创建一个相应的内核线程,这个开销会影响程序的性能。所以这种模型的大多数实现限制了系统支持的线程数量。
多路复用多个用户级线程到同样数量或更少数量的内核线程。
多对一模型允许开发人员创建任意多数量的线程,但内核一次只能调度一个线程,没有增加并发性;一对一模型提供了更大的并发性,但是不应在应用程序内船舰太多的线程。多对多模型没有这两个缺点。多对多模型的一个变种,在原来的基础上,允许绑定某个用户线程到一个内核线程,这个变种,称为双层模型(tow-level model)。
线程库(thread library)为程序员提供创建和管理线程的API。实现线程库的主要方法有两种,一是在用户空间提供一个没有内核支持的库;二是实现由操作系统直接支持的内核级的一个库。二者的区别是线程库的代码和数据结构的位置前者位于用户空间,后者位于内核空间,调用库内的函数时,前者只是导致用户空间的一个本地函数调用,后者会导致对内核的系统调用。目前使用的三种主要线程库:POSIX Pthreads、Windows、Java。
接下来的部分将用上面的三种线程库做介绍,这里先说明一下多线程创建的两个常用策略:异步线程和同步线程。
Pthreads时POSIX标准定义的线程创建与同步的API。这是线程行为的规范(specification),而不是实现(implementation)
下面用代码段展示基本的Pthreads API:
#include //所有Pthreads程序都要包括头文件pthread.h
...
pthread_t tid; //the thread identifier
pthread_attr_t attr; //set of thread attributes
pthread_attr_init(&attr); //get the default attributes
pthread_create(&tid,&attr,runner,argv[]); //create the thread, runner is the function to run
pthread_join(tid,NULL); //wait for the thread to exit
...
略。
为了解决多线程编程的困难,更好的支持设计多线程程序,有一种方法时将多线程的创建和管理交给编译器和运行时库来完成,这种策略称为隐式线程(implicit threading)。
线程池的主要思想是:在进程开始时创建一定数量的线程,并加载到池中等待工作。当有需要时,会唤醒池内的线程(如果有可用线程),一旦线程完成了服务,它会返回池中等待工作。如果没有可以线程会一直等待直到有空线程为止。
线程池具有以下优点:
OpenMP为一组编译指令和API,用于编写C、C++、Fortran等语言的程序。OpenMP识别并行区域(parallel region),即可并行运行的代码块。应用程序开发人员在并行区域插入编译指令,这些指令指示OpenMP运行时库来并行执行这些区域。有兴趣的可以查阅资料深入了解。
GCD为C和C++增加了块(block)的扩展。每块是工作的一个独立单元,用花括号{}将代码括起来,然后前面加上字符,一个简单的例子如下:
^{printf("I am a block");}
通过将这些块放置在调度队列(dispatchqueue)上,GCD调度块以执行。当GCD从队列移除一块后,就将该块分配给线程池中的可用线程。GCD识别两种类型的调度队列:串行(serial)(串行队列上的块按先进先出的顺序删除,每个进程都有自己的串行队列)和并发(concurrent)(并发队列上的块可以按前面的方法先进先出的删除,也可同时删除多个块)。
对于多线程程序,fork()系统调用会根据系统和实际情况可以复制所有线程,也可以仅复制调用了fork()的线程。
exec()的工作方式与第三章所属方式通常相同。exec()参数指定的程序将会取代整个进程,包括所有线程。
UNIX信号(signal)用于通知进程某个特定事件已经发生。信号的接收可以是同步或异步的,取决于信号的来源和原因。无论如何都遵循相同的模式:
信号处理程序可以分为两种:
单线程程序信号总是传递给进程,但对于多线程程序,信号应该被传递到哪里呢?:
信号传递的方法取决于产生信号的类型。传递信号的标准UNIX函数为:
kill(pid_t pid, int signal)
此函数将一个特定信号(signal)传递到一个进程(pid)。
线程撤销(thread cancellation)是在线程完成之前终止线程。例,如果多个线程同时执行搜索任务,其中一个线程搜索到指定目标,则可以撤销其他线程。需要撤销的线程通常称为目标线程(target thread)。撤销有两种情况:、
同一进程的线程共享进程的数据。某些情况下,每个线程可能需要它自己的某些数据,这种数据称为线程本地储存(Thread-Local Storage,TLS)。
多线程编程需要考虑的最后一个问题设计内核与线程库间的通信,多对多和双层模型可能需要这种通信。这种协调允许动态调整内核线程数量,以便确保最优性能。许多系统实现多对多或双层模型是在用户和内核线程之间增加一个中间数据结构(轻量级进程(LightWeight Process,LWP)),以便应用程序调度并运行用户线程。
这里有关概念:调度器激活(scheduler activation)、回调(upcall)、回调处理程序(upcall handler)部分有兴趣自行查阅相关资料。
调度器激活工作如下:内核提供一组虚拟处理器(LWP,对用户级线程库LWP表现为虚拟处理器)给应用程序,应用程序可以调度用户线程到任意可以的LWP。当一个线程阻塞时,一个触发回调的事件会发生,内核会向应用程序发出一个回调,通知它有一个线程将会阻塞并标识出来。然后内核分配一个新的LWP给应用进程运行回调处理程序,它保存阻塞线程的状态,释放阻塞线程运行的LWP,接着调度一个适合在新的LWP上运行的线程。当阻塞线程等待的事件发生时,内核向线程库发送另一个回调,同样的,相应的回调处理程序会调度先前阻塞的线程。
略。
除了fork(),系统调用clone()也提供创建线程的功能。这部分细节不再赘述,有兴趣请自行查阅相关资料。