介绍pthread线程库中的常用函数
文章目录
一、pthread_create
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void*(*start_routine)(void *), void *arg);
使用 pthread_create() 可以向当前进程中添加新的受控线程。
-
当 pthread_create() 成功时,所创建线程的 ID 被存储在由 tid 指向的位置中。
-
如果未指定属性对象,则该对象为 NULL,系统会创建具有以下属性的缺省线程:
进程范围
非分离
缺省栈和缺省栈大小
零优先级
-
start_routine 是新线程执行的函数。当 start_routine 返回时,该线程将退出,其退出状态设置为 start_routine 的返回值。
-
该函数只有一个万能指针参数arg,如果需要向 start_routine 函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。
-
返回值
0:一般均以0表示调用成功。
EAGAIN:超出了系统限制,如创建的线程太多。
EINVAL:attr 的值无效。
初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);
还可以用 pthread_attr_init() 创建属性对象,然后使用该属性对象来创建线程。
初始化函数的入参是一个指向线程属性结构的指针,结构中的元素分别对应着新线程的运行属性。属性对象主要包括是否绑定、是否分离、堆栈地址和大小、优先级等。默认属性为非绑定、非分离、默认1MB堆栈、与父进程有相同优先级。
pthread_attr_t tattr;
pthread_t tid;
extern void *start_routine(void *arg);
void *arg;
int ret;
/* initialized with default attributes */
ret = pthread_attr_init(&tattr);
/* default behavior specified*/
ret = pthread_create(&tid, &tattr, start_routine, arg);
一次性初始化
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
有时候我们需要对一些posix变量只进行一次初始化,如线程键。如果我们进行多次初始化程序就会出现错误。
在传统的顺序编程中,一次性初始化经常通过使用布尔变量来管理。控制变量被静态初始化为0,而任何依赖于初始化的代码都能测试该变量。如果变量值仍然为0,则它能实行初始化,然后将变量置为1。以后检查的代码将跳过初始化。
但是在多线程程序设计中,由于无法判定哪个线程先运行,从而无法知道把初始化代码放在哪个线程合适。如果多个线程并发地执行初始化序列代码,可能有2个线程发现控制变量为0,并且都实行初始化,而该过程本该仅仅执行一次。
当然,我们一般的做法是把初始化函数放在main里,创建线程之前来完成。但是如果我们的程序最终不是做成可执行程序,而是编译成库的形式,那么上述方式就没法做到了。
static int s_nThreadResult = 0;
static pthread_once_t once = PTHREAD_ONCE_INIT;
void thread_init()
{
printf("thread_id: %d, result: %d\n", (int)pthread_self(), s_nThreadResult);
s_nThreadResult = -1;
printf("thread_id: %d, result: %d\n", (int)pthread_self(), s_nThreadResult);
}
void* theThread(void*param)
{
pthread_once(&once,&thread_init);
s_nThreadResult++;
pthread_exit(&s_nThreadResult);
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, &theThread, NULL);
pthread_create(&tid2, NULL, &theThread, NULL);
//SLEEP(3);
void* status = NULL;
int rc = pthread_join(tid1, &status);
assert(rc == 0 && "pthread_join1", rc);
if(status != PTHREAD_CANCELED && status != NULL){
printf("Returnedvaluefromthread:%d\n", *(int*)status);
}
rc = pthread_join(tid2, &status);
assert(rc == 0 && "pthread_join2", rc);
if(status != PTHREAD_CANCELED && status != NULL)
{
printf("Returnedvaluefromthread:%d\n", *(int*)status);
}
return 0;
}
线程特定数据
在单线程程序中,我们经常要用到"全局变量"以实现多个函数间共享数据。在多线程环境下,由于数据空间是共享的,因此全局变量也为所有线程所共有。但有时应用程序设计中有必要提供线程私有的全局变量,仅在某个线程中有效,但却可以跨多个函数访问,比如程序可能需要每个线程维护一个链表,而使用相同的函数操作,最简单的办法就是使用同名而不同变量地址的线程相关数据结构。这样的数据结构可以由POSIX线程库维护,称为线程私有数据(Thread-specific Data,或TSD)。
- 可以使用 pthread_key_create() 分配用于标识进程中线程特定数据的键。
第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数。
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
该函数从TSD池中分配一项,将其值赋给key供以后访问使用。如果destructor不为空,在线程退出时将以key所关联的数据为参数调用destructor(),以释放分配的缓冲区。
不论哪个线程调用pthread_key_create(),所创建的key都是所有线程可访问的,但各个线程可根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量。在Linux Threads的实现中,TSD池用一个结构数组表示:
static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] = { { 0, NULL } };
创建一个TSD就相当于将结构数组中的某一项设置为"in_use",并将其索引返回给*key,然后设置destructor函数。
- 注销一个TSD采用如下API:
int pthread_key_delete(pthread_key_t key);
这个函数并不检查当前是否有线程正使用该TSD,也不会调用destructor函数,而只是将TSD释放以供下一次调用pthread_key_create()使用。在Linux Threads中,它还会将与之相关的线程数据项设为NULL。
- TSD的读写都通过专门的POSIX线程库函数进行:
int pthread_setspecific(pthread_key_t key, const void *pointer);
void * pthread_getspecific(pthread_key_t key);
调用写入函数 pthread_setspecific() 时,将pointer的值(不是所指的内容)与key相关联,而相应的读出函数 pthread_getspecific() 则将与key相关联的数据读出来。数据类型都设为void *,因此可以指向任何类型的数据。
二、pthread_join
int pthread_join(pthread_t tid, void **retval);
代码中如果没有 pthread_join 主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。
-
tid:线程标识符,即线程ID,标识唯一线程。
-
retval:用户定义的指针,用来存储被等待线程的返回值。如果 thread 线程没有返回值,又或者不需要接收 thread 线程的返回值,可以将 retval 参数置为 NULL。
pthread_join 仅适用于非分离的目标线程,指定的线程必须位于当前的进程中。如果没有必要等待特定线程终止之后才进行其他处理,则应当将该线程分离。
一个线程不能同时被多个线程等待,也就是说对一个线程只能调用一次pthread_join。如果多个线程等待同一个线程终止,则所有等待线程将一直等到目标线程终止,然后其中一个等待线程成功返回,其余的等待线程将失败并返回 ESRCH 错误。
在 pthread_join 返回之后,应用程序可回收与已终止线程关联的任何数据存储空间。也可以通过设置threads attributes来设置当一个线程结束时,直接回收此线程所占用的系统资源。
三、pthread_detach
int pthread_detach(thread_t tid);
pthread_detach 函数用于指示应用程序在线程 tid 终止时回收其存储空间。如果 tid 尚未终止,pthread_detach 不会终止该线程。
比如在Web服务器中,当主线程为每个新来的链接创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的链接),这时可以在子线程中加入代码pthread_detach(pthread_self()),或者父线程调用pthread_detach(thread_id)(非阻塞,可立即返回)。这将该子线程的状态设置为detached,该子线程运行结束后会自动释放所有资源。
四、pthread_kill
int pthread_kill(thread_t tid, int sig);
pthread_kill() 将信号 sig 发送到由 tid 指定的线程。tid 所指定的线程必须与调用线程在同一个进程中。
在创建的线程中使用signal(SIGKILL, sig_handler)处理信号,如果给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出。
如果 sig 为零,将执行错误检查,但并不实际发送信号。此错误检查可用来检查 tid 的有效性。
五、pthread_cancel
int pthread_cancel(pthread_t tid);
取消请求的处理方式取决于目标线程的状态,该状态由以下两个函数确定:pthread_setcancelstate() 和 pthread_setcanceltype()。
取消线程
取消操作允许线程请求终止其所在进程中的任何其他线程。仅当取消操作安全时才应取消线程。
线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至取消点,由不同的Cancel状态决定。
相关API如下:
int pthread_setcancelstate(int state, int *oldstate)
设置本线程对Cancel信号的反应。
-
state有两种取值: PTHREAD_CANCEL_ENABLE (缺省)和 PTHREAD_CANCEL_DISABLE ,
-
old state 如果不为NULL则存入原来的Cancel状态以便恢复,为空则不存原Cancel状态。
int pthread_setcanceltype(int type, int *oldtype)
设置本线程取消动作的执行时机。
-
type有两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出(延迟模式)和立即执行取消动作并退出(异步模式)。
-
old type如果不为NULL则存入运来的取消动作类型值。
void pthread_testcancel(void)
pthread_testcancel 在不包含取消点,但是又需要取消点的地方创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求。
当线程取消功能处于启用状态且取消类型设置为延迟模式时,pthread_testcancel 函数有效。
取消点
pthreads 标准指定了几个取消点,其中包括:
-
通过 pthread_testcancel 调用建立线程取消点。
-
pthread_cond_wait 或 pthread_cond_timedwait 中的特定条件出现。
-
被 sigwait 阻塞。
-
一些标准的库调用,通常这些调用包括线程可基于其阻塞的函数。
缺省情况下将启用取消功能。有时希望应用程序禁用取消功能(pthread_setcancelstate)。如果禁用取消功能,则会导致延迟所有的取消请求,直到再次启用取消请求。
放置取消点
执行取消操作存在一定的危险,大多数危险与不完全恢复变量和不释放共享资源有关。比如使互斥锁保留为锁定状态,从而导致死锁。或者,已取消的线程保留了已分配的内存区域,但是系统无法识别这一部分内存,从而无法释放它。
取消处理程序提供的清理服务应当将资源和状态恢复到与起点一致的状态。
执行取消操作
在以下三种不同的情况下可能会执行取消操作:
-
异步
-
执行序列中按标准定义的各个点
-
调用 pthread_testcancel 时
缺省情况下,仅在 POSIX 标准可靠定义的点执行取消操作。
六、pthread_exit
void pthread_exit(void *retval);
pthread_exit 用来终止调用线程,将释放所有的线程特定数据绑定。如果调用线程尚未分离,则线程 ID 和 status 指定的退出状态将保持不变,直到应用程序调用 pthread_join 以等待该线程,结果是 joining 线程得到已终止线程的退出状态,已终止的线程将消失。否则将忽略 status,线程 ID 可以立即回收。
- retval:线程退出状态设置为 retval 的内容
在线程中禁止调用 exit 函数,否则会导致整个进程退出。取而代之的是调用 pthread_exit 函数,这个函数是使调用线程退出,即使主线程调用 pthread_exit 函数也不会使整个进程退出,不影响其他线程的执行。
pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用 malloc 分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。
七、pthread_atfork
在进行linux系统里开发时,经常会调用linux的系统函数fork来产生一个子进程,如果父子进程都没有用到pthread线程相关函数,则就不存在需要理解pthread_atfork的函数的必要。问题是有时候既要考虑多线程,又要考虑多进程,这个时候就要仔细理解pthread_atfork这个函数的作用了。
在父进程调用fork函数派生子进程的时候,如果父进程创建了pthread的互斥锁(pthread_mutex_t)对象,那么子进程将自动继承父进程中互斥锁对象,并且互斥锁的状态也会被子进程继承下来。对于父进程中已经加锁的互斥锁,在子进程中也是被锁住的;对于在父进程中未加锁的互斥锁,在子进程中也是未加锁的。
在父进程调用fork之前所创建的pthread_mutex_t对象会在子进程中继续有效,而pthread_mutex_t对象通常是全局对象,会在父进程的任意线程中被加锁或解锁,这样就无法通过简单的方法让子进程明确知道被继承的 pthread_mutex_t对象到底有没有处于加锁状态。因此就有了pthread_atfork这个函数,通过这个函数可以确保子进程继承pthread_mutex_t对象处在未加锁状态,该函数的原型声明如下:
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
该函数通过3个不同阶段的回调函数来处理互斥锁状态,参数如下:
-
prepare:在fork调用创建出子进程之前执行,这个函数是在父进程的上下文中执行的。作用是给父进程中的互斥锁对象加锁。这个时候如果父进程中的某个线程已经调用pthread_mutex_lock给互斥锁加上了锁,则prepare回调将迫使父进程中调用fork的线程处于阻塞状态,直到能给互斥锁对象加锁为止。
-
parent:在fork调用创建出子进程之后,而fork返回之前执行,在父进程上下文中被执行。作用是释放所有在prepare函数中被锁住的互斥锁。
-
child:在fork返回之前,在子进程上下文中被执行。和parent回调一样,child回调也是用于释放所有在prepare函数中被锁住的互斥锁。
示例程序:
int count = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void prepare() {
int err;
printf("prepare: pthread_mutex_lock ...\n");
err = pthread_mutex_lock(&lock);
if (err != 0) ERROR(err, "prepare: pthread_mutex_lock failed");
printf("prepare: lock start...\n");
}
void parent() {
int err;
printf("parent: pthread_mutex_unlock ...\n");
err = pthread_mutex_unlock(&lock);
if (err != 0) ERROR(err, "parent: pthread_mutex_unlock");
}
void child() {
int err;
printf("child: pthread_mutex_unlock ...\n");
err = pthread_mutex_unlock(&lock);
if (err != 0) ERROR(err, "child: pthread_mutex_unlock");
}
void* thread_proc(void* arg) {
while(1) {
pthread_mutex_lock(&lock);
count++;
printf("parent thread: count:%d\n",count);
sleep(10);
pthread_mutex_unlock(&lock);
sleep(1);
}
return NULL;
}
int main(int argc,char * argv[])
{
int err;
pid_t pid;
pthread_t tid;
pthread_create(&tid, NULL, thread_proc, NULL);
err = pthread_atfork(prepare, parent, child);
if (err != 0) ERROR(err, "atfork");
sleep(1);
printf("parent is about to fork ...\n");
pid = fork();
if (pid < 0) ERROR(errno, "fork");
else if (pid == 0) {
// child process
int status;
printf("child running\n");
while(1) {
pthread_mutex_lock(&lock);
count ++;
printf("child: count:%d\n",count);
sleep(2);
pthread_mutex_unlock(&lock);
sleep(1);
}
exit(0);
}
pthread_join(tid, NULL);
return 0;
}
总结
以上内容来源于网络知识总结,如有侵权请私信联系立即删除:)