一、基本概念
1、线程是计算机中独立运行的最小单位。进程是分配资源的单位。
2、为什么使用多线程?
(1)启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右。
(2)使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
此外,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
3、线程不像进程那样,不是按照严格的父子层次来组织的。和一个进程相关的线程组成一个对等线程池(a pool of peers)。对等(线程)池概念的主要影响是,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止;进一步来说,每个对等线程都能读写相同的共享数据。
4、相关函数
1)创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);
pthread_t pthread_self(void);
int pthread_equal(pthread_t thread1,pthread_t thread2);
int pthread_once(pthread_once_t *once_control,void(*init_routine)(void));
linux系统支持POSIX多线程接口,称为pthread。编写linux下的多线程程序,需要包含头文件pthread.h,链接时需要使用库libpthread.a。
如果在主线程里面创建线程,程序就会在创建线程的地方产生分支,变成两个部分执行。线程的创建通过函数pthread_create来完成。成功返回0。
参数:
thread: 参数是一个指针,当线程成功创建时,返回创建线程ID。
attr: 用于指定线程的属性
start_routine: 该参数是一个函数指针,指向线程创建后要调用的函数。
arg: 传递给线程函数的参数。
2)线程终止
两种方式终止线程。
第一通过return从线程函数返回,
第二种通过调用pthread_exit()函数使线程退出。
需要注意的地方:一是,主线程中如果从main函数返回或是调用了exit函数退出主线程,则整个进程终止,此时所有的其他线程也将终止。另一种是,如果主线程调用pthread_exit函数,则仅仅是主线程消亡,进程不会结束,其他线程也不会结束,知道所有的线程都结束时,进程才结束。
3)线程属性
/* man pthread_attr_init */
typedef struct
{
int detachstate; //是否与其他线程脱离同步
int schedpolicy; //新线程的调度策略
struct sched_param schedparam; //运行优先级等
int inheritsched; //是否继承调用者线程的值
int scope; //线程竞争CPU的范围(优先级的范围)
size_t guardsize; //警戒堆栈的大小
int stackaddr_set; //堆栈地址集
void * stackaddr; //堆栈地址
size_t stacksize; //堆栈大小
} pthread_attr_t;
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。
(1)关于线程绑定
关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
线程的分离与结合
在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。
线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态(即可结合的,joinable,需要回收),这种情况下,原有的线程等待创建的线程结束;只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。
设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。
4)线程等待——正确处理线程终止
#include <pthread.h>
void pthread_exit(void *retval);
void pthread_join(pthread_t th,void *thread_return);//挂起等待th结束,*thread_return=retval;
int pthread_detach(pthread_t th);
如果线程处于joinable状态,则只能只能被创建他的线程等待终止。
在Linux平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。(说明:线程处于joinable状态下)
调用该函数的线程将挂起,等待 th所表示的线程的结束。 thread_return是指向线程 th返回值的指针。需要注意的是 th所表示的线程必须是 joinable的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对th 调用pthread_join() 。如果th 处于detached 状态,那么对th 的pthread_join() 调用将返回错误。
如果不关心一个线程的结束状态,那么也可以将一个线程设置为 detached状态,从而让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为detached状态可以通过两种方式来实现。一种是调用 pthread_detach()函数,可以将线程 th设置为 detached状态。另一种方法是在创建线程时就将它设置为 detached状态,首先初始化一个线程属性变量,然后将其设置为 detached状态,最后将它作为参数传入线程创建函数pthread_create(),这样所创建出来的线程就直接处于detached 状态。
创建detach 线程:
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, THREAD_FUNCTION, arg);
总之为了在使用 pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于detached 状态,否着就需要调用pthread_join() 函数来对其进行资源回收。
线程私有数据进程内的所有线程共享进程的数据空间,因此全局变量为所有线程所共有。但有时线程也需要保存自己的私有数据,这时可以创建线程私有数据(Thread-specific Date)TSD来解决。在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
线程私有数据采用了一键多值的技术,即一个键对应多个数值,访问数据时好像是对同一个变量进行访问,但其实是在访问不同的数据。
创建私有数据的函数有4个:pthread_key_create(创建), pthread_setspecific(设置), pthread_getspecific(获取), pthread_key_delete(删除)。
#include <pthread.h>
int pthread_key_creadte(pthread_key_t *key,void (*destr_fuction) (void *));
int pthread_setspecific(pthread_key_t key,const void * pointer));
void * pthread_getspecific(pthread_key_t key);
int pthread_key_delete(ptherad_key_t key);
6)线程同步
线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和异步信号。
1)互斥锁(mutex)
通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex *mutex);
int pthread_mutex_destroy(pthread_mutex *mutex);
int pthread_mutex_unlock(pthread_mutex *
(1)先初始化锁init()或静态赋值pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER
attr_t有:
PTHREAD_MUTEX_TIMED_NP:其余线程等待队列
PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,允许线程多次加锁,不同线程,解锁后重新竞争
PTHREAD_MUTEX_ERRORCHECK_NP:检错,与一同,线程请求已用锁,返回EDEADLK;
PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,解锁后重新竞争
(2)加锁,lock,trylock,lock阻塞等待锁,trylock立即返回EBUSY
(3)解锁,unlock需满足是加锁状态,且由加锁线程解锁
(4)清除锁,destroy(此时锁必需unlock,否则返回EBUSY,//Linux下互斥锁不占用内存资源
示例代码
编译: g++ -o thread testthread.cpp -lpthread#include <cstdio> #include <cstdlib> #include <unistd.h> #include <pthread.h> #include "iostream" using namespace std; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int tmp; void* thread(void *arg) { cout << "thread id is " << pthread_self() << endl; pthread_mutex_lock(&mutex); tmp = 12; cout << "Now a is " << tmp << endl; pthread_mutex_unlock(&mutex); return NULL; } int main() { pthread_t id; cout << "main thread id is " << pthread_self() << endl; tmp = 3; cout << "In main func tmp = " << tmp << endl; if (!pthread_create(&id, NULL, thread, NULL)) { cout << "Create thread success!" << endl; } else { cout << "Create thread failed!" << endl; } pthread_join(id, NULL); pthread_mutex_destroy(&mutex); return 0; }
说明:pthread库不是Linux系统默认的库,连接时需要使用静态库libpthread.a,所以在使用pthread_create()创建线程,以及调用pthread_atfork()函数建立fork处理程序时,需要链接该库。在编译中要加 -lpthread参数。
条件变量(cond)利用线程间共享的全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞
(1)初始化.init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER(前者为动态初始化,后者为静态初始化);属性置为NULL
(2)等待条件成立.pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真,timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)
(3)激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
(4)清除条件变量:destroy;无线程等待,否则返回EBUSY
对于
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
一定要在mutex的锁定区域内使用。
如果要正确的使用pthread_mutex_lock与pthread_mutex_unlock,请参考
pthread_cleanup_push和pthread_cleanup_pop宏,它能够在线程被cancel的时候正确的释放mutex!
另外,posix1标准说,pthread_cond_signal与pthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是说,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。
说明:
(1)pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。
(2)互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)。
(3)pthread_cond_timedwait 和 pthread_cond_wait 一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间。如果在abstime指定的时间内cond未触发,互斥量mutex被重新加锁,且pthread_cond_timedwait返回错误 ETIMEDOUT。abstime 参数指定一个绝对时间,时间原点与 time 和 gettimeofday 相同:abstime = 0 表示 1970年1月1日00:00:00 GMT。
(4)pthread_cond_destroy 销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程。
(5)条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁。
示例程序1
#include <stdio.h> #include <pthread.h> #include "stdlib.h" #include "unistd.h" pthread_mutex_t mutex; pthread_cond_t cond; void hander(void *arg) { free(arg); (void)pthread_mutex_unlock(&mutex); } void *thread1(void *arg) { pthread_cleanup_push(hander, &mutex); while(1) { printf("thread1 is running\n"); pthread_mutex_lock(&mutex); pthread_cond_wait(&cond,&mutex); printf("thread1 applied the condition\n"); pthread_mutex_unlock(&mutex); sleep(4); } pthread_cleanup_pop(0); } void *thread2(void *arg) { while(1) { printf("thread2 is running\n"); pthread_mutex_lock(&mutex); pthread_cond_wait(&cond,&mutex); printf("thread2 applied the condition\n"); pthread_mutex_unlock(&mutex); sleep(1); } } int main() { pthread_t thid1,thid2; printf("condition variable study!\n"); pthread_mutex_init(&mutex,NULL); pthread_cond_init(&cond,NULL); pthread_create(&thid1,NULL,thread1,NULL); pthread_create(&thid2,NULL,thread2,NULL); sleep(1); do { pthread_cond_signal(&cond); }while(1); sleep(20); pthread_exit(0); return 0; }
示例程序2
#include <pthread.h> #include <unistd.h> #include "stdio.h" #include "stdlib.h" static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; struct node { int n_number; struct node *n_next; } *head = NULL; /*[thread_func]*/ static void cleanup_handler(void *arg) { printf("Cleanup handler of second thread./n"); free(arg); (void)pthread_mutex_unlock(&mtx); } static void *thread_func(void *arg) { struct node *p = NULL; pthread_cleanup_push(cleanup_handler, p); while (1) { //这个mutex主要是用来保证pthread_cond_wait的并发性 pthread_mutex_lock(&mtx); while (head == NULL) { //这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何 //这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线 //程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。 //这个时候,应该让线程继续进入pthread_cond_wait // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx, //然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立 //而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源 //用这个流程是比较清楚的/*block-->unlock-->wait() return-->lock*/ pthread_cond_wait(&cond, &mtx); p = head; head = head->n_next; printf("Got %d from front of queue/n", p->n_number); free(p); } pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁 } pthread_cleanup_pop(0); return 0; } int main(void) { pthread_t tid; int i; struct node *p; //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而 //不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大 pthread_create(&tid, NULL, thread_func, NULL); sleep(1); for (i = 0; i < 10; i++) { p = (struct node*)malloc(sizeof(struct node)); p->n_number = i; pthread_mutex_lock(&mtx); //需要操作head这个临界资源,先加锁, p->n_next = head; head = p; pthread_cond_signal(&cond); pthread_mutex_unlock(&mtx); //解锁 sleep(1); } printf("thread 1 wanna end the line.So cancel thread 2./n"); //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出 //线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。 pthread_cancel(tid); pthread_join(tid, NULL); printf("All done -- exiting/n"); return 0; }
信号量
如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。
#include <semaphore.h>
int sem_init (sem_t *sem , int pshared, unsigned int value);
这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux 只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。
两个原子操作函数:
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
这两个函数都要用一个由sem_init调用初始化的信号量对象的指针做参数。
sem_post:给信号量的值加1;
sem_wait:给信号量减1;对一个值为0的信号量调用sem_wait,这个函数将会等待直到有其它线程使它不再是0为止。
int sem_destroy(sem_t *sem);
这个函数的作用是再我们用完信号量后都它进行清理。归还自己占有的一切资源。
参考:
【1】 http://www.cnblogs.com/feisky/archive/2009/11/12/1601824.html
【2】 http://www.cnblogs.com/mydomain/archive/2011/07/10/2102147.html
【3】 线程函数介绍
http://www.unix.org/version2/whatsnew/threadsref.html
【4】 http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html
【5】 线程常用函数简介
http://www.rosoo.net/a/201004/8954.html
【6】 条件变量
http://blog.csdn.net/hiflower/article/details/2195350
【7】条件变量函数说明
http://blog.csdn.net/hairetz/article/details/4535920
以下内容根据【1】进行整理。关于取消点,将在后面进一步讨论。
1、一般来说,Posix的线程终止有两种情况:正常终止和非正常终止。线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;非正常终止是线程在其他线程的干预下,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。
2、线程终止时的清理
不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑解决的问题。
最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果线程处于响应取消状态,且采用异步方式响应,或者在打开独占锁以前的运行路径上存在取消点,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。
在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() 的调用将在清理函数栈中形成一个函数链;从pthread_cleanup_push的调用点到pthread_cleanup_pop之间的程序段中的终止动作(包括调用pthread_exit()和异常终止,不包括return)都将执行pthread_cleanup_push()所指定的清理函数。
在执行该函数链时按照压栈的相反顺序弹出。execute参数表示执行到 pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。
pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的,这是pthread.h中的宏定义:
#define pthread_cleanup_push(routine,arg) \
{
struct _pthread_cleanup_buffer _buffer; \
_pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute) \
_pthread_cleanup_pop (&_buffer, (execute)); \
}
可见,pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。
在下面的例子里,当线程在"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);
【1】 http://wenku.baidu.com/view/fd4a162e0066f5335a812191.html
这篇文章主要从一个 Linux 下一个 pthread_cancel 函数引起的多线程死锁小例子出发来说明 Linux 系统对 POSIX 线程取消点的实现方式,以及如何避免因此产生的线程死锁。
目 录:
1. 一个 pthread_cancel 引起的线程死锁小例子
2. 取消点(Cancellation Point)
3. 取消类型(Cancellation Type)
4. Linux 的取消点实现
5. 对示例函数进入死锁的解释
6. 如何避免因此产生的死锁
7. 结论
8. 参考文献
1. 一个 pthread_cancel 引起的线程死锁小例子
下面是一段在Linux 平台下能引起线程死锁的小例子。这个实例程序仅仅是使用了条件变量和互斥量进行一个简单的线程同步,thread0 首先启动,锁住互斥量 mutex,然后调用 pthread_cond_wait,它将线程 tid[0] 放在等待条件的线程列表上后,对 mutex 解锁。thread1 启动后等待 10 秒钟,此时 pthread_cond_wait 应该已经将 mutex 解锁,这时 tid[1] 线程锁住 mutex,然后广播信号唤醒 cond 等待条件的所有等待线程,之后解锁 mutex。当 mutex 解锁后,tid[0] 线程的 pthread_cond_wait 函数重新锁住 mutex 并返回,最后 tid[0] 再对 mutex 进行解锁。
示例代码
#include <pthread.h> #include "stdio.h" #include "stdlib.h" #include "unistd.h" pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void* thread0(void* arg) { pthread_mutex_lock(&mutex); printf("in thread 0 tag 1\n"); pthread_cond_wait(&cond, &mutex); printf("in thread 0 tag 2\n"); pthread_mutex_unlock(&mutex); printf("in thread 0 tag 3\n"); pthread_exit(NULL); } void* thread1(void* arg) { sleep(10); printf("in thread 1 tag 1\n"); pthread_mutex_lock(&mutex); printf("in thread 1 tag 2\n"); pthread_cond_broadcast(&cond); pthread_mutex_unlock(&mutex); printf("in thread 1 tag 3\n"); pthread_exit(NULL); } int main() { pthread_t tid[2]; if (pthread_create(&tid[0], NULL, thread0, NULL) != 0) { exit(1); } if (pthread_create(&tid[1], NULL, thread1, NULL) != 0) { exit(1); } sleep(5); printf("in main thread tag 1\n"); pthread_cancel(tid[0]); pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return 0; }
示例代码_对上述程序的跟踪
[Thread debugging using libthread_db enabled] Breakpoint 8, main () at testthread.cpp:34 34 if (pthread_create(&tid[0], NULL, thread0, NULL) != 0) (gdb) bt #0 main () at testthread.cpp:34 (gdb) n [New Thread 0xb7fecb70 (LWP 2494)] in thread 0 tag 1 Breakpoint 9, main () at testthread.cpp:38 38 if (pthread_create(&tid[1], NULL, thread1, NULL) != 0) (gdb) bt #0 main () at testthread.cpp:38 (gdb) n [Switching to Thread 0xb7fecb70 (LWP 2494)] Breakpoint 1, thread0 (arg=0x0) at testthread.cpp:13 13 pthread_cond_wait(&cond, &mutex); (gdb) n [New Thread 0xb77ebb70 (LWP 2495)] in main thread tag 1 [Switching to Thread 0xb7fee6d0 (LWP 2491)] Breakpoint 10, main () at testthread.cpp:44 44 pthread_cancel(tid[0]); (gdb) n in thread 1 tag 1 Breakpoint 11, main () at testthread.cpp:46 46 pthread_join(tid[0], NULL); (gdb) n [Switching to Thread 0xb77ebb70 (LWP 2495)] Breakpoint 2, thread1 (arg=0x0) at testthread.cpp:24 24 pthread_mutex_lock(&mutex); (gdb) n [Thread 0xb7fecb70 (LWP 2494) exited] [Switching to Thread 0xb7fee6d0 (LWP 2491)] Breakpoint 12, main () at testthread.cpp:47 47 pthread_join(tid[1], NULL); (gdb) n ^C Program received signal SIGINT, Interrupt. 0x00110416 in __kernel_vsyscall () (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep y 0x08048742 in thread0(void*) at testthread.cpp:13 breakpoint already hit 1 time 2 breakpoint keep y 0x080487a4 in thread1(void*) at testthread.cpp:24 breakpoint already hit 1 time 3 breakpoint keep y 0x08048762 in thread0(void*) at testthread.cpp:15 4 breakpoint keep y 0x0804877a in thread0(void*) at testthread.cpp:17 5 breakpoint keep y 0x080487bc in thread1(void*) at testthread.cpp:26 6 breakpoint keep y 0x080487c8 in thread1(void*) at testthread.cpp:27 7 breakpoint keep y 0x080487e0 in thread1(void*) at testthread.cpp:29 8 breakpoint keep y 0x080487f5 in main() at testthread.cpp:34 breakpoint already hit 1 time 9 breakpoint keep y 0x0804882e in main() at testthread.cpp:38 breakpoint already hit 1 time 10 breakpoint keep y 0x08048882 in main() at testthread.cpp:44 breakpoint already hit 1 time ---Type <return> to continue, or q <return> to quit--- 11 breakpoint keep y 0x0804888e in main() at testthread.cpp:46 breakpoint already hit 1 time 12 breakpoint keep y 0x080488a2 in main() at testthread.cpp:47 breakpoint already hit 1 time 13 breakpoint keep y 0x080488b6 in main() at testthread.cpp:49 (gdb)
我们发现,
Breakpoint 12, main () at testthread.cpp:47
47 pthread_join(tid[1], NULL);
(gdb) n
^C
一直卡在这里。
看起来似乎没有什么问题,但是 main 函数调用了一个 pthread_cancel 来取消 tid[0] 线程。上面程序编译后运行时会发生无法终止情况,看起来像是 pthread_cancel 将 tid[0] 取消时没有执行 pthread_mutex_unlock 函数,这样 mutex 就被永远锁住,线程 tid[1] 也陷入无休止的等待中。事实是这样吗?
2. 取消点(Cancellation Point)
要注意的是 pthread_cancel 调用并不等待线程终止,它只提出请求。线程在取消请求(pthread_cancel)发出后会继续运行,直到到达某个取消点(Cancellation Point)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。pthread_cancel manual 说以下几个 POSIX 线程函数是取消点:
pthread_join(3)
pthread_cond_wait(3)
pthread_cond_timedwait(3)
pthread_testcancel(3)
sem_wait(3)
sigwait(3)
以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。
在中间我们可以找到 pthread_cond_wait 就是取消点之一。
但是,令人迷惑不解的是,所有介绍 Cancellation Points 的文章都仅仅说,当线程被取消后,将继续运行到取消点并发生取消动作。但我们注意到上面例子中 pthread_cancel 前面 main 函数已经 sleep 了 5 秒,那么在 pthread_cancel 被调用时,thread0 到底运行到 pthread_cond_wait 没有?
如果 thread0 运行到了 pthread_cond_wait,那么照上面的说法,它应该继续运行到下一个取消点并发生取消动作,而后面并没有取消点,所以 thread0 应该运行到 pthread_exit 并结束,这时 mutex 就会被解锁,这样就不应该发生死锁啊。
说明:
从我的GDB中可以看出,运行到pthread_cond_wait这里后,就没有往下运行了。应该说,这是当前的取消点。
3. 取消类型(Cancellation Type)
我们会发现,通常的说法:某某函数是 Cancellation Points,这种方法是容易令人混淆的。因为函数的执行是一个时间过程,而不是一个时间点。其实真正的 Cancellation Points 只是在这些函数中 Cancellation Type 被修改为 PHREAD_CANCEL_ASYNCHRONOUS 和修改回 PTHREAD_CANCEL_DEFERRED 中间的一段时间。
POSIX 的取消类型有两种,一种是延迟取消(PTHREAD_CANCEL_DEFERRED),这是系统默认的取消类型,即在线程到达取消点之前,不会出现真正的取消;另外一种是异步取消(PHREAD_CANCEL_ASYNCHRONOUS),使用异步取消时,线程可以在任意时间取消。
4. Linux 的取消点实现
下面我们看 Linux 是如何实现取消点的。(其实这个准确点儿应该说是 GNU 取消点实现,因为 pthread 库是实现在 glibc 中的。) 我们现在在 Linux 下使用的 pthread 库其实被替换成了 NPTL,被包含在 glibc 库中。
以 pthread_cond_wait 为例,glibc-2.6/nptl/pthread_cond_wait.c 中:
示例代码
145 /* Enable asynchronous cancellation. Required by the standard. */ 146 cbuffer.oldtype = __pthread_enable_asynccancel (); 147 148 /* Wait until woken by signal or broadcast. */ 149 lll_futex_wait (&cond->__data.__futex, futex_val); 150 151 /* Disable asynchronous cancellation. */ 152 __pthread_disable_asynccancel (cbuffer.oldtype);我们可以看到,在线程进入等待之前,pthread_cond_wait 先将线程取消类型设置为异步取消(__pthread_enable_asynccancel),当线程被唤醒时,线程取消类型被修改回延迟取消 __pthread_disable_asynccancel 。
这就意味着,所有在 __pthread_enable_asynccancel 之前接收到的取消请求都会等待__pthread_enable_asynccancel 执行之后进行处理,所有在__pthread_disable_asynccancel之前接收到的请求都会在 __pthread_disable_asynccancel 之前被处理,所以真正的 Cancellation Point 是在这两点之间的一段时间。(也就是在__pthread_enable_asynccancel与__pthread_disable_asynccancel间处理取消请求)
5. 对示例函数进入死锁的解释
当main函数中调用 pthread_cancel 前,thread0 已经进入了 pthread_cond_wait 函数并将自己列入等待条件的线程列表中(lll_futex_wait)。这个可以通过 GDB 在各个函数上设置断点来验证。
当 pthread_cancel 被调用时,tid[0] 线程仍在等待,取消请求发生在 __pthread_disable_asynccancel 前,所以会被立即响应。但是 pthread_cond_wait 为注册了一个线程清理程序(glibc-2.6/nptl/pthread_cond_wait.c):
126 /* Before we block we enable cancellation. Therefore we have to
127 install a cancellation handler. */
128 __pthread_cleanup_push (&buffer, __condvar_cleanup, &cbuffer);
那么这个线程 清理程序 __condvar_cleanup 干了什么事情呢?我们可以注意到在它的实现最后(glibc-2.6/nptl/pthread_cond_wait.c):
85 /* Get the mutex before returning unless asynchronous cancellation
86 is in effect. */
87 __pthread_mutex_cond_lock (cbuffer->mutex);
88}
哦,__condvar_cleanup 在最后将 mutex 重新锁上了。而这时候 thread1 还在休眠(sleep(10)),等它醒来时,mutex 将会永远被锁住,这就是为什么 thread1 陷入无休止的阻塞中。
【可是为什么pthread_cond_wait要在最后上锁呢?】
6. 如何避免因此产生的死锁
由于线程清理函数 pthread_cleanup_push 使用的策略是先进后出(FILO),那么我们可以在 pthread_cond_wait 函数前先注册一个线程处理函数:
示例代码
void cleanup(void *arg) { pthread_mutex_unlock(&mutex); } void* thread0(void* arg) { pthread_cleanup_push(cleanup, NULL); // thread cleanup handler pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); pthread_cleanup_pop(0); pthread_exit(NULL); }这样,当线程被取消时,先执行 pthread_cond_wait 中注册的线程清理函数 __condvar_cleanup,将 mutex 锁上,再执行 thread0 中注册的线程处理函数 cleanup,将mutex解锁。这样就避免了死锁的发生。
7. 结论
多线程下的线程同步一直是一个让人很头痛的问题。POSIX 为了避免立即取消程序引起的资源占用问题而引入的 Cancellation Points 概念是一个非常好的设计,但是不合适的使用 pthread_cancel 仍然会引起线程同步的问题。了解POSIX 线程取消点在 Linux 下的实现更有助于理解它的机制和有利于更好的应用这个机制。
8. 参考文献
[1] W. Richard Stevens, Stephen A. Rago: Advanced Programming in the UNIX Environment, 2nd Edition.
[2] Linux Manpage
http://wzw19191.blog.163.com/blog/static/131135470200992610550684/
[3] http://hi.baidu.com/hackers365/blog/item/412d0f085c1fd18f0a7b8205.html
【4】 http://blog.csdn.net/yanook/article/details/6589798
线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。
线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点,也就是说设置一个CANCELED状态,线程继续运行,只有运行至Cancelation-point的时候才会退出。
(1)什么是线程取消点
根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段:
pthread_testcancel();
retcode = read(fd, buffer, length);
(2)与线程取消相关的pthread函数
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)
Calling pthread_testcancel() creates a cancellation point within the calling thread, so that a thread that is otherwise executing code that contains no cancellation points will respond to a cancellation request.
If cancelability is disabled (using pthread_setcancelstate(3)), or no cancellation request is pending, then a call to pthread_cancel(3) has no effect.
也就是说pthread_testcancel在不包含取消点,但是又需要取消点的地方创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求。
示例代码【3】
#include <pthread.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #define handle_error_en(en, msg) \ do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0) static int done = 0; static int cleanup_pop_arg = 0; static int cnt = 0; static void cleanup_handler(void *arg) { printf("Called clean-up handler\n"); cnt = 0; } static void * thread_start(void *arg) { time_t start, curr; printf("New thread started\n"); pthread_cleanup_push(cleanup_handler, NULL); curr = start = time(NULL); while (!done) { pthread_testcancel(); /* A cancellation point */ if (curr < time(NULL)) { curr = time(NULL); printf("cnt = %d\n", cnt); /* A cancellation point */ cnt++; } } pthread_cleanup_pop(cleanup_pop_arg); return NULL; } int main(int argc, char *argv[]) { pthread_t thr; int s; void *res; s = pthread_create(&thr, NULL, thread_start, NULL); if (s != 0) handle_error_en(s, "pthread_create"); sleep(2); /* Allow new thread to run a while */ if (argc > 1) { if (argc > 2) cleanup_pop_arg = atoi(argv[2]); done = 1; } else { printf("Canceling thread\n"); s = pthread_cancel(thr); if (s != 0) handle_error_en(s, "pthread_cancel"); } s = pthread_join(thr, &res); if (s != 0) handle_error_en(s, "pthread_join"); if (res == PTHREAD_CANCELED) printf("Thread was canceled; cnt = %d\n", cnt); else printf("Thread terminated normally; cnt = %d\n", cnt); exit(EXIT_SUCCESS); }示例代码跟踪
[root@localhost CFunctionTest]# ./thread New thread started cnt = 0 cnt = 1 Canceling thread Called clean-up handler Thread was canceled; cnt = 0 [root@localhost CFunctionTest]# gdb thread (gdb) b 18 //... (gdb) r Starting program: /home/***/a***/CFunctionTest/thread [Thread debugging using libthread_db enabled] Breakpoint 6, main (argc=1, argv=0xbffff374) at testthread.cpp:54 54 s = pthread_create(&thr, NULL, thread_start, NULL); Missing separate debuginfos, use: debuginfo-install glibc-2.12.90-17.i686 libgcc-4.5.1-4.fc14.i686 libstdc++-4.5.1-4.fc14.i686 (gdb) n [New Thread 0xb7fecb70 (LWP 4845)] New thread started [Switching to Thread 0xb7fecb70 (LWP 4845)] Breakpoint 2, thread_start (arg=0x0) at testthread.cpp:29 29 pthread_cleanup_push(cleanup_handler, NULL); (gdb) n 31 curr = start = time(NULL); (gdb) n 33 while (!done) { (gdb) n Breakpoint 3, thread_start (arg=0x0) at testthread.cpp:34 34 pthread_testcancel(); /* A cancellation point */ (gdb) n Canceling thread [Switching to Thread 0xb7fee6d0 (LWP 4840)] Breakpoint 7, main (argc=1, argv=0xbffff374) at testthread.cpp:70 70 s = pthread_cancel(thr); (gdb) n cnt = 0 [Switching to Thread 0xb7fecb70 (LWP 4845)] Breakpoint 4, thread_start (arg=0x0) at testthread.cpp:39 39 cnt++; (gdb) n 33 while (!done) { (gdb) n Breakpoint 3, thread_start (arg=0x0) at testthread.cpp:34 34 pthread_testcancel(); /* A cancellation point */ (gdb) n 35 if (curr < time(NULL)) (gdb) n 37 curr = time(NULL); (gdb) n 38 printf("cnt = %d\n", cnt); /* A cancellation point */ (gdb) n cnt = 1 [Switching to Thread 0xb7fee6d0 (LWP 4840)] Breakpoint 8, main (argc=1, argv=0xbffff374) at testthread.cpp:75 75 s = pthread_join(thr, &res); (gdb) n [Switching to Thread 0xb7fecb70 (LWP 4845)] Breakpoint 4, thread_start (arg=0x0) at testthread.cpp:39 39 cnt++; (gdb) n 33 while (!done) { (gdb) n Breakpoint 3, thread_start (arg=0x0) at testthread.cpp:34 34 pthread_testcancel(); /* A cancellation point */ (gdb) n Breakpoint 1, cleanup_handler (arg=0x0) at testthread.cpp:18 18 printf("Called clean-up handler\n"); (gdb) n Called clean-up handler 19 cnt = 0; (gdb) n 20 } (gdb) n参考:
【1】 http://blog.csdn.net/yanook/article/details/6589798
【2】 http://blog.csdn.net/yulanarti/article/details/6197769
【3】 http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_cleanup_push.3.html
1、在Linux中,线程一般被认为是“轻量级的进程”。
Linux 创建进程所使用的函数是fork()或者vfork()。而对线程的创建和管理Linux可以使用POSIX的线程库pthreads提供的APIs。
2、使用fork()创建进程和使用POSIX线程库差别:
使用fork() 创建进程的特点:
① 代价昂贵,通常子进程需要拷贝父进程的整个上下文,比如数据等。
② 进程间的通信方式比较复杂,比如使用管道、消息、共享内存等方法。
③ 操作系统在实现进程间的切换比线程切换更费时。
使用POSIX pthreads库创建线程的特点:
① 线程可使用存在于进程中的资源,因此创建进程比创建线程更快。
② 线程间的通信方式更容易,比如通过进程中的变量,可以让多个线程共享数据。
③ 操作系统对线程的切换比对进程的切换更容易和快速。
3、线程由内核自动调度,每个线程都有它自己的线程上下文(thread context),包括一个惟一的整数线程ID(Thread ID,TID),栈,栈指针,程序计数器,通用目的寄存器和条件码。每个线程和其他线程一起共享进程上下文的剩余部分,包括整个用户的虚拟地址空间,它是由只读文本(代码),读/写数据,堆以及所有的共享库代码和数据区域组成的,还有,线程也共享同样的打开文件的集合。
4、常见IPC有:管道,FIFO,共享存储器,信号。
5、线程间无需特别的手段进行通信,因为线程间可以共享数据结构,注意的是线程间需要做好同步。linux的消息属于IPC,也就是进程间通信,线程用不上。
pthread_self() 的返回值就是当前线程的标志。
示例代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #define THREAD_NUMBER 2 int retval_hello1= 2, retval_hello2 = 3; void* hello1(void *arg) { char *hello_str = (char *)arg; sleep(1); printf("%s\n", hello_str); pthread_exit(&retval_hello1); } void* hello2(void *arg) { char *hello_str = (char *)arg; sleep(2); printf("%s\n", hello_str); pthread_exit(&retval_hello2); } int main(int argc, char *argv[]) { int i; int ret_val; int *retval_hello[2]; pthread_t pt[THREAD_NUMBER]; const char *arg[THREAD_NUMBER]; arg[0] = "hello world from thread1"; arg[1] = "hello world from thread2"; printf("Begin to create threads...\n"); ret_val = pthread_create(&pt[0], NULL, hello1, (void *)arg[0]); if (ret_val != 0 ) { printf("pthread_create error!\n"); exit(1); } ret_val = pthread_create(&pt[1], NULL, hello2, (void *)arg[1]); if (ret_val != 0 ) { printf("pthread_create error!\n"); exit(1); } printf("Now, the main thread returns.\n"); printf("Begin to wait for threads...\n"); for(i = 0; i < THREAD_NUMBER; i++) { ret_val = pthread_join(pt[i], (void **)&retval_hello[i]); if (ret_val != 0) { printf("pthread_join error!\n"); exit(1); } else { printf("return value is %d\n", *retval_hello[i]); } } return 0; }5、属性设置
属性名 | 意义 |
detachstate | 选择被创建的线程是处于可加入的状态还是分离状态。可加入状态值是PTHREAD_CREATE_JOINABLE;分离状态值是PTHREAD_CREATE_DETACHED。缺省状态值是PTHREAD_CREATE_JOINABLE。 pthread_attr_setdetachstate() 可设置线程为加入或者分离状态; pthread_attr_getdetachstate() 可以获得当前线程是否是加入的或者是分离的状态。 |
schedpolicy | 为被创建的线程选择调度策略。被创建的线程的状态可以是SCHED_OTHER(一般的,非实时调度)、SCHED_RR (实时,轮转调度) 或者SCHED_FIFO(实时,先进先出调度)。缺省值是SCHED_OTHER。实时调度SCHED_RR 和 SCHED_FIFO 只能用于有超级用户权限的进程使用。 pthread_attr_setschedpolicy() 和 pthread_attr_getschedpolicy() 函数可以设置和获得线程的调度属性。 |
schedparam | 为被创建的线程选择调度参数。这里的调度参数指的是线程的调度优先级。缺省优先级是 0 。这个属性对于 SCHED_OTHER 是不重要的;它只对SCHED_RR 和 SCHED_FIFO 两个和实时调度相关的调度方式有效。 pthread_attr_setschedparam() 和 pthread_attr_getschedparam() 两个函数可以分别对线程的优先级进行设置和获取。 |
inheritsched | 选择对新创建的线程的调度策略和调度参数是否被schedpolicy 和schedparam 属性决定(这时的值是PTHREAD_EXPLICIT_SCHED)或者是通过父线程继承而得到的(这时的值是PTHREAD_INHERIT_SCHED)。缺省的值是PTHREAD_EXPLICIT_SCHED。 |
scope | 为选择被创建的线程调度竞争范围。缺省值是PTHREAD_SCOPE_SYSTEM,表示线程和系统的所有的其他运行在CPU上的进程争夺CPU 资源。如果是PTHREAD_SCOPE_PROCESS,表示调度的竞争只发生在运行于同一进程空间的线程之间,线程的优先级只在同一进程空间的线程之间有效,和其他进程无关。 |
6、线程互斥锁的特性
① 原子性。对mutex的加锁和解锁操作是原子的,一个线程进行 mutex 操作的过程中,其他线程不能对同一个 mutex 进行其他操作。
② 单一性。拥有mutex的线程除非释放mutex,否则其他线程不能拥有此mutex。
③ 非忙等待。等待mutex的线程处于等待状态,直到要等待的mutex处于未加锁状态,这时操作系统负责唤醒等待此mutex的线程。
7、条件变量
条件标量是线程的同步设备。在线程间使用条件变量可以使得一个线程在执行过程中,因满足某个条件而发出信号通知另一个线程;而另一个线程可以处于挂起状态,等待某个条件的满足后,才继续执行。
条件变量必须和mutex一起使用来避免竞争情况。
8、一个线程可以通过向另个线程发送“请求”来结束另一个线程的执行。
POSIX pthreads库中关于撤销操作的函数有:
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void);
在撤销线程的时候,可以编写程序让线程进一步进行所谓的“清理”工作,比如已经拥有了某个 mutex,在清理例程中可以释放这个 mutex;如果动态分配了内存,那么可以在清理例程中释放动态分配的内存。
9、信号
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask);
用来改变或者设置线程的信号屏蔽(signal mask),newmask 用来执行新的信号屏蔽,设置新信号屏蔽以前的信号屏蔽被存放到 oldmask 指向的位置。
int pthread_kill(pthread_t thread, int signo);
可以向其他线程发送信号。
int sigwait(const sigset_t *set, int *sig);
挂起调用sigwait() 的线程,直到收到第一个参数 set 指向的信号集中指定的信号,且等待到信号被存放到第二个参数 sig 指向的位置。
10、常用函数整理
1) 创建
int pthread_create(
pthread_t* thread, //返回指向线程标识符的指针
pthread_attr* attr, //设置线程属性
void *(*func)(void*), //新线程将启动的函数的地址
void* arg //传递给新线程启动函数的参数
);
//函数执行成功返回0,失败返回错误码。执行成功后,新线程将从设定的函数处开始执行,原线程则继续执行。
2)终止
void pthread_exit(void* ret)
//终止调用此函数的线程,并返回一个指向某对象的指针(ret),可通过pthread_join来获取此返回值。
//注意,不能返回指向局部变量的指针。
3)合并线程:pthread_join
int pthread_join(pthread_t* thread, void **ret);
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。关于什么是可joinable的线程,参见[3]。
4)通过到斥实现线程同步
(1)声明互斥锁变量:pthread_mutext_t mutex; pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。
(2)互斥锁初始化:pthread_mutex_init(pthread_mutex_t *mymutex, const pthread_mutexattr_t *attr );
第一个参数接受一个指针以初始化为互斥对象,该指针指向一块已分配好的内存区。第二个参数,可以接受一个可选的pthread_mutexattr_t指针。这个结构可用来设置各种互斥对象属性。
(3)锁定互斥锁 :pthread_mutex_lock (&mutex);
pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,同一时间只能被一个线程调用执行。
(4)打开互斥锁:pthread_mutex_unlock(&mutex);
示例代码
#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> int allTickets= 10; //假设总共还剩余十张票 pthread_mutex_t mutex; //声明互斥锁 void* window1(void* arg) { while(allTickets >= 1) { pthread_mutex_lock(&mutex); //加锁 if(allTickets >= 1) { printf("I am window1:%d\n",allTickets); allTickets--; } pthread_mutex_unlock(&mutex); //对共享资源的访问完后解锁 sleep(1); } } void* window2(void* arg) { while(allTickets>=1) { pthread_mutex_lock(&mutex); if(allTickets>=1) { printf("I am window2:%d\n",allTickets); allTickets--; } pthread_mutex_unlock(&mutex); sleep(2); } } int main(void) { pthread_t sell1,sell2; pthread_mutex_init (&mutex,NULL); pthread_create(&sell1, NULL, window1, NULL); pthread_create(&sell2, NULL, window2, NULL); pthread_join(sell1,NULL); pthread_join(sell2,NULL); return 1; }
(5)其他
互斥锁判断上锁:pthread_mutex_trylock()
消除互斥锁:pthread_mutex_destroy()
5)通过信号量实现线程同步
(1)基本概念
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post( )增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait( )减少信号量。
和信号量有关的一些函数,它们都在头文件 semaphore.h中定义。信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init( )用来初始化一个信号量。它的原型为:
extern int sem_init _P ((sem_t *sem, int pshared, unsigned int value));
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不再阻塞,选择机制同样是由线程的调度策略决定的。
函数sem_wait( sem_t *sem )被用来阻塞当前线程,直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
示例生产者与消费者
#include <stdio.h> #include <pthread.h> #include <semaphore.h> #include <stdlib.h> #include <unistd.h> #define N 20 int k = 0; //保存现有产品数量 sem_t full; sem_t empty; pthread_mutex_t mutex; void* producer(void* arg) { while(1) { sem_wait(&empty); pthread_mutex_lock(&mutex); k++; printf(" produce one Remaining:%d\n", k); pthread_mutex_unlock(&mutex); sem_post(&full); sleep(1); } } void* consumer(void* arg) { while(1) { sem_wait(&full); pthread_mutex_lock(&mutex); k--; printf(" consumer one the rest :%d\n",k); pthread_mutex_unlock(&mutex); sem_post(&empty); sleep(1); } } main() { pthread_t thread1, thread2, thread3; sem_init(&full,0,0); sem_init(&empty,0,N); pthread_create(&thread1, NULL, producer, NULL); pthread_create(&thread2, NULL, producer, NULL); pthread_create(&thread3, NULL, consumer, NULL); pthread_join(thread1,NULL); pthread_join(thread2,NULL); pthread_join(thread3,NULL); }注意:要用两个信号量来标示缓冲的大小,这是实现P_C模型的巧妙之处。
(2)图示
(3)函数
sem_init()用于创建一个信号量,并初始化它的值。
sem_wait()和sem_trywait()都相当于P操作,在信号量大于零时它们都能将信号量的值减一,两者的区别在于若信号量小于零时,sem_wait()将会阻塞进程,而sem_trywait()则会立即返回。
sem_post()相当于V操作,它将信号量的值加一同时发出信号来唤醒等待的进程。
sem_getvalue()用于得到信号量的值。
sem_destroy()用于删除信号量。
参考:
【1】 http://www.cnblogs.com/mydomain/archive/2011/07/10/2102147.html
【2】 http://blog.csdn.net/lanmoshui963/article/details/2176376
【3】 http://www.cnblogs.com/mydomain/archive/2011/08/14/2138454.html
【4】 华中科技大学,中山大学PPT