APUE------线程控制

线程属性

使用pthread_attr_t结构修改线程默认属性,并把这些属性与创建的线程联系起来。可以使用pthread_attr_init函数初始化pthread_attr_t结构。

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

如果在创建线程时就知道不需要了解线程的终止状态,就可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始处于分离状态。可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置成以下两个合法值之一:PTHREAD_CREATE_DETACHED.以分离状态启动线程;或者PTHREAD_CREATE_JIONABLE,正常启动线程,应用程序可以获取线程的终止状态。

#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;否则,返回错误编号

可以条用pthread_attr_getdetachstate函数获取当前的detachstate线程属性。

可以使用函数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;否则,返回错误编号

对于进程来说,虚地址空间的大小是固定的。因为进程中只有一个栈,所以他的大小通常不是问题。但对于线程来说,同样大小的虚地址空间必须被所有的线程共享。如果应用程序使 使用了许多线程,以致这些线程栈的累计大小超过了可用的虚地址空间,就需要减少默认的线程栈大小。另一方面,如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧(stack frame),那么需要的栈大小可能比默认的要大。

如果线程栈的虚地址空间都用完了,那可以使用malloc或者mmap来为可替代的栈分配空间,并用pthread_attr_setstack函数来改变新建线程的栈的位置。由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;否则,返回错误编号

如果希望改变默认的栈大小,但有不想自己处理线程栈的分配问题,这时使用pthread_attr_setstacksize函数就非常有用。设置stacksize属性时,选择的sracksize不能小于PTHREAD_STACK_MIN

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。

#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;否则,返回错误编号

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

同步属性
互斥量属性

互斥量属性是用pthread_mutexattr_t结构表示的。

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

值得注意的三个属性是:进程共享属性、健壮属性以及类型属性。

进程共享互斥量属性需设置为PTHREAD_PROCESS_PRIVATE.

#include <pthread.h>
int pthread_mutexattr_getpshared(
                    const pthread_mutexattr_t *restrict attr,
                    int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
                                int pshared);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

进程共享互斥属性设置为PTHREAD_PROCESS_PRIVATE时,允许pthread线程库提供更有效的互斥量实现,这在多线程应用程序中是默认的情况。在多进程共享多个互斥量的情况下,pthread线程库可以限制较大的互斥量实现。

#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返回值为EOWNERDEAD而不是0。
如果应用状态无法恢复,在线程对互斥量解锁以后,该互斥量将处于永久不可用状态。为了避免这样的问题,线程可以调用pthread_mutex_consistent函数,指明与该互斥量相关的状态在互斥量解锁之前是一直的。

#include <pthread.h>
int pthread_mutex_consistent(pthread_mutex_t *mutex);
//返回值:若成功,返回0;否则,返回错误编号

如果线程没有先调用pthread_mutex_consistent就对互斥量进行了解锁,那么其他试图获取该互斥量的阻塞线程就会得到错误码ENOTRECOVERABLE。如果发生这种情况,互斥量将不再可用。线程通过提前调用pthread_mutex_consistent,能让互斥量正常工作,这样它就可以持续被使用。

类型互斥量属性控制着互斥量的锁定特性
PTHREAD_MUTEX_NORMAL 一种标准互斥量类型,不做任何特殊的错误检查或死锁检测。
PTHREAD_MUTEX_ERRORCHAE 此互斥量类型提供错误检查。
PTHREAD_MUTEX_RECURSIVE 此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。所以,如果对一个递归互斥量加锁两次,然后解锁一次,那么这个互斥量将依然处于加锁状态,对它再次解锁以前不能释放该锁。
PTHREAD_MUTEX_DEFAULT 此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种。

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

#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;否则,返回错误编号

在阻塞线程之前,pthread_cond_wait和pthread_cond_timewait函数释放与条件相关的互斥量。这就允许其他线程函数获取互斥量、改变条件、释放互斥量以及给条件变量发信号。既然改变条件时必须占有互斥量,使用递归互斥量就不是一个好主意。如果递归互斥量被多次加锁,然后用在调用pthread_cond_wait函数中,那么条件永远都不会得到满足,因为pthread_cond_wait所做的解锁操作并不能释放互斥量。

读写锁属性

初始化与反初始化

#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_rwlockattr_t *attr,
                                  int pshared);
//两个函数的返回值:若成功,返回0;否则,返回错误编号
条件变量属性
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_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;否则,返回错误编号

时间属性控制计算机pthread_cond_timedwait函数的超时参数(tsptr)时采用的是哪个时钟。

#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,
                              clockid_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_narrierattr_t *restrict attr,
                    int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr,
                                   int pshared);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

进程共享属性的值可以是PTHREAD_PROCESS_SHARED(多进程中的多线程可用),也可以是PTHREAD_PROCESS_PRIVATE(只有初始化屏障的那个进程内的多个线程可用)。

重入

线程在遇到重入问题时与信号处理程序的类似的。
如果一个函数在相同时间点可以被多个线程安全地调用,就称该函数是线程安全的。
如果一个函数对于多线程来说是可重入的,就说这个函数就是线程安全的。但这并不能说明对信号处理程序来说该函数也是可重入的。如果一个函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。
POSIX.1提供了以线程安全的方式管理FILE对象的方法。可以使用flockfile和ftrylockfile获取给定FILE对象关联的锁,这个锁是递归的。

#include <stdio.h>
int ftrylockfile(FILE *fp);
//返回值:若成功,返回0;若不能获取锁,返回非0数值
void flockfile(FILE *fp);
void funlockfile(FILE *fp);

为了避免开销,出现了不加锁版本的基于字符的标准I/O例程

#include <stdio.h>
int getchar_unlocked(void);
int getc_unlocked(FILE *fp);
//两个函数的返回值:若成功,返回下一个字符;若遇到文件尾或出错,返回EOF
int putchar_unlocked(int c);
int putc_unlocked(int c, FILE *fp);
//两个函数的返回值:若成功,返回C;若出错,返回EOF

除非被flockfile或ftrylockfile的调用包围,否则尽量不要调用这4个函数。

线程特定数据

线程特定数据,也称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。
线程模型促进了进程中数据和属性的共享,许多人在设计线程模型时会遇到各种麻烦。那么为什么有人想在这样的模型中促进阻止共享的借口呢?这其中有两个原因。
第一,有时候需要维护基于每线程的数据。
第二,它提供了让基于进程的借口适应多线程环境的机制。一个很明显的实例就是errno。

我们知道一个进程中的所有线程都可以访问这个进程的整个地址空间。除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。线程特定数据也不例外。虽然底层的实现部分并不能阻止这种访问能力,但管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。

再分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问。就是用pthread_key_create创建一个键。

#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, 
                       void (*destructor)(void *));
//返回值:若成功,返回0;否则,返回错误编号

pthread_key_create还可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。

线程一般使用malloc为线程特定数据分配内存。析构函数通常释放已分配的内存。如果线程没有释放内存之前就退出了,那么这块内存就丢失了,即发生了内存泄漏。

线程可以为线程特定数据分配多个键,每个键都有一个析构函数与它关联。线程退出时,线程特定数据的析构函数将按照操作系实现中定义的顺序被调用。

对所有的线程,我们都可以通过调用pthread_key_delete来取消键与线程特定数据值之间的关联关系。

#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
//返回值:若成功,返回0;否则,返回错误编号

注意,嗲用pthread_key_delete并不会激活与键关联的析构函数。要释放任何与键关联的线程特定数据值的内存,需要在应用程序中采取额外的步骤。

有些线程可能看到一个键值,而其他的线程可能看到的另一个不同的键值,这取决于系统是如何调度线程的,解决这种竞争的办法是使用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_once,系统就能保证初始化例程initfn只被调用一次,即系统首次调用时pthread_once时。创建键时避免出现冲突的一个正确方法如下:

void destructor(void *);

pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void thread_init(viod)
{
    err = pthread_key_create(&key,destructor);
}

int threadfunc(void *arg)
{
    pthread_once(&init_done , thread_init);
}

键一旦创建以后,就可以通过调用pthread_setspecific函数吧键和线程特定数据关联起来。可以通过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_cancel函数调用时所呈现的行为。

可取消状态属性可以是PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCEL_DISABLE。线程可以通过调用pthread_setcancelstate修改它的可取消状态。

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
//返回值:若成功,返回0;否则,返回错误编号

pthread_setcancelstate把当前可取消状态设置为state,把原来的可取消状态存储在由oldstate指向的内存单元,这两步是一个原子操作。

线程启动时默认的可取消状态是PTHREAD_CANCEL_ENABLE。当状态设置为PTHREAD_CANCEL_ENABLE时,对pthread_cancel的调用并不会杀死线程。相反,取消请求对这个线程来说还处于挂起状态,当取消状态再次变为PTHREAD_CANCEL_ENABLE时,线程将在下一个取消点对所有挂起的取消请求进行处理。

如果应用程序很长一段时间没有调用规定的取消点。那么你可以调用pthread_testcancel函数在程序中添加自己的取消点。

#include <pthread.h>
void pthread_testcancel(void);

调用pthread_testcancel时,如果有某个取消请求正处于挂起状态,而且取消并没有置为无效,那么线程就会被取消。但是如果取消设置被无效,pthread_testcancel调用就会有任何效果了。

我们所描述的默认的取消类型也称为推迟取消。调用pthread_cancel以后,在线程到达取消点之前,并不会出现真正的取消。也可以调用pthread_setcanceltype来修改类型

#include <pthread.h>
int pthread_setcanceltype(int type , int *oldtype);
//返回值:若成功,返回0;否则,返回错误编号

pthread_setcanceltype函数把取消类型设置为type(类型参数可以是PTHREADCANCEL_DEFERRED,也可以是PTHREAD_CANCEL_ASYNCHRONOUS),把原来的取消类型返回到oldtype指向的整型单元。

异步取消与推迟取消不同,因为使用异步取消时,线程可以在任意时间撤消,不是非得遇到取消点才能被取消。

线程和信号
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set,
                    sigset_t *restrict oset);
//返回值:若成功,返回0;否则,返回错误编号

阻止信号发送。set参数包含线程用于修改信号屏蔽字的信号集。how参数可以取下列3个值之一: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指向的整数将包含发送信号的数量。

要把信号发送给线程可以用pthread_kill

#include <signal.h>
int pthread_kill(pthread_t thread,int signo);
//返回值:若成功,返回0;否则,返回错误编号

可以传一个0值检查线程是否存在。如果信号的默认处理动作是终止该进程,那么把信号传递给某个线程仍然会杀死整个进程。

注意,闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以,进程中的多个线程不可能互不干扰地使用闹钟定时器。

线程和fork

当线程调用fork时,就为子进程创建了整个进程地址空间的副本。
在多线程的进程中,为了避免不一致状态的问题,POSIX.1声明,在fork返回和子进程调用其中一个exec函数之间,子进程只能调用异步信号安全的函数。这就限制了在调用exec之前子进程能做什么,但不涉及子进程中锁状态的问题。

要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程序。

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*prepare)(void),
                   void (*child)(void));
//返回值:若成功,返回0;否则,返回错误编号

用pthread_atfork函数最多可以安装3个帮助清理锁的函数。prepare frok处理程序由父进程在fork创建子进程前调用。这个fork处理程序的任务是获取父进程定义的所有锁。parent fork处理程序是在fork创建子程序以后,返回之前在父进程上下文中调用。这个fork处理程序的任务是对prepare fork处理程序获得的所有锁进行解锁。child fork处理程序在fork返回之前在子进程上下文中调用。与parent fork处理程序一样,child fork处理程序也必须释放parent fork处理程序获取的所有锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值