Linux/UNIX线程控制

线程控制

 线程属性

调用pthread_create函数的例子中,传入的参数都是空指针,而不是指向pthread_attr_t结果的指针。可以用pthread_attr_t结构修改线程默认属性,并把这些属性与创建的线程联系起来。可以使用pthread_attr_init函数初始化pthreaad_attr_t结构。调用pthread_attr_init以后,pthread_attr_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。如果要修改其中个别属性的值,需要调用其他的函数。pthread_attr_destroy可以去除对pthread_attr_t结构的初始化。

#include<pthread.h>

intpthread_attr_init(pthread_attr_t *attr);

intpthread_attr_destroy(pthread_attr_t *attr);

POSIX.1定义的线程属性主要有detachstate(线程的分离状态属性),guardsize(线程栈末尾的警戒缓冲区大小),stackaddr(线程栈最低地址),stacksize(线程栈的大小(字节数))。

 

如果对现有的某个线程的终止状态不感兴趣,可以使用pthread_detach函数让操作系统在线程退出时回收所占用的资源。如果在创建线程时就知道不需要了解线程的终止状态,则可以修改pthread_attr_t结果中的detachstate线程属性,让线程以分离状态启动。可以使用

pthread_attr_setdetachstate把线程属性detachstate设置为下面的合法值之一:设置PTHREAD_CREATE_DETACHED,以分离状态启动线程;或者以PTHREAD_CREATE_JOINABLE,正常启动线程,引用程序可以获取线程的终止状态。

         可以调用pthread_attr_getdetachstate函数获取当前detachstate线程属性,第二个参数所指向的整数也许被设置为PTHREAD_CREATE_DETACHED,也可能被设置为PTHREAD_CREATE_JOINABLE。

         #include<pthread.h>

intpthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

     int pthread_attr_getdetachstate(pthread_attr_t*attr, int *detachstate);

        

函数pthread_attr_getstack和pthread_attr_setstack可以对线程栈属性进行查询和修改。

#include<pthread.h>

int pthread_attr_setstack(pthread_attr_t*attr, void *stackaddr, size_t stacksize);

int pthread_attr_getstack(pthread_attr_t*attr, void **stackaddr, size_t *stacksize);

这两个函数可以用于管理stackaddr线程属性和stacksize线程属性。

应用程序也可以通过pthread_attr_setstacksize和pthread_attr_getstacksize函数读取或设置线程属性stacksize。

#include<pthread.h>

intpthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

intpthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

 

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存大小。这个属性默认设置为PAGESIZE个字节。可以把guardsize线程属性设置为0,从而不允许属性的这种特征行为发生:在这种情况下,不会提供警戒缓冲区。同样的,如果对线程属性stackaddr做了修改,系统就会假设我们会自己管理栈,并使警戒缓冲区机制无效,等同于guardsize线程属性设为0。

#include<pthread.h>

intpthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);

intpthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);

如果guardsize线程属性被修改了,操作系统可能把它取为页大小的整数倍。如果线程的栈指针溢出到警戒区,应用程序就可以通过信号接收到出错信息。

同步属性

就像线程具有属性一样,线程的同步对象也有属性。

互斥量属性

用pthread_mutexattr_init初始化pthread_mutexattr_t结构,用pthread_mutexattr_destroy对该结构进行回收。

#include<pthread.h>

intpthread_mutexattr_init(pthread_mutexattr_t *attr); 

intpthread_mutexattr_destroy(pthread_mutexattr_t *attr); 

值得注意的两个属性是进程共享属性和类型属性,进程共享属性是可选的,可以通过检查系统中是否定义了_POSIX_THREAD_PROCESS_SHARED符号判断这个平台是否支持进程共享属性,也可以在运行时把_SC_THREAD_PROCESS_SHARED参数传给sysconf函数进行检查。

在进程中,多个线程可以同时访问一个同步对象,这是默认行为,在这种情况下,进程共享属性虚设置为PTHREAD_PROESS_PRIVATE。后面将会说明,存在这样的机制,允许相互独立的多个进程把同一个内存区域映射到它各自独立的地址空间中。就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程共享的内存区域中分配的互斥量就可以用于这些进程的同步。

#include<pthread.h>

intpthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr,

int *restrict pshared);  //查询进程共享属性

intpthread_mutexattr_setpshared(pthread_mutexattr_t *attr,

int pshared); //设置进程共享属性

 

         类型互斥量属性控制着互斥量的特性。PTHREAD_MUTEX_NORMAL类型是标准的互斥量类型,并不做任何特殊的错误检查和死锁检查。PTHREAD_MUTEX_ERRORCHECK互斥量类型提供错误检查。

         PTHREAD_MUTEX_RECURSIVE互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。用一个递归互斥量维护锁的计数,在解锁的次数和加锁的次数不相同的情况下不会释放锁。

         PTHREAD_MUTEX_DEFAULT类型可以用于请求默认语义。操作系统在实现它的时候可以把这种类型*地映射到其他类型。

         可以用pthread_mutexattr_gettype函数得到互斥量类型属性,用pthread_mutexattr_settype函数修改互斥量类型属性。

         intpthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,

int *restrict type); //查询类型属性

intpthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); //设置类型属性

 

读写锁属性

读写锁属性与互斥锁属性类似,也具有属性。用pthread_rwlockattr_init初始化pthread_rwlockattr_t结构,用pthread_rwlockattr_destroy回收结构。

intpthread_rwlockattr_init(pthread_rwlockattr_t *attr); 
intpthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

读写锁支持的唯一属性是进程共享属性,该属性与互斥量的进程共享属性相同。就像互斥量的进程共享属性一样,用一对函数来读取和设置读写锁的进程共享属性。

intpthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr,

int *restrict pshared);

int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,intpshared);

条件变量属性

         与读写锁类似

intpthread_condattr_destroy(pthread_condattr_t *attr);
intpthread_condattr_init(pthread_condattr_t *attr); 
intpthread_condattr_getpshared(const pthread_condattr_t *restrict attr,

int *restrict pshared);

int pthread_condattr_setpshared(pthread_condattr_t *attr,intpshared);

重入

如果一个函数在同一时刻可以被多个线程安全调用,就称该函数是线程安全的。但并不能说明对信号处理函数来说该函数也是可重入的。如果一个函数对异步信号处理的重入是安全的,那么就可以说函数时异步——信号安全的。

很多函数并不是线程安全的,因为它们返回的数据是存放在静态的内存缓冲区,可以通过修改接口,要求调用者自己提供缓冲区使函数变为线程安全的。POSIX.1提供了以安全的方式管理FILE对象的方法,使用flockfile和ftrylockfile获取与给定FILE对象关联的锁。这个锁是递归锁。函数原型如下:

#include<stdio.h>

voidflockfile(FILE *filehandle);
int ftrylockfile(FILE*filehandle);
void funlockfile(FILE*filehandle);

为了避免标准I/O在一次一个字符操作时候频繁的获取锁开销,出现了不加锁版本的基于字符的标准I/O例程。函数如下:
        intgetc_unlocked(FILE *stream);
        intgetchar_unlocked(void);
        intputc_unlocked(int c, FILE *stream);
        intputchar_unlocked(int c);

线程私有数据

线程私有数据是存储和查询某个线程相关的数据的一种机制。把这种数据成为线程私有数据或线程特定数据的原因是,希望每个线程可以独立地访问数据副本,而不需要担心与其他线程的同步访问问题。

进程中的所有线程都可以访问进程的整个地址空间。除了使用寄存器以外,线程没有办法阻止其他线程访问它的数据,线程私有数据也不例外。虽然底层的实现部分并不能阻止管理线程私有数据的函数可以提高线程间的数据独立性。

在分配线程私有数据之前,需要创建与该数据相关联的键。这个键将用于获取对线程私有数据的访问权。使用pthread_key_create创建一个键。

intpthread_key_create(pthread_key_t *keyp, void (*destructor)(void*)); 

创建的键放在keyp所指的内存单元,这个键可以被所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联。创建新键时,每个线程的数据地址设置为null值。

pthread_key_create可以选择为该键关联析构函数,当线程调用pthread_exit或者线程执行返回,正常退出时,数据地址已被设置为非null值,那么析构函数就会被调用。

对所有的线程,都可以调用pthread_key_delete来取消与线程私有数据值之间的关联关系。但它不会激活析构函数。

#include<pthreead.h>

intpthread_key_delete(pthread_key_t*         key);

有些线程可能只看到某个键值,而其他的线程看到的可能是另一个不同的键值,这取决于系统是如何调用线程的,解决这种竞争的办法是使用pthread_once。

int pthread_once(pthread_once_t * once_control,
void (*init_routine)(void));  //避免竞争条件

once_control 必须初始化为PTHREAD_ONCE_INIT; 

 

如果每个线程都调用pthread_once,系统就能保证初始化例程init_routine只被调一次。

键一旦创建就可以通过调用pthread_setspecific函数把键和线程私有数据关联起来。可

以通过pthread_getspecific函数获得线程私有数据的地址。

void*pthread_getspecific(pthread_key_t key); //返回线程私有数据值,没有返回NULL
int pthread_setspecific (pthread_key_tkey, const void *value);  //设置线程私有数据

如果没有线程私有数据与键关联,pthread_getspecific将返回一个空指针,可以据此来

确定是否需要调用pthread_setspecific。

线程和信号

每个线程都有自己的信号屏蔽字,但是信号的处理时进程中所有线程共享的。这意味着尽管单个线程可以组织某些信号,但当线程修改了某个信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他的线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。

线程必须使用pthread_sigmask来阻止信号发送,线程可以用sigwait等待一个或多个信号发生,可以用pthread_kill把信号发送给线程。

#include <signal.h>
intpthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
int sigwait(constsigset_t *set, int *sig);
intpthread_kill(pthread_t thread, int sig);
         如下是同步信号处理的例子:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<signal.h>
 
intquitflag;
sigset_tmask;
 
pthread_mutex_t     lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t        wait = PTHREAD_COND_INITIALIZER;
 
void*thread_func(void *arg)
{
    int err,signo;
    for(;;)
    {
        err = sigwait(&mask,&signo);
        if(err!= 0)
        {
            perror("sigwaiterror.\n");
            exit(-1);
        }
        switch(signo)
        {
        case SIGINT:
            printf("\nInterrupt.\n");
            break;
        case SIGQUIT:
            pthread_mutex_lock(&lock);
            printf("\nQuit.\n");
            quitflag = 1;
            pthread_mutex_unlock(&lock);
            pthread_cond_signal(&wait);
            return 0;
        default:
            printf("unexpected siganl%d\n",signo);
            exit(1);
        }
    }
}
intmain()
{
    int         err;
    sigset_t    oldmask;
    pthread_t   tid;
    sigemptyset(&mask);
    sigaddset(&mask,SIGINT);
    sigaddset(&mask,SIGQUIT);
   pthread_sigmask(SIG_BLOCK,&mask,&oldmask);
   pthread_create(&tid,NULL,thread_func,NULL);
    pthread_mutex_lock(&lock);
    while(quitflag == 0)
        pthread_cond_wait(&wait,&lock);
    pthread_mutex_unlock(&lock);
    quitflag = 0;
    sigprocmask(SIG_SETMASK,&oldmask,NULL);
    exit(0);
}


运行结果如下:

$       ./a.out

^C              键入中断字符

interrupt

^C              键入中断字符

interrupt

^C              键入中断字符

interrupt

^\

Quit.         用结束字符终止

线程和fork

在子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁并且需要释放哪些锁。

如果子进程从fork返回以后马上调用某个exec函数,就可以避免这样的问题。这种情况下,老的地址空间被丢弃,所以锁的状态无关紧要。但如果子进程需要继续做处理工作的话,这种方法就行不通,还需要使用其他的策略。

#include <pthread.h>
intpthread_atfork(void (*prepare)(void), void (*parent)(void),void (*child)(void));
用pthread_atfork函数最多可以安装三个帮助清理锁的函数。prepare fork处理程序由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁。parent fork处理程序是在fork创建子进程以后,但在fork返回之前在父进程环境中调用的,这个fork处理程序的任务是对prepare fork处理程序获得的所有锁进行解锁。child fork处理程序也必须释放prepare fork处理程序获得的所有锁。
下面的例子描述如何使用pthread_atfork和fork处理程序:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<signal.h>
 
pthread_mutex_t     lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t     lock2 = PTHREAD_MUTEX_INITIALIZER;
 
voidprepare(void)
{
    printf("preparing locks...\n");
    pthread_mutex_lock(&lock1);
    pthread_mutex_lock(&lock2);
}
voidparent(void)
{
    printf("parent unlockinglocks...\n");
    pthread_mutex_unlock(&lock1);
    pthread_mutex_unlock(&lock2);
}
void child(void)
{
    printf("child unlockinglocks...\n");
    pthread_mutex_unlock(&lock1);
    pthread_mutex_unlock(&lock2);
}
void*thread_func(void *arg)
{
    printf("thread started...\n");
    pause();
    return 0;
}
int main()
{
    pid_t      pid;
    pthread_t  tid;
    pthread_atfork(prepare,parent,child);
   pthread_create(&tid,NULL,thread_func,NULL);
    sleep(2);
    printf("parent about tofork.\n");
    pid = fork();
    if(pid == -1)
    {
        perror("fork() error");
        exit(-1);
    }
    if(pid == 0)
        printf("child returned fromfork.\n");
    else
        printf("parent returned formfork.\n");
    exit(0);
}


执行结果如下:

 threadstarted…

parent about tofork…

preparing locks

parent unlockinglocks…

parent returnedfrom fork

child unlockinglocks…

child returnedfrom fork

可见,prepare fork处理程序在调用fork以后运行,childfork处理程序在fork调用返回到子进程之前运行,parent fork处理程序在返回个父进程前运行。


Linux/UNIX线程控制,布布扣,bubuko.com

Linux/UNIX线程控制

上一篇:C++11中的指针空值nullptr


下一篇:C++对C的加强 总结(1)