您的位置:首页 > 编程语言

UNIX环境高级编程 第十二章:线程控制

2018-01-16 19:26 260 查看
上一章讲了线程以及线程同步的基础知识。本章将讲解控制线程的行为方面的详细内容,介绍线程属性和同步原语属性。前面的章节中使用的都是它们的默认行为,没有进行详细的介绍。还将介绍同一进程的多个线程之间如何保持数据的私有性。最后讨论基于进程的系统调用如何与线程进行交互。

(一)线程属性

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

(1)每个对象与它自己类型的属性对象进行关联。一个属性对象可以代表多个属性。

(2)有一个初始化函数,把属性设置为默认值。

(3)还有一个销毁属性对象的函数。如果初始化函数分配了与属性对象关联的资源,销毁函数负责释放这些资源

(4)每个属性都有一个从属性对象中获取属性值的函数。

(5)每个属性都一个设置属性值的函数。

2. 可以使用pthread_attr_init函数初始化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);


detachstate 线程的分离状态属性
guardsize   线程栈末尾的警戒缓冲区大小(字节数)
stacksddr   线程栈的最低地址
stacksize   线程栈的最小长度(字节数)


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

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

可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置成以下两个合法值之一:

(1)PTHREAD_CREATE_DETACHED以分离方式启动线程;

(2)或者PTHREAD_CREATE_JOINABLE,正常启动线程,应用程序可以获取线程的终止状态。可以调用pthread_attr_getdetachstate函数获取当前的detachstate线程属性。

#include <pthread.h>
//修改线程的分离状态属性;
int pthread_attr_getdetachstate(pthread_attr_t *restrict attr,int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate)


4. 可以使用函数pthread_attr_getstack和pthread_attr_setstack对线程栈属性进行管理。

//获取或修改线程的栈属性: hljs glsl">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);


5. 对于进程来说,虚拟地址空间的大小是固定的。

因为进程中只有一个栈,所以它的大小通常不是问题。但对于线程来说,同样大小的虚拟地址空间必须被所有的线程栈共享

。(1)如果应用程序使用了许多线程,以至这些线程栈的累积大小超过了可用的虚拟地址空间,就需要减少默认的线程栈大小。

(2)另一方面,如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧(stack frame),那么需要的栈大小可能比默认的大。

如果线程的虚拟地址空间都用完了,那可以使用malloc或者mmap来为可替代的栈分配空间,并用pthread_attr_setstack函数来改变新建线程栈的位置。

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

hljs glsl">//获取或修改线程的栈属性:
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);//stackaddr是栈的结尾处


如果希望改变栈的大小,但又不想自己处理线程栈的分配问题(即线程栈的首地址),这时使用pthread_attr_setstacksiz函数就非常有用。设置stacksize属性时,选择stacksize不能小于PTHREAD_STACK_MIN。

6. 线程属性guardsize

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存大小。这个属性默认值是由具体实现来定义的,但常用值是系统页大小。

(1)可以把guardsize线程属性设置为0,但是不允许属性的这种特征行为发生:在这种情况下,不会提供警戒缓冲区。

(2)同样,如果修改了线程属性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);


如果guardsize线程属性被修改了,操作系统可能会把它取为页大小的整数倍。如果线程的栈指针溢出到警戒区域,应用程序就可能通过信号接收到出错信息。线程还有一些其他的pthread_attr_t结构中没有表示的属性:可撤销状态和可撤销类型。

7. 并发度

并发度控制着用户级线程可以映射的内核线程或进程的数目,如果操作系统的实现在内核级的线程和用户级的线程之间保持一对一的映射,那么改变并发度并不会有什么效果,因为所有的线程都可能被调度到,但是如果操作系统的实现让用户级线程到内核级线程或进程之前的映射关系是多对一的话,那么在给定时间内增加可运行的用户级线程数,可能会改善性能。pthread_setconcurrency 函数可以用于提示系统,表明希望的并发度

函数功能:获取线程并发度信息;设置并发度只是提示系统,系统并不一定采取;

#include <pthread.h>
int pthread_getconcurrency(void);//获取当前并发度;
int pthread_setconcurrency(int level);//设置并发度


(二)同步属性

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

1. 互斥量属性可以用 pthread_mutexattr_t 数据结构来进行操作,

(1)都是通过PTHREAD_MUTEX_INITIALZER常量或使用指向互斥量属性结构的空指针作为参数调用pthread_mutex_init函数,得到互斥量的默认属性。

(2)对于非默认属性,可以用pthread_mutexattr_init初始化pthread_mutexattr_t结构,用pthread_mutexattr_destory 来反初始化。属性的初始化操作如下:

初始化互斥量属性;pthread_mutexattr_init函数用默认的互斥量属性初始化pthread_mutexattr_t结构;两个属性是进程共享属性和类型属性;

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);


(1)进程共享属性:设置和读取进程互斥量共享属性。

#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);


进程共享属性可以通过符号_POSIX_THREAD_PROCESS_SHARED符号来判断平台是否支持这个属性。也可以通过_SC_POSIX_THREAD_PROCESS_SHARE参数传递给sysconf函数进行检查。

在进程中,多个线程可以访问同一个同步对象。这时进程共享互斥量属性设置为PTHREAD_PROCESS_PRIVATE。

注:在14和15章中将会看到:允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。进程间共享数据也需要同步。这时进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步。

(2) 健壮属性:互斥量健壮属性与在多个进程间共享互斥量有关。

(3) 获取或修改类型属性p347

int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);//获取互斥量的类型属性
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);//修改互斥量的类型属性


2. 读写锁属性

读写锁与互斥量类似,也是有属性的。以下是初始化与反初始化属性结构的函数:

int pthread_rwlockattr_init(const pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(const pthread_rwlockattr_t *attr);


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

虽然POSIX标准只定义了一个读写锁属性,但不同平台的实现可以自由地定义额外的、非标准的属性。

int pthread_rwlockattr_getshared(const pthread_rwlockattr_t *restrict attr,int *restrict pshared);
int pthread_rwlockattr_setshared(pthread_rwlockattr_t *attr,int pshared);


3. 条件变量属性:条件变量也只有进程共享属性和时钟属性

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);


(1)与其他同步属性一样,条件变量支持进程共享属性。它控制着条件变量是可以被单进程的多个线程使用,还是可以被多进程的线程使用。以下函数用于操作进程共享属性的值。

int pthread_rwlockattr_getshared(const pthread_rwlockattr_t *restrict attr,int *restrict pshared);
int pthread_rwlockattr_setshared(pthread_rwlockattr_t *attr,int pshared);


(2)时钟属性控制pthread_cond_timeout函数的超时参数采用的是哪个时钟。以下函数用于设置和获取超时时钟:

int pthread_condattr_getclock(pthread_condattr_t *attr,clock_t *restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr,clock_t clock_id);


4. 屏障属性

初始化和反初始化:

int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);


(1)目前定义的屏障属性只有进程共享属性,它控制着屏障是可以被多进程线程使用,还是只能被初始化屏障的进程内的多线程使用。以下函数用于获取或设置屏障共享属性。

int pthread_barrierattr_getshared(const pthread_barrierattr_t *restrict  attr,int *restrict pshared);
int pthread_barrierattr_setshared(pthread_barrierattr_t *attr,int pshared);


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

(三)重入

如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。

如果一个函数多个线程来说是可重入的,就说这个函数就是线程安全的。但这并不能说明对信号处理程序来说该函数也是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就说函数是异步信号安全的。

异步信号安全和线程安全的区别???

可能是因为函数内有加锁操作。死锁:线程对同一个互斥量枷锁两次或者两个线程互相加锁另外一个线程的互斥锁。

- 情况1:比如一个函数对资源加锁了,在调用这个函数时信号发生了,信号处理程序中又试图对资源加锁,这时就发生了死锁。

- 情况2:如果一个线程调用的函数对资源加锁,此时内核调度了另一个线程,函数对此资源再次加锁,会阻塞。待到前一个线程解锁时线程二回获得该锁。

(四)线程特定数据

线程特定数据(hread_specific data),也称线程私有数据(thread_private data),是存储和查询某个特定线程相关数据的一种机制。我们把这种数据称为线程特定数据或线程私有数据的原因是,我们希望每个线程可以访问它自己单独的数据副本,而不需要担心与其他线程的同步访问问题。

线程模型促进了进程中数据和属性的共享,那么为什么又需要促进阻止共享的接口呢?这有两个原因:

第一,有时候需要维护基于每个线程(per-thread)的数据。这些数据是独立于某个线程的。

第二,它提供了让基于进程的接口适应多线程环境的机制。一个很明显的实例就是errno。以前基于进程的接口errno定义为进程上下文中全局可访问的整数。系统调用和库例程在调用或执行失败时设置errno,把它作为操作失败的附属结果。为了让线程也能使用哪些原本基于进程的系统调用和库例程,errno被重新定义为线程私有数据。这样,一个线程做了重置errno的操作也不会影响进程中其他线程的errno值。

1. 我们知道一个进程中的所有线程都可以访问这个进程的整个地址空间。除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。

线程特定数据也不例外。虽然底层的实现部分并不能阻止这种访问能力,但管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。

在分配线程特定数据之前,需要创建该数据关联的键(key)。这个键将用于获取对线程特定数据的访问。

#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void *));


(1)创建的键存储在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,每个线程被这个键与不同的线程特定数据地址进行关联。创建新键时,每个线程的数据地址设为空值。

(2)除了创建键以外,还可以为键关联一个可选的析构函数。当这个线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。线程正常退出时该析构函数被调用,若非正常退出则不被调用。

(3)线程通常使用malloc为线程特定数据分配内存。析构函数通常释放已经分配的内存。如果线程没有释放内存之前就退出了,那么这块内存会丢失,即线程所属的进程出现了内存泄漏。

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

int pthread_key_delete(pthread_key_t *keyp);


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

3. 一旦创建键以后,就可以通过调用pthread_setspecific函数把键和线程特定数据关联起来。可以通过pthread_getspecific函数获得线程特定数据的地址。

void *pthread_getspecific(pthread_key_t key);//若没有线程特定数据值,则返回NULL;
int pthread_setspecific(pthread_key_t key,const void *value);


4. 每个进程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。

进程中的信号是递送到单个线程的。如果一个信号与硬件故障有关,那么该信号一般会被发送到引起该事件的线程中去,而其他信号则被发送到任意一个线程。 为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中。然后可以安排专用的线程处理信号。!!!

5. 当线程调用fork时,就为子进程创建了整个进程地址空间的副本。

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

在子进程内部,只存在一个线程,它是由父进程中调用fork线程的副本构成的。如果父进程中的线程占用锁,子进程将同样占有这些锁。问题是子进程并不包含占有锁线程的副本,所以子进程就没办法知道它占有了哪些锁,需要释放哪些锁。如果fork之后立即使用exec替换地址空间,就不会有这个问题。

6.

当把非线程的传旭写成线程的版本时,会碰到函数中使用静态变量的情况,从而引起常见的编译错误。在不考虑冲入的情况下,静态变量无可非议,但是在同一个进程中的不同线程几乎同时调用这样的函数,就会出现错误,后果是不确定的,因为这些静态变量无法为不同的线程保存各自的值。

解决方法:

(1)使用线程特定数据,这样转换成了只能在多线程系统上工作的函数。

每个系统支持有限的线程特定数据元素。POSIX要求这个限制不小于128(每个进程)。系统为每个进程维护一个我们称之为key结构的结构数组。key结构中的标志指示这个数据元素是否正在使用,所有的标志初始化为“不在使用”。当一个线程调用pthread_key_create创建一个新的线程特定数据元素时,系统会返回第一个不在使用的元素。key结构中的析构函数指针,当一个线程终止时,系统将扫描该线程的pkey数组,为每个非空的pkey指针调用相应的析构函数。

除了进程范围的key结构数组外,系统还在进程内维护关于每个线程的多条信息。这些特定于线程的信息我们称之为pthread结构,其部分内容是我们称之为pkey数组的一个128个元素的指针数组。

注意:当我们调用pthread_key_create创建一个键时,系统告诉我们这个键。每个线程可以随后为该键存储一个值(指针),而这个指
a7b0
针通常是每个线程通过malloc获得的。具体的函数如下:

#include <pthread.h>
int pthread_once(pthread_once_t *onceptr, void (*init)(void));
int pthread_key_create(pthread_key_t *keyptr, void (*destructor)(void *value));
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);


不管多少个线程只有第一个执行它的线程运行一次被掉函数,保证了分配的rl_key的安全。

if ((ptr = pthread_getspecific(rl_key)) == NULL)
// 检查当前线程key域是否有值
pthread_setspecific(rl_key, ptr);
// 设置当前线程的pthread结构key的值
void destructor(void *value)
{
printf("pthread:%d destructor value(%p)\n", pthread_self(), value);
free(value);// 线程结束后执行。线程结束后,会自动掉此析构函数,释放分配资源。
}


(2)改变调用顺序,把函数参数和静态变量都放在一个结构中,可以再支持/不支持线程的系统上使用,但是调用函数的所有应用程序都要更改。

(3)改变接口结构,避免使用静态变量
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: