线程控制
线程控制:
线程属性:
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
返回值:成功0,失败,错误编号
让线程已开始就处于分离状态,可以调用pthread_attr_setdetachstate函数把线程属性detachstate
设置为以下两个合法值之一:PTHREAD_CREATE_DETACHED,以分离状态启动线程;
或者PTHREAD_CREATE_JOINABLE,正常启动线程,应用程序可以获取线程的终止状态
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr,int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate);
返回值:成功0,失败,错误编号
一个以分离状态创建线程的函数:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
int makethread(void *(*fn)(void *),void *arg)
{
int err;
pthread_t tid;
pthread_attr_t attr;
err=pthread_attr_init(&attr);
if(err!=0)
return(err);
err=pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if(err==0)
err=pthread_create(&tid,&attr,fn,arg);
pthread_attr_destroy(&attr);
return(err);
}
可以在编译阶段使用_POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE符号
来检查系统是否支持每一个线程栈属性。如果定义了,就说明它支持相应的线程栈属性。也可以把参数传递
给sysconf函数,检查运行时系统对线程栈属性的支持情况。
可以使用函数pthread_attr_getstack和pthread_attr_setstack对线程栈属性进行管理:
#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t *restrict attr,
void **restrict stackaddr,
size_t *restrict stacksize);
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize);
返回值:成功,0,否则,错误编号
stackaddr线程属性被定义为栈的最低内存地址,但这并不一定是栈的开始位置。
可以通过pthread_attr_getstacksize和pthread_attr_setstacksize函数读取或者设置线程属性stacksize:
#include <pthread.h>
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,size_t *restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);
返回值:成功0,失败,返回错误编号
设置stacksize属性时,选择的stacksize不能小于PTHREAD_STACK_MIN.
线程属性guardsize控制着线程末尾之后用以避免栈溢出的扩展内存的大小。这个属性默认值是由具体实现来定义的,但
常用值是系统页大小。可以把guardsize线程属性设置为0,不允许属性的这种特征行为发生:在这种情况下,不会提供
警戒缓冲区。同样,如果修改了线程属性stackaddr,系统就认为我们将自己管理栈,进而是栈警戒缓冲区机制无效,这
等同于把guardsize线程属性设置为0.
#include <pthread.h>
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize);
返回值:成功0,失败,返回错误编号
互斥量属性:
对于非默认属性,可以用pthread_mutexattr_init初始化pthread_mutexattr_t结构,用pthread_mutexattr_destroy来反初始化
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
返回值:成功0,失败,返回错误编号
3个属性是:进程共享属性、健壮属性以及类型属性。
进行共享属性是可选的:检查系统是否定义了_POSIX_THREAD_PROCESS_SHARED符号来判断这个平台是否支持进程共享这个属性,也可以把
_SC_THREAD_PROCESS_SHARED参数传给sysconf函数进行检查.
可以使用pthread_mutexattr_getshared函数查询pthread_mutexattr_t结构,得到它的进程共享属性,使用
pthread_mutexattr_setshared函数修改进程共享属性:
#include <pthread.h>
int pthread_mutexattr_getshared(const pthread_mutexattr_t *restrict attr,int *restrict pshared);
int pthread_mutexattr_setshared(pthread_mutexattr_t *attr,int pshared);
返回值:成功0,失败,返回错误编号
使用pthread_mutexattr_getrobust函数获取健壮的互斥量的值。
#include <pthread.h>
int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr,int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr,int robust);
返回值:成功0,失败,返回错误编号
默认值是PTHREAD_MUTEX_STALLED,这意味着持有互斥量的进程终止时不需要采取特别的动作。
另外一个值时PTHREAD_MUTEX_ROBUST,这个值将导致线程调用pthread_mutex_lock获取锁,而该锁被另一个进程持有,但它终止时并没有
对该锁进行解锁,此是进程会阻塞,从pthread_mutex_lock返回的值为ROWNERDEAD而不是0.
调用pthread_mutex_consistent函数,指明与该互斥量相关的状态互斥量解锁之前时一致的。
#include <pthread.h>
int pthread_mutex_consistent(pthread_mutex_t *mutex);
返回值:成功0,失败,返回错误编号
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr,int type);
返回值:成功0,失败,返回错误编号
读写锁属性:
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
返回值:成功0,失败,返回错误编号
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr,int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockatrr_t *attr,int pshared);
返回值:成功0,失败,返回错误编号
条件变量属性:
进程共享属性和时钟属性。
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_conattr_t *attr);
返回值:成功0,失败,返回错误编号
#include <pthread.h>
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared);
返回值:成功0,失败,返回错误编号
#include <pthread.h>
int pthread_condattr_getclock(const pthread_condattr_t *restrict attr,clockid_t *restrict clock_id);;
int pthread_condattr_setclock(pthread_condattr_t *attr,clokid_t clock_id);
返回值:成功0,失败,返回错误编号
屏障属性:
#include <pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
返回值:成功0,失败,返回错误编号
#include <pthread.h>
int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr,int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr,int pshared);
返回值:成功0,失败,返回错误编号
进程共享属性的值可以是PTHREAD_PROCESS_SHARED(多进程中的多个线程可用),
可以时PTHREAD_PROCESS_PRIVETE(只有初始化屏障的那个进程内的多个线程可用)。
重入:
POSIX.1还提供了以线程安全的方式管理FILE对象的方法。可以使用glockfile和ftrylockfile获取给定FILE对象关联的锁。
这个锁时递归的:当你占有这把锁的时候,还是可以再次获取该锁,而且不会导致死锁。虽然这种锁的具体实现并不规定,但要求所有
操作FILE对象的标准I/O例程的动作行为必须看起来就像它们内部调用了flockfile和funlockfile。
#include <stdio.h>
int ftrylockfile(FILE *fp);
返回值:成功,0,失败,返回非0数值
void flockfile(FILE *fp);
void funlockfile(FILE *fp);
如果标准的I/O例程都获取它们各自的锁,那么在做一次一个字符的I/O时就会出现严重的性能下降。在这种情况下,需要对每一个字符
的读写操作进行获取锁和释放锁的动作。为了避免这种开销,出现了不加锁版本的基于字符的标准I/O例程。
#include <stdio.h>
int getchar_unlocked(void);
int getc_unlocked(FILE *fp);
返回值:成功,返回下一个字符,若遇到文加尾或者出错,返回EOF
int putchar_unlocked(int c);
int putchar_unlocked(int c,FILE *fp);
返回值:成功,返回c,失败,返回EOF
线程特定数据:
1、有时候需要维护基于每个线程的数据。因为线程ID并不能保证是小而连续的整数,所以就不能简单地分配一个每线程数据数组,用线程
ID作为数组的索引。即使线程ID确实是小而连续的整数,我们还希望有一些额外的保护,防止某个线程的数据与其他线程的数据混淆。
2、它提供了基于进程的接口适应多线程环境的机制。一个很明显的实例就是errno。以前的接口(线程出现以前)把errno定义为进程
上下文中全局可访问的整数。系统调用和库例程在调用或执行失败是设置errno,把它作为操作失败时的附属结果。为了让线程也能够
使用那些原本基于进程的系统调用和库例程,errno被重新定义为线程私有数据。这样,一个线程做了重置errno的操作也不会影响进程
中其他线程的errno值。
在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问。使用pthread_key_create创建一个
键:
#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void *));
返回值:成功0,失败,错误编号
创建的键储存在keyp指定的内存单元内,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程特定数据地址进行
关联。创建新键时,每个进程的数据地址设为空值。
它可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经置为非空,那么析构函数就会调用,它的唯一参数
时该数据地址。如果传入的析构函数为空,就表明没有析构函数与这个键关联。当线程调用pthread_exit或者线程执行返回。正常
退出,析构函数就会被调用。如果线程调用了exit、_exit、_Exit或者abort,或者出现其他非正常的退出时,就不会调用析构函数。
我们可以通过调用pthread_key_delete来取消键与线程特定数据值之间的关联关系。
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
返回值:成功0,失败,错误编号。
解决系统的线程之间的竞争调用pthread_once:
#include <pthread.h>
pthread_once_t initflag=PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag,void (*initfn)(void));
返回值:成功0,失败,错误编号
initflag必须是一个非本地变量,而且初始化必须时PTHREAD_ONCE_INIT。
键一旦创建成功,就可以通过pthread_setspecific函数把键和线程特定数据关联起来,
可以通过pthread_getspecific函数获得线程特定数据的地址:
#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
返回值:线程特定数据值,若没有值与该键关联,返回NULL
int pthread_setspecific(pthread_key_t key,const void *value);
返回值:成功0,失败,错误编号
取消选项:
有两个线程属性并没有包含在pthread_attr_t结构中,它们是可取消状态和可取消类型:
着两个属性影响这线程在响应pthread_cancle函数时所呈现的行为。
可取消状态属性可以是PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCEL_DISABLE。
线程可以通过调用pthread_setcancelstate修改它的可取消状态:
#include <pthread.h>
int pthread_setcancelstate(int state,int *oldstate);
返回值:成功,0,失败,返回错误编号。
ptread_setcancelstate把当前的可取消状态设置state,把原来的可取消请求存储在由oldstate
指向的内存单元。
调用pthread_testcancel函数在程序中添加自己的取消点:
#include <pthread.h>
void pthread_testcancel(void);
我们所描述的默认的取消类型也称为i推迟取消。调用pthread_cancel后,在线程到达取消点之前,并不会
真正的取消。可以通过pthread_setcanceltype来修改取消类型。
#include <pthread.h>
int pthread_setcanceltype(int type,int *oldtype);
返回值:成功0,失败,返回错误编号。
pthread_setcanceltype函数把取消类型设置为type(类型参数可以是PTHREADCANCEL_DEFERRED,//拖延,延缓,推迟
也可以是PTHREAD_CANCEL_ASYNCHRONOUS//异步的),把原来的取消类型返回到oldtype指向的整型单元。
异步取消和推迟取消不同,因为使用异步取消时,线程可以在任意时间撤销,不是非得遇到取消点才能被取消。
线程和信号:
进程使用sigprocmask函数来阻止信号发送。然而,sigprocmask的行为在多线程的进程中并没有定义,线程必须使用:
#include <signal.h>
int pthread_sigmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);
成功0,失败,错误编号
pthread_sigmask工作在线程中,而且失败时返回错误码,不再像sigprocmask中那样设置errno并返回-1.
set参数包含线程用于信号屏蔽字的信号集
how参数可以取下列值之一:SIG_BLOCK,把信号集添加到线程信号屏蔽字中
SIG_SETMASK,用信号集替换线程的信号屏蔽字
SIG_UNBLOCK,从线程信号屏蔽字中移除信号集
oset参数不为空,线程之前的信号屏蔽字就存储在它指向的sigset_t结构中
线程可以通过把set参数设置为NULL,并把oset参数设置为sigset_t结构的地址,来获取当前的信号屏蔽字。这种情况下how就会
被忽略。
线程可以通过调用sigwait等待一个或者多个信号的信号:
#include <signal.h>
int sigwait(const sigset_t *restrict set,int *restrict signop);
返回值:成功0,失败,错误编号
set参数指定了线程等待的信号集。返回时,signop指向的整数将包含发送信号的数量。
要把信号发送给进程,可以调用kill。要把信号发送给线程,可以调用pthread_kill:
#include <signal.h>
int pthread_kill(pthread_t thread,int signo);
返回值:成功0,失败,返回错误编号
可以传一个0值的signo来检查线程是否存在。如果信号的默认处理动作时终止该进程,那么把信号传递给某个线程仍然会杀死
整个进程。
注意,闹钟定时器时进程资源,并且所由的线程共享相同的闹钟。所以,进程的多个线程不可能不干扰地使用闹钟定时器。
程序,我们等待信号处理程序设置标志表明主程序应该退出。唯一可运行的控制线程就是主线程和信号处理程序,所以阻塞信号足以
避免错失标志修改。在线程中,我们需要使用互斥量来保护标志。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
int quitflag;
sigset_t mask;
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t waitloc=PTHREAD_COND_INITIALIZER;
void *thr_fn(void *arg)
{
int err,signo;
for(;;){
err=sigwait(&mask,&signo);
if(err!=0)
printf("sigwait failed:%d",err);
switch(signo){
case SIGINT:
printf("\ninterrupt\n");
break;
case SIGQUIT:
pthread_mutex_lock(&lock);
quitflag=1;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&waitloc);
return 0;
default:
printf("unexpected signal %d\n",signo);
exit(1);
}
}
}
int main(void)
{
int err;
sigset_t oldmask;
pthread_t tid;
sigemptyset(&mask);
sigaddset(&mask,SIGINT);
sigaddset(&mask,SIGQUIT);
if((err=pthread_sigmask(SIG_BLOCK,&mask,&oldmask))!=0)
printf("SIG_BLOCK error:%d\n",err);
err=pthread_create(&tid,NULL,thr_fn,0);
if(err!=0)
printf("can't create thread:%d\n",err);
pthread_mutex_lock(&lock);
while(quitflag==0)
{
pthread_cond_wait(&waitloc,&lock);
}
pthread_mutex_unlock(&lock);
quitflag=0;
if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
printf("SIG_SETMASK error\n");
exit(0);
}
执行:
$ ./example5
^C
interrupt
^C
interrupt
^C
interrupt
^C
interrupt
^C
interrupt
^C
interrupt
^\
线程和fork:
清理锁状态,可以通过pthread_atfork函数建立fork处理程序。
#include <pthread.h>
int pthread_atfork(void (*prepare)(void),void (*parent)(void),void (*child)(void));
返回值:成功0,失败,错误编号。
线程和I/O:
pread和pwrite函数。这些函数在多线程环境下是非常有用的,因为进程中的所有线程共享相同的文件描述符。
考虑两个线程,在同一时间对同一个文件描述符进行读写操作。
线程A 线程B
lseek(fd,300,SEEK_SET); lseek(fd,700,SEEK_SET);
read(fd,buf1,100); read(fd,buf2,100);
如果线程A执行lseek然后线程B在线程A调用read之前调用lseek,那么两个线程最终会读取同一条记录。很显然这不是我们
希望的。
为了解决这个问题,可以使用pread,使偏移量的设定和数据的读取称为一个原字操作。
线程A 线程B
pread(fd,buf1,100.300); pread(fd,buf2,100.700);
使用pread可以确保线程A读取偏移量为300的记录,而线程B读取偏移量为700的记录。
可以使用pwrite来解决并发线程对同一文件进行写操作的问题。
技术不分国界