首先,编写一个耗时的单线程程序:
#include<cstdio> #include<unistd.h> int main() { sleep(5); printf("program exited.\n"); }
编译并运行这段程序,该程序5秒后输出,sleep期间不再响应其它消息或执行其他操作。为了更好地处理这种耗时的操作,我们需要使用多线程编程。
先从书上抄些东西:
进程和线程都是操作系统的概念。进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。
线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main函数,将程序的启动点提供给操作系统。主执行线程终止了,进程也就随之终止。
每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。
多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。要说明的一点是,目前部分的计算机是单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。这一点在多线程编程时应该注意。
拥有下述特性的程序可以使用线程:
- 工作可以被多个任务同时执行,或者数据可以同时被多个任务操作。
- 阻塞与潜在的长时间I/O等待。
- 在某些地方使用很多CPU循环而其他地方没有。
- 对异步事件必须响应。
- 一些工作比其他的重要(优先级中断)。
多线程也可以用于串行程序,模拟并行执行。很好例子就是经典的web浏览器,运行在单CPU的电脑上,许多东西可以同时“显示”出来。
使用线程编程的几种常见模型:
- 管理者/工作者(Manager/worker):一个单线程,作为管理器将工作分配给其它线程(工作者),典型的,管理器处理所有输入和分配工作给其它任务。至少两种形式的manager/worker模型比较常用:静态worker池和动态worker池。
- 管道(Pipeline):任务可以被划分为一系列子操作,每一个被串行处理,且是被不同的线程并发处理。汽车装配线可以很好的描述这个模型。比如IDM等下载软件的文件分块同时下载。
- Peer:和manager/worker模型相似,但是主线程在创建了其它线程后,自己也参与工作。
接下来看看实现多线程编程的接口pthread。
线程管理
首先是创建和结束线程
函数:
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,(void*)(*start_rtn)(void*),void *arg); void pthread_exit(void* retval); int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr);
创建线程:
最初,main函数包含了一个缺省的线程。其它线程则需要程序员显式地创建。pthread_create创建一个新线程并使之运行起来。该函数可以在程序的任何地方调用。
pthread_create的参数与返回值:
第一个参数为指向线程标识符的指针。不能设置为NULL。
第二个参数用来设置线程属性。可设置NULL为缺省值。
第三个参数是线程运行函数的起始地址,即线程将会执行一次的C函数。
最后一个参数是传递给运行函数的参数。传递时必须转换成指向void的指针类型。没有参数传递时,可设置为NULL。
若成功则返回0,否则返回出错编号。
一个进程可以创建的线程最大数量取决于系统实现。
一旦创建,线程就称为peers,可以创建其它线程。线程之间没有指定的结构和依赖关系。
有这样一对问答:
Q:一个线程被创建后,怎么知道操作系统何时调度该线程使之运行?
A:除非使用了线程的调度机制,否则线程何时何地被执行取决于操作系统的实现。强壮的程序应该不依赖于线程执行的顺序。
也就是说,多线程程序的运行结果可能是不确定的,因为不知道系统会何时运行该线程。
线程属性:
线程具有属性,用pthread_attr_t表示,在对该结构进行处理之前必须进行初始化,在使用后需要对其去除初始化。我们用pthread_attr_init函数对其初始化,用pthread_attr_destroy对其去除初始化。还有其它的一些函数用于查询和设置线程属性结构的指定属性。
线程属性结构如下:
typedef struct { int etachstate; //线程的分离状态 int schedpolicy; //线程调度策略 structsched_param schedparam; //线程的调度参数 int inheritsched; //线程的继承性 int scope; //线程的作用域 size_t guardsize; //线程栈末尾的警戒缓冲区大小 int stackaddr_set; //线程的栈设置 void* stackaddr; //线程栈的位置 size_t stacksize; //线程栈的大小 }pthread_attr_t;
调用pthread_attr_init之后,pthread_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。
如果要去除对pthread_attr_t结构的初始化,可以调用pthread_attr_destroy函数。如果pthread_attr_init实现时为属性对象分配了动态内存空间,pthread_attr_destroy还会用无效的值初始化属性对象,因此如果经pthread_attr_destroy去除初始化之后的pthread_attr_t结构被pthread_create函数调用,将会导致其返回错误。
线程属性的其他特性和用法之后再讨论。
结束终止:
结束线程的方法有一下几种:
- 线程从主线程(main函数的初始线程)返回。
- 线程调用了pthread_exit函数。
- 其它线程使用 pthread_cancel函数结束线程。
- 调用exec或者exit函数,整个进程结束。
pthread_exit用于显式退出线程。典型地,pthread_exit()函数在线程完成工作时或不再需要时候被调用,退出线程。如果main函数在调用了pthread_exit()后将退出,尽管在main中已经没有可执行的代码了,进程和所有线程将保持存活状态,其他线程将会继续执行。否则,它们会随着main的结束而终止。对于正常退出,可以免于调用pthread_exit(),除非你想得到一个返回值。程序员可以可选择地指定终止状态,当任何线程连接(join)该线程时,该状态就返回给连接(join)该线程的线程。pthread_exit()函数并不会关闭文件,任何在线程中打开的文件将会一直处于打开状态,直到线程结束。
现在我们使用多线程来使我们的程序能在sleep期间执行其他操作。
#include<cstdio> #include<pthread.h> #include<unistd.h> void* printhello(void*) { for(int i=1;i<=4;i++) { sleep(1); printf("hello! %d sec has past\n",i); } } int main() { pthread_t tid; pthread_create(&tid,NULL,printhello,NULL); sleep(5); printf("program exited.\n"); }
该程序利用多线程,在main函数sleep的时候还能进行输出。
如果我们在main中使用pthread_exit()退出
#include<cstdio> #include<pthread.h> #include<unistd.h> void* printhello(void*) { for(int i=1;i<=4;i++) { sleep(1); printf("hello! %d sec has past\n",i); } } int main() { pthread_t tid; pthread_create(&tid,NULL,printhello,NULL); pthread_exit(NULL); //退出main的线程 printf("program exited.\n"); //这段代码将不被执行 }
会发现子线程仍然能继续执行,如果将
pthread_exit(NULL); //退出main的线程
改为
return 0;
程序将会直接终止,不输出。
向线程传递参数
pthread_create()函数允许程序员向线程的start_rtn函数传递一个参数。当多个参数需要被传递时,可以通过定义一个结构体包含所有要传的参数,然后用pthread_create()传递一个指向改结构体的指针,来打破传递参数的个数的限制。 所有参数都应该传引用传递并转化成(void*)。
要安全地向一个新创建的线程传递数据应确保所传递的数据是线程安全的(不能被其他线程修改)。
下面的代码片段演示了如何向一个线程传递一个简单的整数。
#include<cstdio> #include<pthread.h> #include<unistd.h> void* printid(void* id) { printf("my thread id is %d\n",*(int*)id); } int main() { pthread_t tid; pthread_create(&tid,NULL,printid,(void*)tid); pthread_exit(NULL);//不使用return是因为主函数退出太快以至于子线程没输出就被终止了 }
下面的代码片段演示了如何向一个线程传递结构体参数。
#include<cstdio> #include<pthread.h> #include<unistd.h> struct point { int x,y; }; void* print(void* p) { printf("I got a point (%d,%d).\n",((point*)p)->x,((point*)p)->y); } int main() { pthread_t tid[10]; point p[10]; for(int i=0;i<10;i++) { p[i].x=p[i].y=i; pthread_create(&tid[i],NULL,print,(void*)&p[i]); } pthread_exit(NULL); }
而下面的代码是错误的,循环会在线程访问传递的参数前改变传递给线程的地址,这将造成段错误(Segmentation fault)。
#include<cstdio> #include<pthread.h> #include<unistd.h> void* printid(void* i) { printf("i got a number %d\n",*(int*)i); } int main() { pthread_t tid[10]; for(int i=0;i<10;i++) { pthread_create(&tid[i],NULL,printid,(void*)i); } pthread_exit(NULL); }