Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。与vxworks上任务的概念类似,都是调度的最小单元,都有共享的堆、栈、代码区、全局变量等。
2. 创建线程
int pthread_create(pthread_t * thread,
pthread_attr_t * attr,
void * (*start_routine)(void *),
void * arg)
thread:返回创建的线程的ID
attr:线程属性,调度策略、优先级等都在这里设置,如果为NULL则表示用默认属性
start_routine:线程入口函数,可以返回一个void*类型的返回值,该返回值可由pthread_join()捕获
arg:传给start_routine的参数,可以为NULL
返回值:成功返回0
- 3. 设置线程属性
线程属性通过attr进行设置。
设置与查询attr结构的为pthread_attr_get***()与pthread_attr_set***()两个函数系列。
设置与查询线程参数的为pthread_get***()与pthread_set***()两个函数系列。也可以在创建时通过pthrea_create传入参数。
注:有些必须在线程创建时设置,如调度策略。
3.1. 调度策略
调度策略有三种:
SCHED_OTHER:非实时、正常
SCHED_RR:实时、轮询法
SCHED_FIFO:实时、先入先出,与vxworks的调度机制一致
例程:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);//sched_policy
3.2. 优先级
线程优先级支持两种设置方式,一种是创建时设置,另一种是创建后动态设置
例程:
pthread_attr_setschedparam(&attr, new_priority);
如果是静态设置,则之后会用该属性创建任务,如果是动态设置,则使用下列函数设置:
pthread_attr_setschedparam(&attr, &task->prv_priority);
pthread_attr_getschedparam(&attr, &schedparam);
schedparam.sched_priority = new_priority;
pthread_attr_setschedparam(&attr, &schedparam);
pthread_setschedparam(pthrid, sched_policy, &schedparam);
3.3. 脱离同步
Pthread_join()函数可以使主线程与子线程保持同步。如果设置了detachstate状态,则pthread_join()会失效,线程会自动释放所占用的资源。线程的缺省状态为PHREAD_CREATE_JOINABLE状态,线程运行起来后,一旦被设置为PTHREAD_CREATE_DETACH状态,则无法再恢复到joinable状态。
例程:
pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);、
3.4. 调度继承
线程A创建了线程B,则线程B的调度策略是与线程A的调度策略与线程B的继承策略有关的。如果线程B继承策略为PTHREAD_INHERIT_SCHED,则线程B的调度策略与线程A相同;如果线程B继承策略为PTHREAD_EXPLICIT_SCHE,则线程B的调度策略由attr决定。
pthread_attr_setdetachstate (&attr,PTHREAD_CREATE_DETACHED);
- 4. 线程取消
4.1. 线程取消定义
Pthread线程可以通过发送取消请求的方式终止一个线程的运行。
取消线程的操作主要应用于下列场景中:有一个线程在使用select监控网口,主控线程此时接到了用户的通知,要放弃监听,则主控线程会向监听线程发送取消请求。
Linux的pthread线程接收到取消请求时,并不会立刻终止线程,而是要等到取消点时才会结束任务。这样我们可以为取消点建立某些特殊的处理。Select是取消点,所以可以退出。
4.2. 取消点
Vxworks可以在任意位置杀死任务,这样做导致任务被杀死后的位置不可控,所以vxworks中不会很少使用taskDeleteHook。
Pthread规定了取消点的概念。不论线程何时收到取消请求,都只能在取消点上才能取消线程。这就保证了风险的可控。
Pthread标准指定了以下几个取消点:
- pthread_testcancel
- 所有调度点,如pthread_cond_wait、sigwait、select、sleep等
根据POSIX标准,read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段:
pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();
这段代码可以保证read附近有取消点,但是否有可能会卡在read中无法返回呢?
如果线程处于无限循环中,且循环体内没有执行至取消点的必然路径,则线程无法由外部其他线程的取消请求而终止。因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用。
4.3. 线程取消函数
int pthread_cancel(pthread_t thread)
发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。
int pthread_setcancelstate(int state, int *oldstate)
设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。
int pthread_setcanceltype(int type, int *oldtype)
设置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入原来的取消动作类型值。
void pthread_testcancel(void)
检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。
4.4. 例程
#include #include #include pthread_key_t key; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; unsigned long abc=0; void* Test03(void *p) { printf("Cancel point"); return NULL; } void* Test01(void* ptr) { pthread_cleanup_push(Test03, NULL); /* push */ while(1) { abc++; pthread_testcancel(); } pthread_cleanup_pop(0); /* pop */ return NULL; } void* Test02(void* ptr) { while(1) { sleep(2); printf("2222cond_wait:abc=0x%08x\n", abc); } return NULL; } int main(void) { int tid1, tid2; int ret; printf("Start:\n"); ret = pthread_create(&tid1, NULL, Test01, NULL); ret = pthread_create(&tid2, NULL, Test02, NULL); sleep(6); pthread_cancel(tid1); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; } |
结果:
Start:
2222cond_wait:abc=0x22c29a05
2222cond_wait:abc=0x47b49007
Cancel point2222cond_wait:abc=0x6c9de3ad
2222cond_wait:abc=0x6c9de3ad
2222cond_wait:abc=0x6c9de3ad
如果不加取消点pthread_testcancel(),线程1无法退出。
- 5. 线程终止方式
线程终止有两种情况(不考虑进程),一种是线程主体函数return时,线程会自动终止,此时的退出是可预知的;另一种是其它线程向通以进程下的线程发送取消请求,线程会根据情况判断是否终止,此时的终止是不可预知的。
Vxworks中的任务与此类似。任务的主体函数return时,任务会自动终止;其它任务调用taskDelete()可以杀死任意一个任务。TaskDelete()并不安全,因为任务可能被杀死在任意一个时刻。
5.1. 线程终止时的清理
不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑解决的问题。
最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果线程处于响应取消状态,且采用异步方式响应,或者在打开独占锁以前的运行路径上存在取消点,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。
在POSIX线程API中提供了一个pthread_cleanup_push()/pthread_cleanup_pop()函数对用于自动释放资源,相当于是增加了一个析构函数。从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作(包括调用pthread_exit()和取消点终止)都将执行pthread_cleanup_push()所指定的清理函数。
API定义如下:
void pthread_cleanup_push(void (*routine) (void *), void *arg) void pthread_cleanup_pop(int execute) |
pthread_cleanup_push()/pthread_cleanup_pop()
采用先入后出的栈结构管理,void routine(void *arg)函数在调用pthread_cleanup_push()时压入清理函数栈,多次对pthread_cleanup_push()的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。
execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。
这两个其实是宏,必须成对出现,否则会编译不过。
编译。在下面的例子里,当线程在"do some work"中终止时,将主动调用pthread_mutex_unlock(mut),以完成解锁动作。
pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut); pthread_mutex_lock(&mut); /* do some work */ pthread_mutex_unlock(&mut); pthread_cleanup_pop(0); |
必须要注意的是,如果线程处于PTHREAD_CANCEL_ASYNCHRONOUS状态,上述代码段就有可能出错,因为CANCEL事件有可能在pthread_cleanup_push()和pthread_mutex_lock()之间发生,或者在pthread_mutex_unlock()和pthread_cleanup_pop()之间发生,从而导致清理函数unlock一个并没有加锁的mutex变量,造成错误。因此,在使用清理函数的时候,都应该暂时设置成PTHREAD_CANCEL_DEFERRED模式。为此,pthread中还提供了一对不保证可移植的pthread_cleanup_push_defer_np()/pthread_cleanup_pop_defer_np()扩展函数,功能与以下代码段相当:
{ int oldtype; pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype); pthread_cleanup_push(routine, arg); ... pthread_cleanup_pop(execute); pthread_setcanceltype(oldtype, NULL); } |
5.2. 线程终止的同步及其返回值
一般情况下,进程中各个线程的运行都是相互独立的,线程的终止并不会通知,也不会影响其他线程,终止的线程所占用的资源也并不会随着线程的终止而得到释放。正如进程之间可以用wait()系统调用来同步终止并释放资源一样,线程之间也有类似机制,那就是pthread_join()函数。
void pthread_exit(void *retval) int pthread_join(pthread_t th, void **thread_return) int pthread_detach(pthread_t th) |
pthread_join()的调用者将挂起并等待th线程终止,retval是pthread_exit()调用者线程(线程ID为th)的返回值,如果thread_return不为NULL,则*thread_return=retval。需要注意的是一个线程仅允许唯一的一个线程使用pthread_join()等待它的终止,并且被等待的线程应该处于可join状态,即非DETACHED状态。
如果进程中的某个线程执行了pthread_detach(th),则th线程将处于DETACHED状态,这使得th线程在结束运行时自行释放所占用的内存资源,同时也无法由pthread_join()同步,pthread_detach()执行之后,对th请求pthread_join()将返回错误。
一个可join的线程所占用的内存仅当有线程对其执行了pthread_join()后才会释放,因此为了避免内存泄漏,所有线程的终止,要么已设为DETACHED,要么就需要使用pthread_join()来回收。
5.3. 关于ptread_exite()和return
理论上说,pthread_exit()和线程宿体函数退出的功能是相同的,函数结束时会在内部自动调用pthread_exit()来清理线程相关的资源。但实际上二者由于编译器的处理有很大的不同。
在进程主函数(main())中调用pthread_exit(),只会使主函数所在的线程(可以说是进程的主线程)退出;而如果是return,编译器将使其调用进程退出的代码(如_exit()),从而导致进程及其所有线程结束运行。
其次,在线程宿主函数中主动调用return,如果return语句包含在pthread_cleanup_push()/pthread_cleanup_pop()对中,则不会引起清理函数的执行,反而会导致segment fault。
5.4. 判断是否为同一个线程
int pthread_equal(pthread_t thread1, pthread_t thread2)
判断两个线程描述符是否指向同一线程。在LinuxThreads中,线程ID相同的线程必然是同一个线程,因此,这个函数的实现仅仅判断thread1和thread2是否相等。
5.5. 仅执行一次的操作
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void))
本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。这个类似与线程的构造函数。
#include #include pthread_once_t once=PTHREAD_ONCE_INIT; void once_run(void) { printf("once_run in thread %d\n",pthread_self()); } void * child1(void *arg) { int tid=pthread_self(); printf("thread %d enter\n",tid); pthread_once(&once,once_run); printf("thread %d returns\n",tid); } void * child2(void *arg) { int tid=pthread_self(); printf("thread %d enter\n",tid); pthread_once(&once,once_run); printf("thread %d returns\n",tid); } int main(void) { int tid1,tid2; printf("hello\n"); pthread_create(&tid1,NULL,child1,NULL); pthread_create(&tid2,NULL,child2,NULL); sleep(10); printf("main thread exit\n"); return 0; } |
once_run()函数仅执行一次,且究竟在哪个线程中执行是不定的,尽管pthread_once(&once,once_run)出现在两个线程中。
LinuxThreads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次,而once_control则表征是否执行过。如果once_control的初值不是PTHREAD_ONCE_INIT(LinuxThreads定义为0),pthread_once()的行为就会不正常。在LinuxThreads中,实际"一次性函数"的执行状态有三种:NEVER(0)、IN_PROGRESS(1)、DONE(2),如果once初值设为1,则由于所有pthread_once()都必须等待其中一个激发"已执行一次"信号,因此所有pthread_once()都会陷入永久的等待中;如果设为2,则表示该函数已执行过一次,从而所有pthread_once()都会立即返回0。
pthread_kill_other_threads_np()
void pthread_kill_other_threads_np(void)
这个函数是LinuxThreads针对本身无法实现的POSIX约定而做的扩展。POSIX要求当进程的某一个线程执行exec*系统调用在进程空间中加载另一个程序时,当前进程的所有线程都应终止。由于LinuxThreads的局限性,该机制无法在exec中实现,因此要求线程执行exec前手工终止其他所有线程。pthread_kill_other_threads_np()的作用就是这个。
需要注意的是,pthread_kill_other_threads_np()并没有通过pthread_cancel()来终止线程,而是直接向管理线程发"进程退出"信号,使所有其他线程都结束运行,而不经过Cancel动作,当然也不会执行退出回调函数。尽管LinuxThreads的实验结果与文档说明相同,但代码实现中却是用的__pthread_sig_cancel信号来kill线程,应该效果与执行pthread_cancel()是一样的,其中原因目前还不清楚。
- 6. TSD
6.1. TSD概念
在单线程程序中,我们经常要用到"全局变量"以实现多个函数间共享数据。在多线程环境下,由于数据空间是共享的,因此全局变量也为所有线程所共有。但有时应用程序设计中有必要提供线程私有的全局变量,仅在某个线程中有效,但却可以跨多个函数访问,比如程序可能需要每个线程维护一个链表,而使用相同的函数操作,最简单的办法就是使用同名而不同变量地址的线程相关数据结构。这样的数据结构可以由Posix线程库维护,称为线程私有数据(Thread-specific Data,或TSD)。
6.2. 创建与注销
Posix定义了两个API分别用来创建和注销TSD:
int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *)) |
该函数从TSD池中分配一项,将其值赋给key供以后访问使用。如果destr_function不为空,在线程退出(pthread_exit())时将以key所关联的数据为参数调用destr_function(),以释放分配的缓冲区。不论哪个线程调用pthread_key_create(),所创建的key都是所有线程可访问的,但各个线程可根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量。在LinuxThreads的实现中,TSD池用一个结构数组表示:
static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] = { { 0, NULL } }; |
创建一个TSD就相当于将结构数组中的某一项设置为"in_use",并将其索引返回给*key,然后设置destructor函数为destr_function。注销一个TSD采用如下API:
int pthread_key_delete(pthread_key_t key) |
这个函数并不检查当前是否有线程正使用该TSD,也不会调用清理函数(destr_function),而只是将TSD释放以供下一次调用pthread_key_create()使用。在LinuxThreads中,它还会将与之相关的线程数据项设为NULL(见"访问")。 6.3. 访问TSD的读写都通过专门的Posix Thread函数进行,其API定义如下:
int pthread_setspecific(pthread_key_t key, const void *pointer)void * pthread_getspecific(pthread_key_t key) |
写入(pthread_setspecific())时,将pointer的值(不是所指的内容)与key相关联,而相应的读出函数则将与key相关联的数据读出来。数据类型都设为void *,因此可以指向任何类型的数据。在LinuxThreads中,使用了一个位于线程描述结构(_pthread_descr_struct)中的二维void *指针数组来存放与key关联的数据,数组大小由以下几个宏来说明:
#define PTHREAD_KEY_2NDLEVEL_SIZE 32#define PTHREAD_KEY_1STLEVEL_SIZE \((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE - 1)/ PTHREAD_KEY_2NDLEVEL_SIZE) 其中在/usr/include/bits/local_lim.h中定义了PTHREAD_KEYS_MAX为1024, 因此一维数组大小为32。而具体存放的位置由key值经过以下计算得到:idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZEidx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE |
也就是说,数据存放与一个32×32的稀疏矩阵中。同样,访问的时候也由key值经过类似计算得到数据所在位置索引,再取出其中内容返回。
6.4. 使用范例
/* fireaxe的例程 */ #include #include pthread_key_t key; void echomsg(int t) { printf("destructor excuted in thread %d,param=%d\n",pthread_self(),t); } void * child1(void *arg) { int tid=pthread_self(); printf("thread %d enter\n",tid); pthread_setspecific(key,(void *)tid); sleep(2); printf("thread %d returns %d\n",tid,pthread_getspecific(key)); sleep(5); } void * child2(void *arg) { int tid=pthread_self(); printf("thread %d enter\n",tid); pthread_setspecific(key,(void *)tid); sleep(1); printf("thread %d returns %d\n",tid,pthread_getspecific(key)); sleep(5); } int main(void) { int tid1,tid2; printf("hello\n"); pthread_key_create(&key,echomsg); pthread_create(&tid1,NULL,child1,NULL); pthread_create(&tid2,NULL,child2,NULL); sleep(10); pthread_key_delete(key); printf("main thread exit\n"); return 0; } |
给例程创建两个线程分别设置同一个线程私有数据为自己的线程ID,为了检验其私有性,程序错开了两个线程私有数据的写入和读出的时间,从程序运行结果可以看出,两个线程对TSD的修改互不干扰。同时,当线程退出时,清理函数会自动执行,参数为tid。
- 7. 线程同步
7.1. 互斥锁(互斥信号量)
Pthread的互斥锁与vxworks的互斥信号量类似,都是用于互斥保护。
需要注意的是,linux有取消点的概念,即任务在运行时可以被取消。如果使用了互斥锁,可能会造成为解锁就被取消。为了解决这一问题,linux提供了可以在取消上加回调函数的功能:
pthread_cleanup_push()
pthread_cleanup_pop()
两个必须成对出现如果线程运行到两个函数之间被取消时,push注册的函数会被调用。
例程:
如果没有push与pop两行,则tid1被杀死后会导致tid2无法获取到互斥锁。
/* fireaxe的例程 */ #include #include #include pthread_key_t key; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void* Test01(void* ptr) { pthread_cleanup_push(pthread_mutex_unlock, &mutex); /* push */ while(1) { sleep(3); pthread_mutex_lock(&mutex); printf("1111mutex_lock\n"); pthread_cond_wait(&cond, &mutex); printf("1111cond_wait\n"); pthread_mutex_unlock(&mutex); } pthread_cleanup_pop(0); /* pop */ return NULL; } void* Test02(void* ptr) { while(1) { sleep(2); pthread_mutex_lock(&mutex); printf("2222mutex_lock\n"); pthread_cond_wait(&cond, &mutex); printf("2222cond_wait\n"); pthread_mutex_unlock(&mutex); sleep(2); } return NULL; } int main(void) { int tid1, tid2; int ret; pthread_mutex_init(&mutex, NULL); pthread_mutex_init(&mutex, NULL); printf("Start:\n"); ret = pthread_create(&tid1, NULL, Test01, NULL); ret = pthread_create(&tid2, NULL, Test02, NULL); printf("ret = 0x%x\n", ret); sleep(4); pthread_cancel(tid1); do { sleep(4); pthread_cond_signal(&cond); } while(1); printf("end.\n"); return 0; } |
7.2. 条件变量(同步信号量)
Pthread的条件变量与vxworks中同步信号量类似,都是用于任务同步。区别是条件变量的操作不是原子操作,需要借助互斥锁保证其原子性。7.3. 信号灯(计数信号量)
Pthread的信号灯与vxworks中计数信号量信号量类似,都是用于表示资源是否可用。
- 8. 参考文档