UNIX环境高级编程(3) 第十二章

本文详细探讨了线程控制和管理的关键概念,包括线程限制、属性设置、同步对象属性、线程特定数据、取消选项及线程与信号、fork的关系。通过介绍不同线程属性的设置与获取方法,阐述了线程在操作系统中的行为调整方式,以及如何通过同步属性如互斥量、读写锁、条件变量和屏障属性来协调多线程间的交互。

12 线程控制

12.2 线程限制

在第2章没有列出限制名,而这些限制可以通过sysconf函数查询。这些限制是为了增强应用程序在不同的操作系统实现之间的可移植性。

限制名称描述name参数
PTHREAD_DESTRUCTOR_ITERATIONS线程退出时操作系统实现试图销毁线程特定数据的最大次数_SC_THREAD_DESTRUCTOR_IRERATIONS
PTHREAD_KEYS_MAX进程可以创建的键的最大数目_SC_THREAD_KEYS_MAX
pTHREAD_STACK_MIN一个线程的栈可用的最小字节数_SC_THREAD_STACK_MIN
PTHREAD_THREADS_MAX进程可以创建的最大线程数_SC_THREAD_THREADS_MAX

12.3 线程属性

pthread接口允许我们通过设置每个对象关联的不同属性来细调线程和同步对象的行为。

  • (1)每个对象与它自己类型的属性对象进行关联(线程与线程属性关联,互斥量和互斥量属性关联,等)。一个属性对象可以代表多个属性。需要提供相应的函数来管理属性对象。
  • (2)有一个初始化函数,把属性设置为默认值。
  • (3)还有一个销毁属性对象的函数。如果初始化函数分配了与属性对象关联的资源,销毁函数负责释放这些资源。
  • (4)每个属性都有一个从属性对象中获取属性值的函数。
  • (5)每个属性都有一个设置属性值的函数。
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);//初始化pthread_attr_t结构(包含所有操作系统实现支持的所有线程属性的默认值
int pthread_attr_destroy(pthread_attr_t *attr);
若成功,返回0;否则,返回错误编号

如果pthread_attr_init的实现对属性对象的内存空间是动态分配的,pthread_attr_destroy就会释放该内存空间。

分离线程:如果对现有的某个线程的终止状态不感兴趣的话,可以使用pthread_detach函数让操作系统在线程退出时收回它所占用的资源。

如果在创建线程时就知道不需要了解线程的终止状态,就可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始就处理返利状态。

#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr,int *detachstate);//获取当前的detachstate属性
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate);
若成功,返回0;否则,返回错误编号

detachstate:

PTHREAD_CREATE_DETACHED:以分离状态启动线程
PTHREAD_CREATE_JOINABLE:正常启动线程,应用程序可以获取线程的终止状态。

可以使用函数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线程属性被定义为栈的最低内存地址,但并不一定是栈的开始位置。可能是栈的结尾位置。

应用程序可以通过下面两个函数读取或设置线程属性stacksize:

#include <pthread.h>
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,
                              size_t *restrict stacksize);
//读取线程属性stacksize
int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);//设置线程属性stacksize
若成功,返回0;否则,返回错误编号

注:设置的stacksize不能小于PTHREAD_STACK_MIN。

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。这个属性默认值是由具体实现来定义的,常用值是系统页大小。可以把guardsize线程属性设置为0,不允许属性的这种特征行为发生:在这种情况下,不会提供警戒缓冲区。同样,如果修改了线程属性stackaddr,系统就会认为我们自己管理栈,进而使栈警戒缓冲区机制无效。

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

注:如果修改了线程属性stackaddr,系统就认为我们将自己管理栈,进而使栈警戒缓冲区机制无效,这等同于把guardsize线程属性设置为0。

12.4 同步属性

线程的同步对象也有属性。本节有:互斥量属性、读写锁属性、条件变量属性以及屏障属性。

4.1 互斥量属性

对于非默认属性,可以用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;否则,返回错误编号。

进程共享互斥量属性默认设置为PTHREAD_PROCESS_PRIVATE。存在机制:允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。即如果进程共享互斥量设置为PTHREAD_PROCESS_SHARED,从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步。

#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_MUTEX_STALLED,这意味着持有互斥量的进程终止时不需要采取特别的动作。这种情况下,使用互斥量后的行为是未定义的,等待该互斥量解锁的应用程序会被有效地“拖住”。另一个取值是PTHREAD_MUTEX_ROBUST。这个值将导致线程调用pthread_mutex_lock获取锁,而该锁被另一个进程持有,但它终止时并没有对该锁进行解锁,此时线程会阻塞,从pthread_mutex_lock返回的值为EOWNERDEAD而不是0.

#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_consisteng函数,指明与该互斥量相关的状态在互斥量解锁之前是一致的。

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

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

类型互斥量属性:

互斥量类型特性
PTHREAD_MUTEX_NORMAL标准互斥量类型,不做任何特殊的错误检查货死锁检测
PTHREAD_MUTEX_ERRORCHECK提供错误检查
PTHREAD_MUTEX_RECURSIVE允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不等时,不会释放锁
PTHREAD_MUTEX_DEFAULT可以提供默认属性和行为。操作形同在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种

互斥量类型行为:

互斥量类型没有解锁时重新加锁?不占用时解锁?在已解锁时解锁?
PTHREAD_MUTEX_NORMAL死锁未定义未定义
PTHREAD_MUTEX_ERRORCHECK返回错误返回错误返回错误
PTHREAD_MUTEX_RECURSIVE允许返回错误返回错误
PTHREAD_MUTEX_DEFAULT未定义未定义未定义
#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_timedwait函数释放与条件相关的互斥量,而递归互斥量被多次加锁,pthread_cond_wait所做的解锁操作并不能释放互斥量。

4.2 读写锁支持的唯一属性是进程共享属性:

用下面函数可进行读写锁属性的pthread_rwlockattr_t 结构初始化和反初始化:

#include <pthread.h>
int pthread_relockattr_getpshared(const pthread_relockattr_t 
                                        *restrcit attr,
                                  int *restrcit pshared);
int pthread_rwlockattr_setpshared(pthread_relockattr_t *attr,
                                  int pshared)
若成功,返回0;否则,返回错误编号。
4.3 条件变量属性

条件变量有两个属性:进程共享属性和时钟属性。

#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 
                                        *restrcit attr
                                  int *restrcit pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,
                                  int pshared)
若成功,返回0;否则,返回错误编号

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

使用下函数获取和设置时钟ID:

#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 *restrict clock_id);
若成功,返回0;否则,返回错误编号   
4.4 屏障属性
#include <pthread.h>
it pthread_barrierattr_init(pthread_barrierattr_t *attr);
it pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
若成功,返回0;否则,返回错误编号

屏障属性只有进程共享属性:

int pthread_barrierattr_getpshared(const pthread_barrierattr_t 
                                        *restrcit attr,
                                  int *restrcit pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr,
                                  int pshared)
若成功,返回0;否则,返回错误编号
12.5 重入

如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。很多函数并不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中。
如果一个函数对多个线程来说是可重入的,就说这个函数就是线程安全的。

以线程安全的方式管理FILE对象的方法:使用flockfile和ftrylockfile获取给定FILE对象关联的锁(这个锁是递归的):

#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 putc_unlocked(int c,FILE *fp);
若成功,返回c;若出错,返回EOF

一旦对FILE对象进行加锁,就可以在释放锁之前对这些函数进行多次调用。

12.6 线程特定数据

thread-specific data,也称为线程私有数据(thread-private data),是储存和查询某个特定线程相关数据的一种机制。

线程模型促进了进程中数据和属性的共享,许多人在设计线程模型时会遇到各种麻烦,那么为什么有人想在这样的模型中促进阻止共享的接口呢?两个原因:

  • 1,有时候需要维护基于每线程(per-thread)的数据,因为线程ID并不能暴增是小而连续的整数,所以就不能简单地分配一个每
    每线程数据组,用线程ID作为数组的索引。
  • 2,提供让基于进程的接口适应多线程环境的机制。一个很明显的实例就是errno。

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

#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
	成功返回0,出错返回错误编号;

对所有的线程,我们都可以通过调用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 (*initflag)(void))
	成功返回0;否则返回错误编码;

键一旦创建以后就可以通过调用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,否则返回错误编码;

12.7 取消选项

线程可通过调用pthread_setcancelstate修改它的可取消状态:

#include <pthread.h>
int	pthread_setcancelstate(int stat, int *oldstate);
成功返回0,否则返回错误编码。  

如果应用程序在很长的一段时间内都不会调用POSIX.1定义的取消点,那么你可以调用pthread_testcancel函数在程序中添加自己的取消点:

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

调用上函数时,如果有某个取消请求正处于挂起状态,而且取消并没有置为无效,那么线程就会被取消,但是如果取消被置为无效,pthread_testcancel调用就没有任何效果了。
可以通过pthread_setcanceltype修改取消类型:

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

12.8 线程和信号

每个线程都有自己的信号屏蔽字,但是信号的处理时进程中所有线程共享的。这就意味着单个线程可阻止某些信号,担当某个线程修改了给定的信号相关的处理行为之后,所有线程都必须共享这个处理行为的改变。10.12节讨论了如何使用sigprocmask函数来阻止信号发送。然而,sigprocmask的行为在多线程的京城中并没有定义,线程必须使用pthread_sigmask。

#include <signal.h>
int	pthread_sigmask(int how, const sigset_t *restrict set, sigset_t  *restrict oset);
	返回值:成功返回0,出错返回错误编号;

线程可以通过调用sigwait等待一个或多个信号的出现。

#include <signal.h>
int	sigwait(const sigset_t *restrict set, int *restrict signop);
	成功返回0,出错返回出错编号;

要把信号发送给进程,可以调用kill(10.9节);要把信号发送给线程,可以调用平pthread_kill。

#include <signal.h>
int pthread_kill(pthread_t thread, int signo);
	成功返回0,出错返回错误编码;

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

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

12.9 线程和fork

当线程调用fork时,就位子进程创建了整个进程地址空间的副本。只要两者都没有对内存内容作出改动,父进程和子进程之间还可以共享内存页的副本。

子进程通过集成整个地址空间的副本,还从父进程那里继承了互斥量、读写锁和条件变量的状态。如果父进程包含一个以上的线程,子进程在fork返回后,如果紧接着不是马上调用exec的话,就需要亲历锁状态。

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

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

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

要清除锁状态,可通过下函数建立fork处理程序:

#include <pthread.h>
int pthread_atfork(void (* prepare)(void), void (* parent)(void), void (* child)(void));  //函数成功返回0, 错误返回错误码。

该函数最多可安装3个fork句柄来帮助我们清理互斥锁状态。
prepare句柄将在fork调用创建出子进程之前被执行,它可以用来获取所有父进程中定义的所有锁。

parent句柄则是fork调用创建出子进程之后,而fork返回之前,在父进程中被执行。它的作用是释放所有在prepare句柄中被锁住的所有锁。

child句柄是fork返回之前,在子进程中被执行。和parent句柄一样,child句柄也是用于释放所有在prepare句柄中被锁住的所有锁。

线程和I/O

3.11节介绍了pread和pwrite函数,这些函数在多线程环境下是非常有用的,因为进程中的所有线程共享相同的文件描述符。

考虑两个线程,在同一时间对两个文件描述符进行读写操作:

线程A

lseek(fd, 300, SEEK_SET);
read(fd, buf1, 100);

线程B

lseek(fd, 700, SEEK_SET);
read(fd, buf2, 100);

如果线程A执行lseek然后线程B在线程A调用read之前调用lseek,那么两个线程最终会读取同一条记录。很显然不是我们希望的。

为了解决这个问题,可以使用pread,使偏移量的设定和数据读取成为一个原子操作。

线程A

pread(fd, buf1, 100, 300);

线程B

read(fd, buf2, 100, 700);

使用pread可以确保线程A读取偏移量为300的记录,而线程B读取偏移量为700的记录。可以使用pwrite来解决并发线程对同一文件进行写操作的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值