线程高级编程

一次性初始化

pthread_once_t once_control = PTHREAD_ONCE_INIT;//控制参数,用来确定是否已调用相关的初始化例程。
int pthread_once(pthread_once_t *once_control,void (*init_routine)(void));
//使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。

  如果once_control的初值不是PTHREAD_ONCE_INIT(Linux Threads定义为0),pthread_once() 的行为就会不正常。

#include<iostream>  
#include<pthread.h>  
using namespace std;  
  
pthread_once_t once = PTHREAD_ONCE_INIT;  
  
void once_run(void)  
{  
        cout<<"once_run in thread "<<(unsigned int )pthread_self()<<endl;  
}  
  
void * child1(void * arg)  
{  
        pthread_t tid =pthread_self();  
        cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;  
        pthread_once(&once,once_run);  
        cout<<"thread "<<tid<<" return"<<endl;  
}  
  
  
void * child2(void * arg)  
{  
        pthread_t tid =pthread_self();  
        cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;  
        pthread_once(&once,once_run);  
        cout<<"thread "<<tid<<" return"<<endl;  
}  
  
int main(void)  
{  
        pthread_t tid1,tid2;  
        cout<<"hello"<<endl;  
        pthread_create(&tid1,NULL,child1,NULL);  
        pthread_create(&tid2,NULL,child2,NULL);  
        sleep(10);  
        cout<<"main thread exit"<<endl;  
        return 0;  
  
}
View Code

取消

  取消操作允许线程请求终止其所在进程中的任何其他线程。不希望或不需要对一组相关的线程执行进一步操作时,可以选择执行取消操作。取消线程的一个示例是异步生成取消条件。

  接收到cancel信号,线程有两种处理类型: 立即响应 和 延迟响应(在最近的取消点响应),默认是延迟响应

//发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。
int pthread_cancel(pthread_t thread);

//设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,
//分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。
int pthread_setcancelstate(int state,int *oldstate);

//设置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,
//仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出)
//oldtype如果不为NULL则存入运来的取消动作类型值。
int pthread_setcanceltype(int type,int *oldtype);

//pthread_testcancel在不包含取消点,但是又需要取消点的地方创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求.
//线程取消功能处于启用状态且取消状态设置为延迟状态时,pthread_testcancel()函数有效。
//如果在取消功能处处于禁用状态下调用pthread_testcancel(),则该函数不起作用。
//请务必仅在线程取消线程操作安全的序列中插入pthread_testcancel()。除通过pthread_testcancel()调用以编程方式建立的取消点意外,
//pthread标准还指定了几个取消点。测试退出点,就是测试cancel信号.
void pthread_testcancel(void)

取消模式

模式 状态 类型 含义
off 禁用 二者之一 取消pending被推迟直到启用取消模式
deferred(推迟) 启用 推迟 在下一个取消点执行取消
asynchronous(异步) 启用 异步 可以随时执行取消
  1. 默认情况下取消被推迟,并且在程序的特定点发生,该点检查线程数是否被请求要求终止,被称为取消点。
  2. 可能等待一无界时间的大多数函数应该成为被推迟取消点。推迟的取消点包括等待条件变量,读写文件,以及线程可能被阻塞一段时间的其他函数
  3. 一个线程尚未解锁互斥量就被终止,试图加锁该互斥量的下一个线程永远被阻塞等待下去
  4. 从一个锁住互斥量的终止线程恢复数据的唯一办法:应用程序分析所有的共享数据并且恢复它到一个一致正确的状态
  5. 取消线程是一个异步的,当pthread_cancle返回时,线程未必已被取消,可能仅被通知有一个针对他的未解决的取消请求,如果需要知道线程何时终止,需要在取消他之后调用pthread_join与他连接
  6. 如果有一个异步取消类型集,或当线程下一次达到一个推迟的取消点时,取消请求被系统释放,发生这种情况,系统设置线程的取消类型为PTHREAD_CANCEL_DELIVEROD,取消状态为PTHREAD_CANCEL_DISABLE
  7. 当做为一个取消点函数检测到一个未解决的取消请求时,函数不返回调用者
  8. 如果目标线程从一个取消点返回,另外的线程针对目标现成调用了pthread_cancle,就存在一个未解决的取消,如果这一取消是未解决的,系统将很快开始调用清除函数,然后终止线程。
  9. 当异步取消开启时不允许调用任何获得资源的函数,因为异步取消能打断很多事。在异步取消后不要调用任何代码。

取消点

  1. 通过pthread_testcancel调用以编程方式建立线程取消点。 
  2. 线程等待pthread_cond_wait或pthread_cond_timewait()中的特定条件。 
  3. 被sigwait(2)阻塞的函数 
  4. 大多数取消点包含无限时间阻塞线程的I/O操作
  5. 如果目标线程从上一个取消点返回后,另外的线程对目标调用了pthread_cancel,就存在一个未决的取消,如果这一取消是未决的,系统将很快开始调用清除函数,然后终止线程 

  根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标。

pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();

  其他线程通过调用pthread_cancel()函数向目标线程发送取消请求。取消请求发出后根据目标线程的cancel state来决定取消请求是否会到达目标线程:

  1. 如果目标线程的cancel state是PTHREAD_CANCEL_ENABLE(默认),取消请求会到达目标线程。
  2. 如果目标线程的cancel state是PTHREAD_CANCEL_DISABLE,取消请求会被放入队列。直到目标线程的cancel state变为PTHREAD_CANCEL_ENABLE,取消请求才会从队列里取出,发到目标线程。
void thread_function(void *arg)
{
/**
* 线程准备执行一些关键工作,在这个过程中不希望被取消。
* 所以先通过pthread_setcancelstate()将本线程的cancel state
* 设为disabled。
*/
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
/* 执行关键工作 */
...
/**
* 关键工作执行完成,可以被取消。
* 通过pthread_setcancelstate()将本线程的cancel state
* 设为enabled。
*/
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
/**
* 调用pthread_testcancel()函数,检查一下在cancel state
* 为disabled状态的时候,是否有取消请求发送给本线程。
* 如果有的话就取消(退出)。
*/
pthread_testcancel();
/**
* pthread_testcancel()返回了,表明之前没有取消请求发送给本线程,
* 继续其余的工作。
* 这时候如果有取消请求发送给本线程,会在下一次执行到
* cancellation point的时候(例如sleep(), read(), write(), ...)时取消。
*/
...
/**
* 从这里开始,函数里不再包含cancellation point了。
* 如果收到取消请求,将无法取消。所以先把本线程的cancel type
* 设为asynchronous,收到取消请求将立即取消。
*/
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
/* 不包含cancellation point的代码 */
...
}

清除

  当一段代码被取消时需要恢复一些状态,必须使用清除处理器,当线程在等待一个条件变量时被取消,他将被唤醒,并保持互斥量的加锁状态,在线程终止前,通常需要恢复不变量,总是需要释放互斥来量。

  可以把每一个线程考虑为有一个活动的清除处理函数的栈,用pthread_cleanup_push将清除处理函数加入到栈,pthread_cleanup_pop删除最近添加的处理函数,当线程被取消或调用pthread_exit时,pthread从最近增加清除处理函数开始,依次调用各个活动的清除处理函数。

  清除处理函数也可以设计为线程被取消时,也能经常使用清除处理函数,不论取消是否正常完成,当pthread_cleanup_pop以非零值调用时就算线程没被取消,清除处理函数也要被执行

/* push the handler "routine" on cleanup stack */
void pthread_cleanup_push(void(*routine)(void *), void *args);

//如果弹出函数中的参数为非零值,则会从栈中删除该处理程序并执行该处理程序。如果该参数为零,则会弹出该处理程序,而不执行它。
void pthread_cleanup_pop(int execute);
/* pop the "func" out of cleanup stack and execute "func" */
pthread_cleanup_pop (1);
/* pop the "func" and DONT execute "func" */
pthread_cleanup_pop (0); 

code

    pthread_mutex_lock(&mutex);

    //一些会阻塞程序运行的调用,比如套接字的accept,等待客户连接,这里是随便找的一个可以阻塞的接口
    sock = accept(......);            

    pthread_mutex_unlock(&mutex);
    /*这个例子中,如果线程1执行accept时,线程会阻塞(也就是等在那里,有客户端连接的时候才返回,或则出现其他故障),线程等待中
    这时候线程2发现线程1等了很久,不赖烦了,他想关掉线程1,于是调用pthread_cancel()或者类似函数,请求线程1立即退出。
    这时候线程1仍然在accept等待中,当它收到线程2的cancel信号后,就会从accept中退出,然后终止线程,注意这个时候线程1还没有执行
    pthread_mutex_unlock(&mutex);也就是说锁资源没有释放,这回造成其他线程的死锁问题。*/

    /*所以必须在线程接收到cancel后用一种方法来保证异常退出(也就是线程没达到终点)时可以做清理工作(主要是解锁方面)
    pthread_cleanup_push与pthread_cleanup_pop就是这样的。*/

    pthread_cleanup_push(some_clean_func,...)
    pthread_mutex_lock(&mutex);
    //一些会阻塞程序运行的调用,比如套接字的accept,等待客户连接,里是随便找的一个可以阻塞的接口
    sock = accept(......);            这
    pthread_mutex_unlock(&mutex);
    pthread_cleanup_pop(0);
    return nullptr;
    
    /*上面的代码,如果accept被cancel后线程退出,会自动调用some_clean_func函数,在这个函数中你可以释放锁资源。
    如果accept没有被cancel,那么线程继续执行,当pthread_mutex_unlock(&mutex);表示线程自己正确的释放资源了,
    而执行pthread_cleanup_pop(0);也就是取消掉前面的some_clean_func函数。接着return线程就正确的结束了。*/
    
    //通俗点就是:
    /*pthread_cleanup_push注册一个回调函数,如果你的线程在对应的pthread_cleanup_pop之前异常退出(return是正常退出,
    其他是异常),那么系统就会执行这个回调函数(回调函数要做什么你自己决定)。但是如果在pthread_cleanup_pop之前没有异常退出,
    pthread_cleanup_pop就把对应的回调函数取消了,*/

    //关于取消点的解释:

    //比如你执行:
            printf("thread sleep\n");
            sleep(10);
            printf("thread wake...\n");
   // 在sleep函数中,线程睡眠,结果收到cancel信号,这时候线程从sleep中醒来,但是线程不会立刻退出。
   //这是应为pthread与C库方面的原因,pthread的建议是,如果一个函数是阻塞的,
   //那么你必须在这个函数前后建立取消点,比如:
            printf("thread sleep\n");
            pthread_testcancel();
            sleep(10);
            pthread_testcancel();
            printf("thread wake...\n");
    //这样,就添加了两个取消掉。在执行到pthread_testcancel的位置时,线程才可能响应cancel退出进程。

线程私有数据

  进程内所有的线程共享地址空间,任何声明为静态或外部的变量,或在进程堆声明的变量都可以被进程内的所有线程读写。

  1. static,extern,或堆变量的值是上次线程改写的值
  2. 一个线程真正拥有的唯一私有存储时处理器寄存器。甚至栈地址也能被共享,寄存器和“私有”堆栈都不能代替非线程代码中使用的持久静态存储

  当线程需要有个私有变量时,首先决定所有线程时候共享相同的值,或线程是否该有他自己的值

  1. 如果共享变量,使用静态或外部数据,同步跨越多线程对共享数据进行存取
  2. 如果每个线程需要个私有变量,在每个线程的堆栈中分配内存并将值保存在那里,但是代码需要任何线程发现私有数据,私有数据允许每个线程有一份变量的拷贝,好像每个线程有一连串公共键值索引私有数据

  程序创建一个键,每个线程能独立的设定或得到自己的键值,键对于所有的线程是相同的,每个键值能将它独立的键值与共享键所联系,每个线程能将它独立的键值与共享键联系,每个线程能在任何时间为键改变他的私有值。

      线程特定数据看似很复杂,其实我们可以把它理解为就是一个索引指针。key结构中存储的是索引,pthread结构中存储的是指针,指向线程中的私有数据,通常是malloc函数返回的指针。

#include <pthread.h>

pthread_key_t keyp;
//成功返回0,失败返回错误号。必须保证pthread_key_t只被创建一次,如果创建两次,第二个键覆盖第一个键
//第一个键与任何线程相关设置的值一起丢失
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *)); 
//成功返回0,失败返回错误号。
int pthread_key_delete(pthread_key_t *key);
//返回线程特定数据,或者如果没有值关联到这个关键字时返回NULL。
void *pthread_getspecific(pthread_key_t key); 
//成功返回0,失败返回错误号
int pthread_setspecific(pthread_key_t key, const void *value); 

  除了进程范围内地的key结构数组外,系统还在进程中维护关于每个线程的线程结构,把这个特定于线程的结构称为pthread结构,它的部分内容是于key数组对应的指针数组(如图2所示),它的128个指针和进程中的128个可能的键(索引)是逐一关联的。指针指向的内存就是线程特有数据。

  1. pthread_key_create对于每个pthread_key_t只能被调用一次,如果创建两次,第二次将第一次的值覆盖
  2. pthreads保证一次只有128个线程私有数据键,由<limits.h>中的PTHREAD_KEYS_MAX指定
  3. 释放线程私有数据键时,不影响任何线程对该键设置的当前值,甚至不影响调用线程的当前键值,使用已删除的线程私有数据键导致未定义的行为
  4. 当没有线程持有该键的值时,才能删除线程私有数据键
  5. 不能把pthread_setspecific的value设置线程私有数据为NULL,因为此值意味着不是在赋空值,而是说该键在当前的线程不在有值
  6. 任何新建,初始值为NULL
  7. 当一个线程终止时,调用键的destructor函数前将对应的线程私有数据值清空,设置为NULL
  8. 如果线程的私有数据值存储的是地址,在destructor中释放时要传递给destructor参数,而非调用pthrad_getspecific的参数
  9. 终止线程对于某键的值为NULL的不调用destructor
  10. destructor仅在线程终止时被调用,而不是当线程私有数据键值改变时
  11. 线程私有数据的键的destructor函数在你替代现存的键值时不会被调用。即:如果在堆中分配一个结构并将它指向该结构的指针作为线程私有数据的值,则在以后分配一个新结构并且把它指向新结构的指针赋值给相同的线程私有数据键时,需要将旧结构释放。phtreads不会释放旧结构,也不会使用指向旧结构的指针调用你的destructor函数

destructor

  此函数是当线程退出时他处理有一些线程私有数据键定义的值。如果键值指向堆寄存器的指针,需要释放存储器避免每次线程终止时留下内存泄漏,当线程终止时,具有非空私有数据的键值的一个线程终止时,键的destructor将以当前值为参数被调用。

  当一个线程退出时,pthrads在进程中检查所有私有数据的键,并且将不是空的线程私有数据置空然后调用destructor函数

  如果从destructor函数中调用其他函数,destructor将为正在被破坏或其他任何键赋新值

  pthreads实现在核对列表某个固定的次数后再放弃,当放弃它时,最终的线程的私有数据值没有被破坏,如果值指向堆存储器的指针,结果可能是内存泄漏,<limits.h>中TPHREAD_DESTRUCTOR规定了系统检查的列表次数,并且值至少为4,或不停的检查,则总是设置线程私有数据值得destructor函数引起无限循环。

  通常仅当子系统1使用了取决于另外独立的子系统2的线程私有数据时,新的线程私有数据值才在destructor函数内被设置;如果子系统1的destructor需要调用子系统2,他可能无意中导致子系统2分配新的线程私有数据,尽管子系统2的destructor需要被再次调用以释放新数据,子系统1的线程私有数据仍保持NULL,所以循环将终止。

  启动一个进程并创建了若干线程,其中一个线程(比如线程1),要申请线程私有数据,系统调用pthread_key_creat()在图1所示的key结构数组中找到第一个未用的元素,并把它的键,也就是看面说的索引(0-127),返回给调用者,假设返回的索引是1,线程之后通过pthrea_getspecific()调用获得本线程的pkey[1]值,返回的是一个空指针ptr = null,这个指针就是我们可以通过索引1使用的线程数据的首地址了,但是他现在为空,因此根据实际情况用malloc分配一快内存,在使用pthread_setspecific()调用将特定数据的指针指向刚才分配到内存区域。整个过程结束后key结构和pthread

void destructor(void *) 

pthread_key_t key; 

pthread_once_t init_done = PTHREAD_ONCE_INIT; 

void thread_init(void) 
{ 
    err = pthread_key_create(&key, destructor); 
} 
int threadfunc(void *arg) 
{ 
    pthread_once(&init_done, thread_init); //保证只被创建一次
    
    if( (ptr = pthread_getspecific(key)) == NULL ){
        ptr = malloc(len);
        pthread_setspecific(key,ptr);
        ...
    }
    ... 
}

  当调用pthread_key_create 后会产生一个所有线程都可见的线程特定数据(TSD)的键值(如上图中所有的线程都会得到一个pkey[1]的值), 但是这个键所指向的真实数据却是不同的,虽然都是pkey[1], 但是他们并不是指向同一块内存,而是指向了只属于自己的实际数据, 因此, 如果线程0更改了pkey[1]所指向的数据, 而并不能够影像到线程n;

  在线程调用pthread_setspecific后会将每个线程的特定数据与thread_key_t绑定起来,虽然只有一个pthread_key_t,但每个线程的特定数据是独立的内存空间,当线程退出时会执行destructor 函数。

/** 示例1: 设置/获取线程特定数据 
在两个线程中分别设置/获取线程特定数据, 查看两个线程中的数据是否是一样的(肯定是不一样的O(∩_∩)O~) 
**/  
pthread_key_t key;  
typedef struct Tsd  
{  
    pthread_t tid;  
    char *str;  
} tsd_t;  
//用来销毁每个线程所指向的实际数据,线程私有数据键的destruct函数在你替代键值时不会被调用,
void destructor_function(void *value)  
{  
    free(value);  
    cout << "destructor ..." << endl;  
}  
  
void *thread_routine(void *args)  
{  
    //设置线程特定数据  
    tsd_t *value = (tsd_t *)malloc(sizeof(tsd_t));  
    value->tid = pthread_self();  
    value->str = (char *)args;  
    pthread_setspecific(key, value);  
    printf("%s setspecific, address: %p\n", (char *)args, value);  
  
    //获取线程特定数据  
    value = (tsd_t *)pthread_getspecific(key);  
    printf("tid: 0x%x, str = %s\n", (unsigned int)value->tid, value->str);  
    sleep(2);  
  
    //再次获取线程特定数据  
    value = (tsd_t *)pthread_getspecific(key);  
    printf("tid: 0x%x, str = %s\n", (unsigned int)value->tid, value->str);  
  
    pthread_exit(NULL);  
}  
  
int main()  
{  
    //这样每个线程当中都会有一个key可用了,  
    //但是每个key所绑定的实际区域需要每个线程自己指定  
    pthread_key_create(&key, destructor_function);  
  
    pthread_t tid1, tid2;  
    pthread_create(&tid1, NULL, thread_routine, (void *)"thread1");  
    pthread_create(&tid2, NULL, thread_routine, (void *)"thread2");  
  
    pthread_join(tid1, NULL);  
    pthread_join(tid2, NULL);  
    pthread_key_delete(key);  
  
    return 0;  
}  
/** 示例2:运用pthread_once, 让key只初始化一次 
注意: 将对key的初始化放入到init_routine中 
**/  
pthread_key_t key;  
pthread_once_t once_control = PTHREAD_ONCE_INIT;  
typedef struct Tsd  
{  
    pthread_t tid;  
    char *str;  
} tsd_t;  
  
//线程特定数据销毁函数,  
//用来销毁每个线程所指向的实际数据  
void destructor_function(void *value)  
{  
    free(value);  
    cout << "destructor ..." << endl;  
}  
  
//初始化函数, 将对key的初始化放入该函数中,  
//可以保证inti_routine函数只运行一次  
void init_routine()  
{  
    pthread_key_create(&key, destructor_function);  
    cout << "init..." << endl;  
}  
  
void *thread_routine(void *args)  
{  
    pthread_once(&once_control, init_routine);  
  
    //设置线程特定数据  
    tsd_t *value = (tsd_t *)malloc(sizeof(tsd_t));  
    value->tid = pthread_self();  
    value->str = (char *)args;  
    pthread_setspecific(key, value);  
    printf("%s setspecific, address: %p\n", (char *)args, value);  
  
    //获取线程特定数据  
    value = (tsd_t *)pthread_getspecific(key);  
    printf("tid: 0x%x, str = %s\n", (unsigned int)value->tid, value->str);  
    sleep(2);  
  
    //再次获取线程特定数据  
    value = (tsd_t *)pthread_getspecific(key);  
    printf("tid: 0x%x, str = %s\n", (unsigned int)value->tid, value->str);  
  
    pthread_exit(NULL);  
}  
  
int main()  
{  
    pthread_t tid1, tid2;  
    pthread_create(&tid1, NULL, thread_routine, (void *)"thread1");  
    pthread_create(&tid2, NULL, thread_routine, (void *)"thread2");  
  
    pthread_join(tid1, NULL);  
    pthread_join(tid2, NULL);  
    pthread_key_delete(key);  
  
    return 0;  
}  

 

posted on 2018-06-18 14:13  tianzeng  阅读(4005)  评论(0编辑  收藏  举报

导航