Chapter 12 线程控制

1.线程限制

用sysconf函数可以获得和thread相关的一些系统信息,主要是线程相关的一些最大值:

线程限量和sysconf的名字参数
限量名描述名字参数
PTHREAD_ DESTRUCTOR_ITERATIONS 当一个线程退出时一个实现将尝试销毁线程相关数据的最大次数。 _SC_THREAD_ DESTRUCTOR_ITERATIONS
PTHREAD_ KEYS_MAX 一个进程可以创建的关键字的最大数量。 _SC_THREAD_ KEYS_MAX
PTHREAD_ STACK_MIN 可以作为一个线程栈的最少字节数。 _SC_THREAD_ STACK_MIN
PTHREAD_ THREADS_MAX 一个进程可以创建的最大线程数 _SC_THREAD_ THREADS_MAX

虽然标准定义了这些常量,不过在很多系统上面可能根本就没有定义对应的限制符号(如_SC_THREAD_DESTRUCTOR_ITERATIONS可能未定义),或者sysconf函数返回错误。因此在很多时候这些很难派上用场

 

2.线程属性

1).前面讲到pthread_create等函数的时候,这些函数有一个参数pthread_attr_t。缺省情况下可以传NULL。但是如果想自己定义线程的相关属性的话,应该调用pthread_attr_init函数来定义:

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
//成功返回0,失败返回错误号

pthread_attr_init函数负责初始化pthread_attr_t结构为缺省值。pthread_attr_destroy负责释放在pthread_attr_init函数调用时分配的内存,同时pthread_attr_destroy将会用无效值初始化属性对象,所以如果它被误用,pthread_create会返回一个错误。

 

2).

POSIX.1线程属性
名字描述FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9
detachstate 分离的线程属性 * * * *
guardsize 在线程栈末尾的保卫缓冲的字节尺寸   * * *
stackaddr 线程栈的最低地址 ob * * ob
stacksize 线程栈的字节尺寸 * * * *

注:线程都拥有自己的栈

 

a.如果在创建这个线程的时候知道不需要线程的终止状态,,通过修改pthread_attr_t结构体里的detachstate属性,使线程以分离状态启动。可以使用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,失败返回错误号。

可以调用pthread_attr_getdetachstate来得到当月的detachstate属性。被第二个参数指向的整型被设置为PTHREAD_CREATE_DETACHED或PTHREAD_CREATE_JOINABLE,取决于给定的pthread_attr_t结构体的这个属性的值。

 

b.查询和修改线程栈属性的一般通过较新的函数pthread_attr_getstack和pthread_attr_setstack来进行。这些函数去除了更老的接口定义里的歧义。

#include <pthread.h>

int pthread_attr_getstack(const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restric stacksize);
int pthread_attr_setstack(pthread_attr_t *attr, void *statckaddr, size_t *stacksize);
//两者成功返回0,失败返回错误号。

这两个函数可以用于管理stackaddr线程属性,也可以用于管理stacksize线程属性,如果线程栈用完了虚拟进程空间,那么调用malloc或mmap(14.9节)来为一个代替的栈分配空间,可以调用pthread_attr_setstack和pthread_attr_getstack来获得/设置线程的栈位置

 

c.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_setstackszie(pthread_attr_t *attr, size_t stacksize);
//两者成功返回0,失败返回错误号。

 

d.线程属性guardsize控制着线程末尾之后用以避免栈溢出的扩展内存的大小,缺省情况下这个大小正好是一个页=PAGESIZE。甚至可以用函数将该数值设置为0来禁止这个功能,如果我们修改了栈地址的话,系统会认为我们会自己处理溢出的问题,因此也不会提供这个功能。调用pthread_attr_get_guardsize和pthread_attr_set_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,失败返回错误号。


e.更多线程属性

如果具体操作系统实现是按照1对1,也就是一个用户模式线程对应一个内核模式线程的话,那么修改这个值没有作用。但是如果操作系统实现用少量内核模式线程/进程来模拟用户模式线程的话,那么修改这个值可能会提高或者降低程序和系统的性能。Level值并没有具体的意义,只是一个hint。Level=0表示让系统自动选择。pthread_setconcurrency函数可以提示系统,表明希望的并发度,函数原型如下:

#include <pthread.h>

int pthread_getconcurrency(void);
//返回当前并发级数。

int pthread_setconcurrency(int level);
//成功返回0,失败返回错误号。

pthread_getconcurrency函数返回当前并发度。如果操作系统正控制着并发度(也就是说,之前没有调用过pthread_setconcurrency),那么pthread_getconcurrency将返回0

 

3.同步属性

就像线程具有属性一样,线程的同步对象也有属性,下面列出互斥量、读写锁和条件变量的属性

1).互斥量属性

a.使用pthread_mutexattr_init来初始化一个phread_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,失败返回错误号。

 

b.可以使用pthread_mutexattr_getpshared函数来查询一个pthread_mutexattr_t结构体来得到进程共享属性。我们可以用pthread_mutexattr_setpshared函数改变进程共享属性。

#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库提供更高效的实现,当属性被设为PTHREAD_PROCESS_PRIVATE时,这是多线程应用的默认情况

 

c.可以用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_MUTEX_NORMAL 死锁 无定义 无定义
PTHREAD_MUTEX_ERRORCHECK 返回错误 返回错误 返回错误
PTHREAD_MUTEX_RECURSIVE 允许 返回错误 返回错误
PTHREAD_MUTEX_DEFAULT 无定义 无定义 无定义

 

2).读写锁属性

读写锁和互斥量互斥,也有属性。用pthread_rwlockattr_init来初始化一个pthread_rwlockattr_t结构体,用pthread_rwlockattr_destroy来回收这个结构体。

#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,失败返回错误号。

 

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 *restrict attr, int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
//两者成功返回0, 失败返回错误号。

 

4.重入

1).大部分Single UNIX Specification所定义的函数都是线程安全的,但是也有不少例外。实际使用的时候建议参考文档,确定函数是否是线程安全。

2.)文件支持用ftrylockfile, flockfile, funlockfile来锁定文件访问。标准IO函数被要求必须调用在内部实现中调用flockfile, funlockfile。基于字符的部分IO函数具有非线程安全版本,以_unlocked结尾,如:getchar_unlocked, getc_unlocked, putchar_unlocked, putc_unlocked

3).书中提供了一个可重入的getenv_r实现。要点是:

a.用到了Recursive Mutex(使用pthread_mutexattr_settype函数调用设置)来保护自己和其他线程冲突(普通的Mutex就可以做到),同时允许重入(必须用Recursive Mutex)

b.要求调用者提供自己的buffer,而不是用静态全局变量envbuf来访问结果

c.使用pthread_once函数保证只调用一个初始化函数一遍,用于初始化Mutex(当然用其他方法也可以)

 

 

5.线程私有数据

  • 线程私有数据是一种很方便的将数据和线程联系起来的方法,在C Runtime中也大量用到线程私有数据来维护线程相关的数据,一个典型的例子是errno:实际上errno是一个函数调用,返回和线程相关的错误值。Windows中有类似的机制,称为TLS (Thread Local Storage)
  • 访问线程私有数据需要使用Key。不同线程使用同一个key访问同一类型的数据(比如Errno),但是可以存放不同的值。Key的类型为pthread_key_t
  • 用pthread_key_create函数创建key

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

#include <pthread.h>

int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
//成功返回0,失败返回错误号。

创建的关键字被存储在keyp所指的内存单元。这个键可以被进程里所有线程使用,但是每个线程将把这个键关联于一个不同的线程特定数据地址。当创建字被创建时,每个线程的数据地址被设为null值。

一般情况下,这个destructor用来销毁用户用malloc为线程私有数据分配的空间。注意:一般不应该用destructor来调用pthread_key_delete,因为delete对于一个key只用调一次,而destructor是对每个线程都调用的,前提是线程正常退出并且TSD不为NULL。

Key的总数量可能会有限制。可以用PTHREAD_KEYS_MAX来查询最大值。因为调用析构函数的时候这个析构函数可能又会创建新的Key,所以当线程退出的时候,调用Destructor的过程会反复继续直到没有key具有非NULL值或者次数到达最大值PTHREAD_DESTRUCTOR_ITERATIONS为止。这个值可以用sysconf获得。

 

2).对于所有线程,都可以通过调用pthread_key_delete来取消与线程私有数据值之间的关联

#include <pthread.h>

int pthread_key_delete(pthread_key_t *key);
//成功返回0,失败返回错误号。

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

 

3).有些线程可能看到一个关键字值,而另一个线程可能看到一个不同的值,这取决于系统如果调度线程。解决这个竞争的方法是使用pthread_once。

#include <pthread.h>

pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
//成功返回0,失败返回错误码。

 

4).一旦关键被创建,就可以把线程私有数据关联到这个键,通过调用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_getspecific将返回一个空指针,可以使用它来确定是否需要调用pthread_setspecific

 

6.取消选项

有两个线程属性没有包含在pthread_attr_t结构体中,它们是取消状态和取消类型。这些属性影响了线程响应pthread_cancel所呈现的调用行为。

1).可取消状态属性可以是PTHREAD_CANCEL_ENABLE或PTHREAD_CANCEL_DISABLE。通过调用pthread_setcancelsatate改变它的可取消状态

#include <pthread.h>

int pthread_setcancelstate(int state, int *oldstate);
//成功返回0,失败返回错误号。

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

 

2).可以调用pthread_testcancel来在你的程序里加入的取消点。

#include <pthread.h>

void pthread_testcancel(void);

调用pthread_testcancel时,如果有某个和取消请求正处于未决状态,且取消没有置为无效,那么线程将被取消。但是如果取消被置为无效时,那么调用pthread_testcancel没有任何效果。

 

3).调用pthread_setcanceltype来改变取消类型

#include <pthread.h>

int pthread_setcanceltype(int type, int *oldtype);
//成功返回0,失败返回错误号。

type参数可以是PTHREAD_CANCEL_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS。

 

7.线程和信号

与前面的进程信号类似

1).pthread_sigmask函数可以阻止Signal的发送:

#include <siganl.h>

int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
//成功返回0,失败返回错误号。

 

2).调用sigwait函数可以等待signal的产生:

#include <signal.h>

int sigwait(const sigset_t *restrict set, int *restrict signop);
//成功返回0,失败返回错误号。

当signal是pending的情况下,调用sigwait会立刻返回并且把signal从pending list中移走,这样这个signal就不会被调用。为了避免这种行为,可以将pthread_sigmask和sigwait合用,首先用pthread_sigmask在signal产生之前阻止某个signal,然后用sigwait等待这个signal。Sigwait会自动Unblock这个signal,然后在等待结束之后恢复mask。

 

3).调用pthread_kill可以给一个线程发送signal:

#include <signal.h>

int pthread_kill(pthread_t thread, int signo);
//成功返回0,失败返回错误码。


 

8.线程和fork

当线程调用fork的时候,整个进程的地址空间都被copy(严格来说是copy-on-write)到子进程。所有互斥量Mutex / 读写锁Reader-Writer Lock / 信号量Condition Variable的状态都被继承下来。子进程中,只存在一个线程,就是当初调用fork的进程的拷贝。由于不是所有线程都被copy,因此需要将所有的同步对象的状态进行处理。(如果是调用exec函数的话没有这个问题,因为整个地址空间被丢弃了)处理的函数是pthread_atfork:

 

#include <pthread.h>

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
//成功返回0,失败返回错误号。

父进程和子进程最后解锁存储在不同内存位置的复制的锁,好像以下的事件序列发生一样:

1).父进程申请它所有的锁;

2).子进程申请它所有的锁;

3).父进程释放它的锁;

4).子进程释放它的锁。

 

posted on 2012-08-04 15:14  as_  阅读(905)  评论(0编辑  收藏  举报

导航