之前讨论的进程是具有单个控制线程的程序,但现代操作系统的设计都允许一个进程包含多个线程
每个线程是CPU使用的基本单元,包括线程ID,线程计数器,寄存器和堆栈,与同一进程的其他线程共享代码段数据段和操作系统资源,如果一个进程有多个线程,那么能同时执行多个任务。
多核系统的每个核都可以执行一个单独线程,
并发和并行:并行系统同时执行多个任务,并发系统支持多个任务,允许多个任务都可以取得进展,但是不一定同时执行。
数据并行注重将数据分布于多个计算核上,并在每个核上执行相同的操作;
任务并行将任务分配到不同的计算核,每个线程执行一个独特的操作,不同线程可以操作相同的数据或者不同的数据。
用户层的用户线程和内核层的内核线程。用户线程位于内核之上,无需内核支持,内核线程由操作系统直接支持管理。
将多个用户线程映射到一个内核线程,线程管理是由用户空间的线程库完成的,
将每个用户线程映射到内核线程,允许在一个线程执行阻塞调用时,另一个线程继续执行,且支持多个线程并行运行在多处理器系统上。唯一缺点是创建一个用户线程就要创建相应的内核线程,会影响程序的性能(linux模型)
该模型复用多个用户线程到同样数量或更少数量的内核线程。
线程库是提供给程序员的创建和管理线程的API,一种方法是在用户空间中提供一个没有内核支持的库,代码和数据结构是位于用户空间的;调用函数只是用户空间的本地调用,而非系统调用。
另一种方法是实现由操作系统支持的内核级的库,代码和数据位于内核空间
对于POSIX和Windows线程,全局声明的数据可由同一进程的所有线程共享。
pthread是POSIX定义的线程创建与同步API,是线程行为的规范而不是实现
#include
#include
int sum;
void *runner(void *param);
int main(int argc,char **argv)
{
pthread_t tid;
pthread_attr_t attr;
if(argc!=2)
{ fprintf(stderr,"useage:a.out \n" );
return -1;
}
if(atoi(argv[1]<0))
{
fprintf(stderr,"%d must be >=0\n",atoi(argv[1]));
return -1;
}
pthread_attr_init(&attr);
pthread_create(&tid,&attr,runner,argv[1]);
pthread_join(tid,NULL);
printf("sun = %dn",sum);
}
void *runner(void *param)
{
int i,upper = atoi(param);
sum=0;
for(int i=1;i<upper;i++)
sum+=i;
pthread_exit(0);
}
main()
开始,之后创建了runner()
线程,共享全局数据sumpthread_attr_t
表示线程的属性,包括堆栈大小,调度信息等,pthread_create()
创建单独线程pthread_join()
等待子线程完成,pthread_exit()
终止线程Summation()
以便在单独线程中执行,还需要传递给该函数一个void指针CreateThread()
传递给该函数安全信息,堆栈大小,调度标志,WaitForSingleObject()
阻塞,直到累加和线程退出WaitForMultipleObjects()
接受四个参数,等待多个线程完成:等待对象数量、对象数组指针、是否等待所有对象、超时时间java的线程是程序执行的基本模型,所有java程序至少包含一个控制线程,即使只有main()
的一个简单java程序也是在JVM中作为一个线程运行的
Thread
的新的类,并重载函数run()
另一种是定义一个实现接口Runnable()
的类.public interface Runnable
{
public abstract void run();
}
对于Windows和Pthread 线程间数据共享可简单声明为全局数,而java必须向相应线程传递共享对象引用来实现。
一种方法是将多线程的创建和管理交给编译器和RTE library来完成,这种策略成为隐式线程
因为创建线程需要开销,而且完成任务后线程会被丢弃,且如果并发请求过多,无限制地创建线程可能会耗尽系统资源,所以用线程池来管理线程
线程池的线程数量可以通过一些因素加以估算,也可以动态调整,负荷低时提供小规模的池,减少内存消耗
DWORD WINAPI PoolFunction(AVOID Param){}
PoolFunction()
的指针传递给线程池API中一个函数(e.g.)QueueUserWorkItem(LPTHREAD START ROUTINE Function, PVOID Param, ULONG Flags)
参数分别为作为单独线程运行的函数指针,传递给Functinon的参数,指示创建和管理线程的标志 。调用后,池中线程会运行PoolFunction()
OpenMP识别并行区域,通过编译指令来指示OpenMP来并行执行这些代码块
int main()
{
#pragma omp parallel
{
printf("I am a parallel region.");
}
return 0;
}
OpenMP遇到#pragma omp parallel
就会创建与系统处理核一样多的线程,并同时执行该区域。它还提供了一些其他指令比如循环的并行化 #pragma omp parallel for
还可指定哪些数据可以在线程间共享,哪些只能属于某个线程
大中央调度(GCD) 为C/C++增加了块的扩展,每个块是工作的一个独立单元^{ printf("I am a block") }
这些块放在调度队列上,GCD再调度这些块的执行。当GCD从队列中选择并移除一个块后,就将该块分配给线程池内的可用线程。
fork()
那么新进程复制所有线程或者只复制调用了fork()
的线程(依赖于系统实现)exec()
那么新执行的程序会替换整个进程。UNIX信号用来通知进程某个特定事件已经发生,信号的接收可以是同步的或者异步的。
同步信号如非法访问内存和除0,异步信号是由运行程序之外的事件发生而产生的信号。
kill(pid_t pid, int signal)
一般线程可以指定它接收什么信号,拒绝什么信号,因此一个异步信号只能传递给不拒绝的线程,通常传递给第一个不拒绝它的线程。POSIX 提供函数pthread_kill(pthread_t tid, int signal)
线程撤销是在线程完成之前终止线程。
pthread_cancel()
来撤销线程,默认的撤销类型为延迟撤销,只有线程到达撤销点
时才会发生撤销一般在撤销点会调用pthread_testcancel()
。这个函数会检查是否有撤销请求,如果有就调用清理程序 进而释放资源。许多系统在实现多对多或者双层模型时,在用户和内核线程之间增加一个中间数据结构:轻量级进程LWP
一个例子:
一个应用的线程要阻塞,即发生了一个触发回调的事件,内核会向应用发一个回调,再分配一个虚拟处理器给应用,应用在该虚拟处理器上运行回调处理程序,保存阻塞线程状态,释放阻塞线程的虚拟处理器,接着调度另一个适合在新分配的虚拟处理器上运行的线程。而阻塞线程的等待事件发生时,内核会向应用发出另一个回调,分配虚拟处理器,应用执行回调处理程序,并在该虚拟处理器上执行线程。
Linux不区分线程和进程,讨论控制流时一般用任务一词
clone()
来创建线程,但需要传递标志来确定父子任务的共享规则,如果共享内存空间,信号处理程序,文件系统等等,这种方式相当于线程创建,因为共享大部分的资源,如果不传递标志,则不共享,相当于进程创建。