分类: C/C++
2012-09-17 13:17:49
1. 引言
我在编写程序的时候,我喜欢多进程编程,而不是多线程。原因是各个进程完全独立,管理起来非常方
便。但是在很多情况下,尤其是嵌入式系统中,线程占用的系统资源比进程少得多。有文章说,一个进程
的系统开销大概是一个线程的30倍。所以,熟悉多线程的设计还是非常有必要的。
2.线程的基本操作
2.1 一个简单的多线程编程
Linux下的多线程遵循POSIX线程接口,称为pThread。编写Linux下的多线程程序,需要使用头文件
pthread.h,连接时需要使用库libpthread.a。
#include
#include
void thread1(void)
{
int i=0;
for(i=0;i<3;i++)
{
printf("This is a pthread1.n");
sleep(1);
}
}
void thread2(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread2.n");
pthread_exit(0);
}
int main(void)
{
pthread_t id1,id2;
int i,ret;
ret=pthread_create(&id1,NULL,(void *) thread1,NULL);
if(ret!=0)
{
printf ("Create pthread error!n");
exit (1);
}
ret=pthread_create(&id2,NULL,(void *) thread2,NULL);
if(ret!=0)
{
printf ("Create pthread error!n");
exit (1);
}
pthread_join(id1,NULL);
pthread_join(id2,NULL);
exit (0);
}
编译 gcc -o thread thread.c -lpthread
运行结果:
This is a pthread1.
This is a pthread2.
This is a pthread2.
This is a pthread2.
This is a pthread1.
This is a pthread1.
2.2 线程的创建和退出
在上面的例子中,我们使用函数pthread_create创建线程。该函数说明如下:
int pthread_create(pthread_t *restrict thread,const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。在前面的例子中,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。
在上面的例子中,线程1运行结束之后,会自动退出。这是一种线程退出的常用方法,另外一种方法是线程2采用的方法,调用pthread_exit进行退出。这两种方法都可以,但是不能使用exit,这个函数是退出整个进程。
pthread_join的作用是等待线程返回。
int pthread_join(pthread_t thread, void **value_ptr);
第一个参数为指向线程标识符的指针,第二个参数是存储线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
2.3 线程的属性
在pthread_create函数中,第二个参数是线程的属性。在例子中,我们使用了默认参数,即将该函数的第二个参数设为NULL。对大多数程序来说,使用默认属性就够了。但在某些情况下,我们需要设置线程的属性。
属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
2.3.1 绑定
Linux系统采用“一对一”的线程机制,也就是一个用户线程对应一个内核线程。绑定属性就是指一个用户线程固定地分配给一个内核线程,因为 CPU 时间片的调度是面向内核线程(也就是轻量级进程)的,因此具有绑定属性的线程可以保证在需要的时候总有一个内核线程与之对应。而与之相对的非绑定属性就是指用户线程和内核线程的关系不是始终固定的,而是由系统来控制分配的。
设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。
#include
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);
2.3.2 分离
分离属性是用来决定一个线程以什么样的方式来终止自己。在非分离情况下,当一个线程结束时,它所占用的系统资源并没有被释放,也就是没有真正的终止。只有当 pthread_join()函数返回时,创建的线程才能释放自己占用的系统资源。而在分离属性情况下,一个线程结束时立即释放它所占有的系统资源。这里要注意的一点是,如果设置一个线程的分离属性,而这个线程运行又非常快,那么它很可能在 pthread_create 函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这时调用 pthread_create 的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
2.3.3 优先级
另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。
#include
#include
pthread_attr_t attr;
pthread_t tid;
sched_param param;//线程优先级数据结构
int newprio=20; //准备设置的线程优先级
pthread_attr_init(&attr); //设置任何线程属性之前,必须初始化
pthread_attr_getschedparam(&attr, ¶m); //取得默认优先级
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m); //设置优先级
pthread_create(&tid, &attr, (void *)myfunction, myarg);
除了线程优先级,在某些场合,还会配合线程调度策略一起进行使用。
函数pthread_attr_setschedpolicy和pthread_attr_getschedpolicy分别用来设置和得到线程的调度策略。
int pthread_attr_setschedpolicy(pthread_attr_t *attr,int policy);
policy既是调度策略。调度策略可能的值是先进先出(SCHED_FIFO)、轮转法(SCHED_RR),或其它(SCHED_OTHER)。
SCHED_FIFO策略允许一个线程运行直到有更高优先级的线程准备好,或者直到它自愿阻塞自己。
在SCHED_RR策略下,如果有一个SCHED_RR策略的线程执行了超过一个固定的时期(时间片间隔)没有阻塞,而另外的SCHED_RR或SCHBD_FIPO策略的相同优先级的线程准备好时,运行的线程将被抢占以便准备好的线程可以执行。
2.4 线程控制
由于线程共享进程的资源和地址空间,因此在对这些资源进行操作时,必须考虑到线程间资源访问的惟一性问题,这里主要介绍 POSIX 中线程同步的方法,主要有互斥锁和信号量的方式。
2.4.1 互斥锁
mutex 是一种简单的加锁的方法来控制对共享资源的存取。这个互斥锁只有两种状态,也就是上锁和解锁,可以把互斥锁看作某种意义上的全局变量。在同一时刻只能有一个线程掌握某个互斥上的锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一个已经上锁的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。可以说,这把互斥锁使得共享资源按序在各个线程中操作。
互斥锁的操作主要包括以下几个步骤。
· 互斥锁初始化:pthread_mutex_init
· 互斥锁上锁:pthread_mutex_lock
· 互斥锁判断上锁:pthread_mutex_trylock(可以理解为pthread_mutex_lock的非阻塞版本)
· 互斥锁接锁:pthread_mutex_unlock
· 消除互斥锁:pthread_mutex_destroy
其中,互斥锁分为三种,快速互斥锁(PTHREAD_MUTEX_INITIALIZER)、递归互斥锁(PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP)和检错互斥锁(PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP)。这三种锁的区别主要在于其他未占有互斥锁的线程在希望得到互斥锁时的是否需要阻塞等待。快速锁是指调用线程会阻塞直至拥有互斥锁的线程解锁为止。递归互斥锁能够成功地返回并且增加调用线程在互斥上加锁的次数,而检错互斥锁则为快速互斥锁的非阻塞版本,它会立即返回并返回一个错误信息。
pthread_mutex_init函数原型
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
mutex:互斥锁
mutexattr:PTHREAD_MUTEX_INITIALIZER:创建快速互斥锁
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP:创建递归互斥锁
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP:创建检错互斥锁
下面的实例使用互斥锁来实现对变量 lock_var 的加一、打印操作,线程1负责对lock_var加一,线程2负责打印。
#include
#include
#include
#include
#include
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //快速互斥锁
int lock_var; //模拟待保护数据
time_t end_time; //模拟过程终止时间
void pthread1(void *arg);
void pthread2(void *arg);
int main(int argc, char *argv[])
{
pthread_t id1,id2;
pthread_t mon_th_id;
int ret;
end_time = time(NULL)+10;
pthread_mutex_init(&mutex,NULL);//初始化互斥量,如果第二个参数为NULL,则生成快速互斥锁
ret=pthread_create(&id1,NULL,(void *)pthread1, NULL);
if(ret!=0)
perror("pthread1 create failed.");
ret=pthread_create(&id2,NULL,(void *)pthread2, NULL);
if(ret!=0)
perror("pthread2 create failed.");
pthread_join(id1,NULL);
pthread_join(id2,NULL);
exit(0);
}
void pthread1(void *arg)
{
int i;
while(time(NULL) < end_time)
{
if(pthread_mutex_lock(&mutex)!=0)
{
perror("pthread_mutex_lock");
}
else
printf("pthread1:pthread1 lock the variablen");
for(i=0;i<2;i++)
{
sleep(1);
lock_var++;
}
if(pthread_mutex_unlock(&mutex)!=0)
{
perror("pthread_mutex_unlock");
}
else
printf("pthread1:pthread1 unlock the variablen");
sleep(1);
}
}
void pthread2(void *arg)
{
int nolock=0;
int ret;
while(time(NULL) < end_time)
{
ret=pthread_mutex_trylock(&mutex);
// 如果互斥锁已经被锁定,与pthread_mutex_lock函数不同,pthread_mutex_trylock立刻返回。
if(ret==EBUSY)
printf("pthread2:the variable is locked by pthread1n");
else
{
if(ret!=0)
{
perror("pthread_mutex_trylock error.");
exit(1);
}
else
printf("pthread2:pthread2 got lock.The variable is %dn",lock_var);
if(pthread_mutex_unlock(&mutex)!=0)
{
perror("pthread_mutex_unlock");
}
else
printf("pthread2:pthread2 unlock the variablen");
}//end of if(ret==EBUSY)
sleep(3);
}//end of while
}
2.4.2 信号量
信号量也就是操作系统中所用到的 PV 原语,它广泛用于进程或线程间的同步与互斥,这里提请大家注意,在Linux下,信号量只能用于线程通信。信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
我们再来简单复习下PV 原语的工作原理。
PV 原语是对整数计数器信号量 sem 的操作。一次 P 操作使 sem 减一,而一次 V 操作使sem 加一。进程(或线程)根据信号量的值来判断是否对公共资源具有访问权限。当信号量sem 的值大于零时,该进程(或线程)具有公共资源的访问权限;相反,当信号量 sem的值小于或等于零时,该进程(或线程)就将阻塞直到信号量 sem 的值大于 0 为止。
PV原语主要用于进程或线程间的同步和互斥这两种典型情况。若用于互斥,几个进程(或线程)往往只设置一个信号量 sem。当信号量用于同步操作时,往往会设置多个信号量,并安排不同的初始值来实现它们之间的顺序执行。
Linux 实现了 POSIX 的无名信号量,主要用于线程间的互斥同步。这里主要介绍几个常见函数。
• sem_init 用于创建一个信号量,并能初始化它的值。
• sem_wait 和 sem_trywait 相当于 P 操作,它们都能将信号量的值减一,两者的区别在
于若信号量小于零时,sem_wait 将会阻塞进程,而 sem_trywait 则会立即返回。
• sem_post 相当于 V 操作,它将信号量的值加一同时发出信号唤醒等待的进程。
• sem_getvalue 用于得到信号量的值。
• sem_destroy 用于删除信号量。
我们用信号量来实现上节中互斥量的例子。
#include
#include
#include
#include
#include
#include
#include
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //快速互斥锁
int lock_var; //模拟待保护数据
time_t end_time; //模拟过程终止时间
sem_t sem; //用作互斥锁的信号量,新增加
void pthread1(void *arg);
void pthread2(void *arg);
int main(int argc, char *argv[])
{
pthread_t id1,id2;
pthread_t mon_th_id;
int ret;
end_time = time(NULL)+10;
//pthread_mutex_init(&mutex,NULL);
//初始化信号量,初始值设定为1。
//sem_init原型: int sem_init(sem_t *sem, int pshared, unsigned value);
ret=sem_init(&sem,0,1);
ret=pthread_create(&id1,NULL,(void *)pthread1, NULL);
if(ret!=0)
perror("pthread1 create failed.");
ret=pthread_create(&id2,NULL,(void *)pthread2, NULL);
if(ret!=0)
perror("pthread2 create failed.");
pthread_join(id1,NULL);
pthread_join(id2,NULL);
exit(0);
}
void pthread1(void *arg)
{
int i;
while(time(NULL) < end_time)
{
//锁定互斥信号量。sem减一,p操作。
sem_wait(&sem);
for(i=0;i<2;i++)
{
sleep(1);
lock_var++;
printf("lock_var=%dn",lock_var);
}
//释放互斥信号量,sem加一,V操作
sem_post(&sem);
sleep(1);
}
}
void pthread2(void *arg)
{
int nolock=0;
int ret;
while(time(NULL) < end_time)
{
//ret=pthread_mutex_trylock(&mutex);
sem_wait(&sem);
printf("pthread2:pthread2 got lock;lock_var=%dn",lock_var);
sem_post(&sem);
sleep(3);
}//end of while
}
运行结果如下:
lock_var=1
lock_var=2
pthread2:pthread2 got lock;lock_var=2
lock_var=3
lock_var=4
pthread2:pthread2 got lock;lock_var=4
lock_var=5
lock_var=6
pthread2:pthread2 got lock;lock_var=6
lock_var=7
lock_var=8
信号量也可以实现两个线程的同步。比如用线程1控制线程2中的信号量,当线程1结束后,再释放线程2的信号量,可以保证线程1运行结束后再运行线程2.
2.4.3 线程数据
在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。
创建键的函数原型为:
extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这个函数,以后的调用将被它忽略。
在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。
pthread_key_t myWinKey;
void createWindow ( void )
{
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
pthread_once ( & once, createMyKey) ;
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
setWindow(win);
pthread_setpecific ( myWinKey, win);
}
void createMyKey ( void )
{
pthread_keycreate(&myWinKey, freeWinKey);
}
void freeWinKey ( Fl_Window * win)
{
delete win;
}
这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。
2.4.4 条件变量
看了些资料,有点复杂,主要功能是完成线程同步,而且还要配合互斥量,感觉还是信号量来的简单些。这里就不介绍了。