POSIX线程编程
一个程序中可以有多个代码指向序列,这每个序列就是一个线程(Thread),线程是进程内部的一个控制序列。一个进程至少具有一个执行线程。
Linux中通过fork创建的新进程,与Phtead API创建的线程是具有很大不同的:
- fork出的进程,拥有自己的变量、PID,其时间调度是独立的,其执行几乎完全独立于父进程
- 进程中创建的新线程,具有自己的栈(因而具有独立的局部变量),但是它与创建者共享全局变量、文件描述符、信号处理函数、当前目录状态
尽管线程已经出现很长时间,但是在IEEE POSIX委员会发布相关标准前,并没有在UNIX系统中得到广泛的支持。POSIX 1003.1c规范改变了这一状况,线程的实现被标准化,绝大部分的Linux发行版都支持它。
Linux在1996年开始支持线程,当时的函数库称为LinuxThread,该线程实现和POSIX标准存在细微的差别,特别是信号处理的相关部分。许多项目致力于改善Linux对线程的支持,以清除与POSIX标准的差异、增加性能,其中大部分工作集中在如何将用户级线程映射到内核级线程。这些项目中,以下一代POSIX线程(NGPT)和本地POSIX线程库(NPTL)为代表,而NPTL成为了Linux线程库的新标准。
尽管相比起某些其它OS,Linux创建进程的效率很高,但是创建新线程比创建新进程的代价要小得多。
线程具有以下优势场景:
- 有时,一个程序需要“同时”做几件事情。例如:编辑文档的时候同时进行单词个数的统计;数据库软件需要同时服务多个连接,这些连接可以对应不同的线程,线程之间需要紧密协作才能完成加锁、数据一致性要求,这些需求通过线程很容易实现
- 混杂着输入、计算、输出的应用程序。分离为多个线程来处理,可以有效改善程序的性能。需要服务多个客户端的网络服务器也是天生适合多线程的例子
- 一般而言,线程之间的切换需要操作系统做的工作比进程切换少得多。多个线程对资源的需求要远远小于多个进程
线程的主要缺点是:
- 多线程程序需要非常仔细的设计。在多线程程序中,因为时序上的细微差别、无意间的变量共享引发错误的可能性很大
- 多线程程序的调试比单线程困难的多,因为线程之间的交互非常难于控制
- 对于计算密集型程序,将计算拆分到多个线程运行,对于单处理器来说没有价值
Linux下线程具有一套完整的库函数,这些函数大部分以pthread_开头,声明在头文件: pthread.h 中,链接时需要指定-lpthread。
在最初设计UNIX/POSIX例程时,人们往往假设每个进程只有一个执行线程,典型的例子就是errno全局变量。在多线程环境下,需要“可重入例程”:即多次调用仍然能正常工作,这些调用可能来自不同的线程,也可能是某种形式的递归调用。
编写多线程程序时,通常需要在任何include语句之前定义宏 _REENTRANT ,以启用“可重入”功能,它将做以下三件事情:
- 对部分函数进行重新定义,定义为其可重入版本,这些函数的名字一般不会发生改变,只是在原函数的名字后面加上 _r
- stdio.h中原来以宏实现的一些函数,变成可重入函数
- errno.h中定义的变量errno变成一个函数调用,能够以多线程的形式来获取真正的errno值
下面是基本的线程API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <pthread.h> /** * 创建一个新的线程 * @param thread 指向pthread_t的指针,线程被创建时,指针指向的变量被写入一个标识符,该标识符用来引用新的线程 * @param attr 设置线程的属性,不需要时设置为NULL * @param start_routine 线程启动时执行的函数 * @param arg 线程启动时指向的函数的参数 * * @return 成功时返回0,错误时返回错误代码,pthread_函数的大部分都没有遵循UNIX函数惯例——失败时返回-1 */ int pthread_create( pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)( void * ), void *arg ); /** * 终止一个线程的执行(谁调用,谁被终止),类似于exit()终止一个进程的执行 * @param retval 存放返回值的指针,注意不能使用局部变量,因为线程退出后已经销毁 */ void pthread_exit( void *retval ); /** * 类似于进程中用来收集子进程信息的wait函数 * @param th 将要等待的线程 * @param thread_return 指向一个指针,该指针指向线程的返回值 */ int pthread_join( pthread_t th, void **thread_return ); /** * 可以让一个线程要求另外一个线程终止,就好像进程之间发送信号一样。 * @param thread 需要终止的线程的标识符 */ int pthread_cancel( pthread_t thread ); /** * 可以设置自己的取消状态 * @param state PTHREAD_CANCEL_ENABLE 允许取消请求(默认);PTHREAD_CANCEL_DISABLE 忽视取消请求 */ int pthread_setcancelstate( int state, int *oldstate ); /** * 可以设置自己的取消方式 * @param type * PTHREAD_CANCEL_ASYNCHRONOUS 接收到请求后立即采取行动; * PTHREAD_CANCEL_DEFERRED 延迟取消行为(默认),直到线程执行了以下函数之一: * pthread_join, pthread_cond_wait,pthread_cond_timedwait, pthread_testcancel, sem_wait, sigwait */ int pthread_setcanceltype( int type, int *oldtype ); |
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> void *thread_function( void *arg ); //一个共享变量 char message[] = "Hello"; int main() { setbuf( stdout, _IOFBF ); int res; pthread_t a_thread; void *thread_result; //存放线程返回值的指针 //创建一个线程 res = pthread_create( &a_thread, NULL, thread_function, ( void * ) message ); if ( res != 0 ) { perror( "Thread creation failed" ); exit( EXIT_FAILURE ); } printf( "Waiting for thread to finish...\n" ); //等待线程运行结束,将返回值指针的地址传递给函数,线程指向完毕后,会填写此地址 res = pthread_join( a_thread, &thread_result ); if ( res != 0 ) { perror( "Thread join failed" ); exit( EXIT_FAILURE ); } //打印线程的返回值 printf( "Thread joined, it returned message: %s\n", ( char * ) thread_result ); //打印共享变量 printf( "Message changed: %s\n", message ); exit( EXIT_SUCCESS ); } //线程入口函数定义 void *thread_function( void *arg ) { printf( "Thread is running. Argument: %s\n", ( char * ) arg ); sleep( 3 ); //修改共享变量 strcpy( message, "Bye" ); //线程退出,返回一段文本 pthread_exit( "Thread exit" ); } |
在创建线程的时候,允许指定一个属性参数,下面的API用于控制属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <pthread.h> /** * 初始化一个线程属性 */ int pthread_attr_init( pthread_attr_t *attr ); /** * 销毁一个线程属性,一旦被销毁,除非再次初始化,否则不能继续使用 */ int pthread_attr_destroy( pthread_attr_t * ); //线程属性控制函数 //detachstate:允许无需对线程进行rejoin //detachstate可以为:PTHREAD_CREATE_JOINABLE(默认)、PTHREAD_CREATE_DETACHED,后者不允许针对其pthread_join调用 int pthread_attr_setdetachstate( pthread_attr_t *attr, int detachstate ); int pthread_attr_getdetachstate( const pthread_attr_t *attr, int *detachstate ); //schedpolicy:控制线程如何被调度。可选值:SCHED_OTHER(默认)、SCHED_RP、SCHED_FIFO //后两个值要求超级用户权限,分别使用循环调度、先进先出调度策略 int pthread_attr_setschedpolicy( pthread_attr_t *attr, int policy ); int pthread_attr_getschedpolicy( const pthread_attr_t *attr, int *policy ); //scheparam:与schedpolicy配合使用,可以控制SCHED_OTHER的行为 int pthread_attr_setschedparam( pthread_attr_t *attr, const struct sched_param *param ); int pthread_attr_getschedparam( const pthread_attr_t *attr, struct sched_param *param ); //inheritsched:是否从创建者继承调度属性。可选值:PTHREAD_EXPLICIT_SCHED、PTHREAD_INHERIT_SCHED //分别表示需要明确设定,以及从创建者继承 int pthread_attr_setinheritsched( pthread_attr_t *attr, int inherit ); int pthread_attr_getinheritsched( const pthread_attr_t *attr, int *inherit ); //scope:控制线程调度的计算方式,Linux目前仅仅支持PTHREAD_SCOPE_SYSTEM int pthread_attr_setscope( pthread_attr_t *attr, int scope ); int pthread_attr_getscope( const pthread_attr_t *attr, int *scope ); //stacksize:控制线程栈大小,单位字节 int pthread_attr_setstacksize( pthread_attr_t *attr, int scope ); int pthread_attr_getstacksize( const pthread_attr_t *attr, int *scope ); |
下面是一个控制线程优先级的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <sys/sched.h> int main( int argc, char **argv ) { pthread_attr_t thread_attr; int max_priority; int min_priority; struct sched_param scheduling_value; int res = pthread_attr_setschedpolicy( &thread_attr, SCHED_OTHER ); if ( res != 0 ) { perror( "Setting scheduling policy failed" ); exit( EXIT_FAILURE ); } max_priority = sched_get_priority_max( SCHED_OTHER ); min_priority = sched_get_priority_min( SCHED_OTHER ); scheduling_value.sched_priority = min_priority; //设置为最低优先级 res = pthread_attr_setschedparam( &thread_attr, &scheduling_value ); if ( res != 0 ) { perror( "Setting scheduling priority failed" ); exit( EXIT_FAILURE ); } } |
Linux包含一组控制线程执行和代码临界区域访问的方法。
信号量这个概念最初由Dijkstra提出,他是一种特殊的变量,可以被增加或减少,即使在多线程环境下,对其进行增加、减少等关键操作也能保证原子性。这意味着多个线程对信号量的操作将被顺序执行,而对于普通变量,来自程序中多个线程的冲突操作造成的结果是不能确定的。
信号量用于控制对一组相同对象的访问。Linux下有两组函数可以用于信号量:
- 一组取自POSIX的扩展,用于线程
- 一组被称为System V信号量,用于进程的同步
Linux可用的信号量包括两种:二进制信号量、计数信号量,前者只有0/1两个值,可以用来保护代码同时只能被一个线程访问。线程中使用的基本信号量函数有以下四个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <semaphore.h> /** * 创建一个信号量 * @param sem 被初始化的信号量 * @param pshared 信号量的类型,如果为0表示是进程内部信号量,如果为1表示可以在多个进程之间共享 * @param value 信号量初始值 */ int sem_init( sem_t *sem, int pshared, unsigned int value ); /** * 以原子的方式将信号量的值加1 * 所谓原子操作是指:如果两个线程同时企图调用该函数,他们不会互相干扰,结果总是一致的加2 */ int sem_post( sem_t * sem ); /** * 以原子的方式将信号量的值减1,但它会等到直到信号量有个非零值时才会指向减法操作 * 如果对值为0的信号量调用该操作,调用者将一直等待直到信号量不为0为止 */ int sem_wait( sem_t * sem ); //尝试获取一个信号量,立即返回 int sem_trywait( sem_t *sem ); //等待一个信号量,在指定时间后超时退出 int sem_timedwait( sem_t *sem, const struct timespec *abstime ); /** * 在用完信号量后对其进行清理 */ int sem_destroy( sem_t * sem ); |
用于控制同一时刻只能有一个线程可以访问共享内存,互斥量允许锁住某个对象,直到操作完成后再释放它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <pthread.h> //这些函数成功返回0,失败返回错误代码,不设置errno /** * 初始化一个互斥量 * @param mutex 互斥量 * @param mutexattr 互斥量的属性,可以定制其行为 */ int pthread_mutex_init( pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr ); /** * 锁定一个互斥量,如果互斥量已经被加锁,该调用会一直阻塞直到其解锁 */ int pthread_mutex_lock( pthread_mutex_t *mutex ); /** * 解锁一个互斥量 */ int pthread_mutex_unlock( pthread_mutex_t *mutex ); /** * 销毁一个互斥量 */ int pthread_mutex_destroy( pthread_mutex_t *mutex ); |
Leave a Reply