1、引言
2、线程限制
UNIX
操作系统对于线程操作有一些限制。如下图所示,可以通过sysconf
函数进行查询
- 下图给出了4种操作系统实现中线程限制的值。注意,表格中描述的
没有确定的限制
不代表无限制。
3、线程属性
3.1、线程属性概念
- 对于与线程相关的对象类型,一般都有一个属性类型与之关联(如线程和线程属性关联、互斥量和互斥量属性关联):
- 有一个初始化属性对象的函数,把属性设置为默认值
- 有一个销毁属性对象的函数,即释放属性对象资源
- 一个属性对象可以代表多个属性。属性对象对应用程序不透明,因此应用程序不需要了解属性对象结构实现细节,而是通过指定函数与之交互。
- 属性对象中的每个属性都有一个设置属性值的函数,还有一个获取属性值的函数
3.2、初始化和反初始化pthread_attr_t
- 在上一章中,通过
pthread_create
函数创建线程,其中pthread_attr_t
是线程属性对象。如果要设置线程为默认属性,则该参数设为NULL
。 - 可以通过
pthread_attr_t
结构修改线程属性,通过pthread_attr_init
函数初始化pthread_attr_t
为默认属性值。通过pthread_attr_destroy
函数销毁线程属性对象int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr);
- 线程属性包括以下几种(部分):
属性名称 说明 detachstate 线程的分离状态属性 guardsize 线程栈末尾的警戒缓冲区大小(字节) stackaddr 线程栈的最低地址 stacksize 线程栈的最小长度(字节数)
3.3、线程的分离状态属性
- 使用
pthread_attr_setdetachstate
函数设置线程属性对象的detachstate
属性:PTHREAD_CREATE_DETACHED
:以分离(detach
)状态启动线程PTHREAD_CREATE_JOINABLE
(默认):正常启动线程,应用程序可以获取线程的终止状态(通过pthread_join
函数)
- 通过
pthread_attr_getdetachstate
函数获取当前的detachstate
线程属性int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
- 实例:给出一个以分离状态创建线程的函数
#include "apue.h" #include <pthread.h> int makethread(void *(*fn)(void *), void *arg) { int err; pthread_t tid; pthread_attr_t attr; /*初始化attr为默认属性值*/ err = pthread_attr_init(&attr); if (err != 0) return(err); err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); if (err == 0) err = pthread_create(&tid, &attr, fn, arg); /*此例中忽略了pthread_attr_destroy函数的返回值。在这个实例中,我们对线程属性进行了合理的初始化, 因此pthread_attr_destroy应该不会失败。如果失败了,将难以清理,并可能造成少量的内存泄露*/ pthread_attr_destroy(&attr); return(err); }
3.4、线程栈属性(stackaddr stacksize)
- 可以通过
sysconf(_SC_THREAD_ATTR_STACKADDR)
和sysconf(_SC_THREAD_ATTR_STACKSIZE)
来检查系统对线程栈属性的支持情况。
1、stackaddr
- 使用函数
pthread_attr_getstack
和pthread_attr_setstack
对线程栈属性进行获取/设置(即获取/设置线程栈的最低地址和大小)int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr, size_t stacksize); int pthread_attr_getstack(const pthread_attr_t *attr,void **stackaddr, size_t *stacksize);
- 对于进程来说,虚拟地址空间大小是固定的(对于
32
位的操作系统,虚拟地址空间的大小为2^32B
即0
~4GB
的虚拟地址空间),因为进程中只有一个栈,所以它的大小通常不是问题。 - 但是对于线程来说,同样大小的虚拟地址空间必须被所有的线程栈共享。如果应用程序使用很多线程,以至于这些线程栈的累计大小超过了可用的虚拟地址空间,就需要减少默认的线程栈大小。
- 如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧(如递归),那么需要的栈大小可能要比默认的大。
- 如果线程栈的虚拟地址空间都用完了,可以使用
malloc
或mmap
来为可替代的栈分配空间,并用pthread_attr_setstack
函数来改变新建线程的栈位置。stackaddr
参数指定的地址用作线程栈的内存范围中最低可寻址地址,stacksize
为分配的缓冲区字节数。 - 注意,
stackaddr
线程属性被定义为栈的最低内存地址,但这并不一定是栈的开始位置:如果进程内存空间中栈是从高地址向低地址方向增长的,那么stackaddr
将是栈的结尾地址而非开始地址。
- 对于进程来说,虚拟地址空间大小是固定的(对于
2、stacksize
- 通过
pthread_attr_getstacksize
和pthread_attr_setstacksize
读取/设置线程属性stacksize
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
- 如果希望改变默认的栈大小,但是不想自己处理线程栈的分配问题(向
pthread_attr_setstack
那样自己设置线程栈空间),可以使用这个函数。 - 注意
stacksize
不能小于PTHREAD_STACK_MIN
限制
- 如果希望改变默认的栈大小,但是不想自己处理线程栈的分配问题(向
3.5、线程栈末尾的警戒缓冲区大小属性
guardsize
线程属性控制着线程栈末尾之后用以避免栈溢出的扩展内存大小。- 这个属性默认值由具体实现来定义,常用值时系统页大小(如
4KB
)。 - 可以将
guardsize
线程属性设置为0
,不允许属性的这种行为发生:此时不提供警戒缓冲区;同样,如果调用pthread_attr_setstack
修改线程stackaddr
属性,系统就认为我们自己管理栈,因此警戒缓冲区机制无效,等同于将guardsize
设为0
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize); int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);
- 注意,如果修改了
guardsize
线程属性,操作系统可能会把它取为页的整数倍大小。 - 如果线程的栈指针溢出到警戒缓冲区中,应用程序可能通过信号接收到出错信息。
- 注意,如果修改了
3.6、补充:栈空间
- 可以通过ulimit命令查看进程默认栈大小:可见栈默认大小是8MB
$ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 7721 max locked memory (kbytes, -l) 65536 max memory size (kbytes, -m) unlimited open files (-n) 65535 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 7721 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
- 所以如果我们用
alloca
函数在栈上申请一个很大的空间的话,应该就会发生栈越界等内存异常的程序崩溃现象。即如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow
)错误
- 所以如果我们用
4、同步属性
- 类似于线程属性,线程的同步对象也有属性。在第十一章中自旋锁有一个属性称为进程共享属性。本节讨论互斥量属性、读写锁属性、条件变量属性、屏障属性
4.1、互斥量属性
- 通过
pthread_mutexattr_t
结构体表示互斥量属性对象。 - 可以通过
pthread_mutexattr_init
初始化互斥量属性对象,通过pthread_mutexattr_destroy
反初始化互斥量属性对象。int pthread_mutexattr_init (pthread_mutexattr_t *__attr); int pthread_mutexattr_destroy (pthread_mutexattr_t *__attr);
pthread_mutexattr_init
将pthread_mutexattr_t
初始化为默认的互斥量属性。pthread_mutexattr_t
互斥量属性对象包含三个属性:- 进程共享属性
- 健壮属性
- 类型属性
1、互斥量属性:进程共享属性
- 使用
pthread_mutexattr_getpshared
和pthread_mutexattr_setpshared
函数获取/修改pthread_mutexattr_t
结构:int pthread_mutexattr_getpshared (const pthread_mutexattr_t *attr,int *__restrict __pshared); int pthread_mutexattr_setpshared (pthread_mutexattr_t *__attr,int __pshared);
PTHREAD_PROCESS_PRIVATE
(默认):该进程中的线程可以访问该互斥量对象PTHREAD_PROCESS_SHARED
:允许相互独立的多个进程把同一个内存数据块映射到它们各自的地址空间中,即从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步(该互斥量可能位于在多个进程之间共享的共享内存对象中)。
2、互斥量属性:健壮属性
- 与在多个进程间共享的互斥量有关。
- 当持有互斥量的进程终止(即对互斥量加锁的进程终止)时,需要解决互斥量状态恢复的问题。这种情况下其他阻塞在这个锁的进程将会一致阻塞下去。
- 通过
pthread_mutexattr_getrobust
和pthread_mutexattr_setrobust
获取/设置健壮属性值int pthread_mutexattr_getrobust (const pthread_mutexattr_t *__attr,int *__robustness); int pthread_mutexattr_setrobust (pthread_mutexattr_t *__attr,int __robustness)
PTHREAD_MUTEX_STALLED
(默认):- 持有互斥量的进程终止时不需要采取特别的动作。这种情况下,等待该互斥量解锁的进程会被阻塞下去。
PTHREAD_MUTEX_ROBUST
:- 当拥有这个锁的线程挂了后,下一个尝试去获得锁的线程会获得该锁并返回
EOWNWERDEAD
值,新的拥有者应该再去调用pthread_mutex_consistent
来保持锁状态的一致性,并解锁。否则当这个锁解锁以后,该互斥量就不再可用,其他试图获得该互斥量的线程就不能拿到该锁并返回ENOTRECOVERABLE
。int pthread_mutex_consistent(pthread_mutex_t *mutex);
- 如果一个健壮的互斥量处于不一致的状态,这个函数会使它保持一致。如果互斥锁的所有者在持有互斥锁时终止,则互斥锁可能会处于不一致状态,在这种情况下,获取互斥锁的下一个所有者将成功并通过调用
pthread_mutex_lock()
的EOWNERDEAD
返回值通知。
- 如果一个健壮的互斥量处于不一致的状态,这个函数会使它保持一致。如果互斥锁的所有者在持有互斥锁时终止,则互斥锁可能会处于不一致状态,在这种情况下,获取互斥锁的下一个所有者将成功并通过调用
- 当拥有这个锁的线程挂了后,下一个尝试去获得锁的线程会获得该锁并返回
3、互斥量属性:类型属性
-
通过
pthread_mutexattr_gettype
和pthread_mutexattr_settype
获取/修改互斥量类型属性int pthread_mutexattr_settype (pthread_mutexattr_t *__attr, int __kind); int pthread_mutexattr_gettype (const pthread_mutexattr_t * __attr, int * __kind)
-
类型属性表现了互斥量的锁定特性
PTHREAD_MUTEX_NORMAL
:- 标准互斥量类型,不做任何特殊的错误检查或死锁检测。
PTHREAD_MUTEX_ERRORCHECK
:- 此互斥量类型提供错误检查
PTHREAD_MUTEX_RECURSIVE
:- 此互斥量类型允许同一线程在互斥量解锁之前多次加锁(即递归互斥量)。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。
PTHREAD_MUTEX_DEFAULT
:- 此互斥量类型提供默认特性和行为。Linux操作系统把这种类型映射为
PTHREAD_MUTEX_NORMAL
。
- 此互斥量类型提供默认特性和行为。Linux操作系统把这种类型映射为
-
以上几种类型互斥量的行为:
4、实例
- 实例:展示递归互斥量解决并发问题的情况
- 假设
func1
和func2
函数的接口不能改变。我们只能把互斥量嵌入式到数据结构中,把这个数据结构的地址(x)作为参数传入。 - 如果
func1
和func2
函数都必须操作这个结构,而且可能会有一个以上的线程同时访问该数据结构,那么func1
和func2
必须在操作数据以前对互斥量加锁,并且该互斥量应该是递归类型的,否则会出现死锁。
- 假设
- 实例:展示了使用递归互斥量的一种替代方法
- 通过提供
func2
函数的私有版本,称之为func2_locked
函数,可以保持func1
和func2
函数接口不变,而且避免使用递归互斥量。 - 要调用
func2_locked
函数,必须占有嵌入在数据结构中的互斥量,这个数据结构的地址是作为参数传入的。func2_locked
的函数体包括func2
的副本,func2
现在只是获取互斥量,调用func2_locked
,然后释放互斥量。 - 提供加锁和不加锁版本的函数,在简单的情况下可行,但在更加复杂的情况下就不得不依赖递归锁。比如:库需要调用库以外的函数,而且可能会再次毁掉库中的函数时。
- 通过提供
- 实例:解释了有必要使用递归互斥量的另一种情况。
#include "apue.h" #include <pthread.h> #include <time.h> #include <sys/time.h> extern int makethread(void *(*)(void *), void *); struct to_info { void (*to_fn)(void *); /* function */ void *to_arg; /* argument */ struct timespec to_wait; /* time to wait */ }; #define SECTONSEC 1000000000 /* seconds to nanoseconds */ #if !defined(CLOCK_REALTIME) || defined(BSD) #define clock_nanosleep(ID, FL, REQ, REM) nanosleep((REQ), (REM)) #endif #ifndef CLOCK_REALTIME #define CLOCK_REALTIME 0 #define USECTONSEC 1000 /* microseconds to nanoseconds */ void clock_gettime(int id, struct timespec *tsp) { struct timeval tv; gettimeofday(&tv, NULL); tsp->tv_sec = tv.tv_sec; tsp->tv_nsec = tv.tv_usec * USECTONSEC; } #endif void * timeout_helper(void *arg) { struct to_info *tip; tip = (struct to_info *)arg; /*线程在时间未到时将一直等待,时间到了以后再调用请求的函数*/ clock_nanosleep(CLOCK_REALTIME, 0, &tip->to_wait, NULL); (*tip->to_fn)(tip->to_arg); free(arg); return(0); } /*该函数允许安排另一个函数在未来的某个时间运行*/ void timeout(const struct timespec *when, void (*func)(void *), void *arg) { struct timespec now; struct to_info *tip; int err; clock_gettime(CLOCK_REALTIME, &now); if ((when->tv_sec > now.tv_sec) || (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec)) { tip = malloc(sizeof(struct to_info)); if (tip != NULL) { tip->to_fn = func; tip->to_arg = arg; tip->to_wait.tv_sec = when->tv_sec - now.tv_sec; if (when->tv_nsec >= now.tv_nsec) { tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec; } else { tip->to_wait.tv_sec--; tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec + when->tv_nsec; } /*我们使用makethread函数以分离状态创建线程。 因为传递给timeout函数的func函数参数在未来运行, 所以我们不希望一直空等待线程结束。*/ err = makethread(timeout_helper, (void *)tip); if (err == 0) return; else free(tip); } } /* * We get here if (a) when <= now, or (b) malloc fails, or * (c) we can't make a thread, so we just call the function now. */ (*func)(arg); } pthread_mutexattr_t attr; pthread_mutex_t mutex; /*需要把retry函数安排为原子操作,retry函数试图对同一个互斥量进行加锁,所以互斥量必须是递归的*/ void retry(void *arg) { pthread_mutex_lock(&mutex); /* perform retry steps ... */ pthread_mutex_unlock(&mutex); } int main(void) { int err, condition, arg; struct timespec when; /*初始化互斥量属性对象*/ if ((err = pthread_mutexattr_init(&attr)) != 0) err_exit(err, "pthread_mutexattr_init failed"); /*修改互斥量类型属性为PTHREAD_MUTEX_RECURSIVE, 允许同一线程在互斥量解锁之前多次加锁(即递归互斥量)*/ if ((err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) != 0) err_exit(err, "can't set recursive type"); /*使用属性attr初始化互斥量mutex*/ if ((err = pthread_mutex_init(&mutex, &attr)) != 0) err_exit(err, "can't create recursive mutex"); /* continue processing ... */ /*timeout的调用者需要占有互斥量来检查条件,所以对互斥量mutex进行加锁*/ pthread_mutex_lock(&mutex); /* * Check the condition under the protection of a lock to * make the check and the call to timeout atomic. */ if (condition) { /* * Calculate the absolute time when we want to retry. */ clock_gettime(CLOCK_REALTIME, &when); when.tv_sec += 10; /* 10 seconds from now */ timeout(&when, retry, (void *)((unsigned long)arg)); } pthread_mutex_unlock(&mutex); /* continue processing ... */ exit(0); }
4.2、读写锁互斥量
- 通过
pthread_rwlockattr_t
表示读写锁互斥量属性对象。- 通过
pthread_rwlockattr_init
和pthread_rwlock_desdroy
初始化/反初始化读写锁互斥量属性对象。int pthread_rwlockattr_init (pthread_rwlockattr_t *__attr); int pthread_rwlockattr_destroy (pthread_rwlockattr_t *__attr);
- 通过
- 读写锁唯一支持的属性是进程共享属性。它的含义与互斥量的进程共享属性相同。
- 可以通过一组函数设置/获取读写锁的进程共享属性
int pthread_rwlockattr_getpshared (const pthread_rwlockattr_t *__attr,int * __pshared); int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *__attr,int __pshared)
PTHREAD_PROCESS_SHARED
PTHREAD_PROCESS_PRIVATE
- 可以通过一组函数设置/获取读写锁的进程共享属性
4.3、条件变量属性
- 通过
pthread_condattr_t
类型表示条件变量属性对象。- 通过一对函数初始化/反初始化条件变量属性对象。
int pthread_condattr_init (pthread_condattr_t *__attr); int pthread_condattr_destroy (pthread_condattr_t *__attr);
- 通过一对函数初始化/反初始化条件变量属性对象。
- 条件变量属性有两种:
- 进程共享属性
- 时钟属性
1、进程共享属性
- 它控制着条件变量可以被单进程的多个线程使用,还是可以被多进程的线程使用(与上文中其他的线程同步对象属性类似)。可以通过一组函数获取/设置条件变量的进程共享属性值。
int pthread_condattr_getpshared (const pthread_condattr_t *__attr,int * __pshared); int pthread_condattr_setpshared (pthread_condattr_t *__attr,int __pshared)
PTHREAD_PROCESS_SHARED
PTHREAD_PROCESS_PRIVATE
2、时钟属性
- 该属性只针对
pthread_cond_timedwait
函数有用,表示其超时参数采用的是哪个时钟(clockid_t
类型),其时钟类型可以是以下类型:CLOCK_REALTIME
:系统实时时间,即从1970年开始的时间CLOCK_MONOTONIC
:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响CLOCK_PROCESS_CPUTIME_ID
:本进程到当前代码的CPU时间CLOCK_THREAD_CPUTIME_ID
:本线程到当前代码的CPU时间
- 同样可以通过一组函数获取/设置条件变量的时钟属性
int pthread_condattr_getclock (const pthread_condattr_t *__attr,clockid_t * __clock_id); int pthread_condattr_setclock (pthread_condattr_t *__attr,clockid_t __clock_id)
- 注意,并没有为其他有超时等待函数的属性对象定义时钟属性。
4.4、屏障属性
- 通过
pthread_barrierattr_t
表示屏障属性对象- 通过一组函数初始化/反初始化屏障属性对象
int pthread_barrierattr_init (pthread_barrierattr_t *__attr); int pthread_barrierattr_destroy (pthread_barrierattr_t *__attr);
- 通过一组函数初始化/反初始化屏障属性对象
- 屏障属性对象只有进程共享属性,它控制着屏障可以被多进程的线程属性,还是只能被初始化屏障的进程内多线程使用。
- 通过一组函数获取/设置屏障的进程共享属性
int pthread_barrierattr_getpshared (const pthread_barrierattr_t *__attr,int *__restrict __pshared); int pthread_barrierattr_setpshared (pthread_barrierattr_t *__attr,int __pshared);
PTHREAD_PROCESS_SHARED
PTHREAD_PROCESS_PRIVATE
- 通过一组函数获取/设置屏障的进程共享属性
5、重入(线程安全函数)
-
多个控制线程在相同的时间有可能调用相同的函数,这种情况即为重入。
-
如果一个函数在相同的时间点可以被多个线程安全的调用,就称该函数是线程安全的。除了下图列出的函数,其他函数都是线程安全的。
-
对于一些非线程安全函数,会提供可替代的线程安全版本。这些函数的命名方式与它们的非线程安全版本的名字相似,只在最后加了
_r
,表明这些版本是可重入的。 -
很多函数不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中(如静态局部变量)。在
_r
函数中,通过修改接口,要求调用者自己提供缓冲区使函数变为线程安全的。
-
注意,如果一个函数对多个线程来说是可重入的,就说明这个函数是线程安全的。但是不能说明对信号处理程序来说该函数也是可重入的。
-
如果函数对异步信号处理程序的重入是安全的,那么就说函数是异步信号安全的(图10-4介绍的就是异步信号安全函数)
-
提供了以线程安全的方式管理
FILE
对象的方法,通过flockfile
和ftrylockfile
获取与FILE
对象关联的锁,并且该锁是递归的。(规定所有操作FILE
对象的标准I/O
库函数的动作行为看起来就像是内部调用了flockfile
和funlockfile
)void flockfile(FILE *filehandle); int ftrylockfile(FILE *filehandle); void funlockfile(FILE *filehandle);
-
根据上文规定,标准I/O库在操作FILE对象时都会获取对应的锁,这就导致在做一次一个字符的I/O时会出现严重的性能下降(因为每次都需要获取锁和释放锁)。因此提供了不加锁版本的基于字符的标准I/O函数。
int getc_unlocked(FILE *stream); int getchar_unlocked(void); int putc_unlocked(int c, FILE *stream); int putchar_unlocked(int c);
-
注意,尽量不要使用这几个函数,它们可能由于多个线程非同步访问是数据而引起种种问题(因为没有获得锁,因此不是互斥的访问数据)。
-
实例:展示了
getenv
(见7.9节)的一个可能实现,注意这个版本不是可重入的。#include <limits.h> #include <string.h> #define MAXSTRINGSZ 4096 static char envbuf[MAXSTRINGSZ]; extern char **environ; char * getenv(const char *name) { int i, len; len = strlen(name); for (i = 0; environ[i] != NULL; i++) { if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) { strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ-1); return(envbuf); } } return(NULL); }
- 如果两个线程同时调用这个函数,会看到不一致的结果,因为所有调用
getenv
的线程返回的字符串都存储在同一个静态缓冲区中。
- 如果两个线程同时调用这个函数,会看到不一致的结果,因为所有调用
-
实例:给出了
getenv
的可重入版本,即getenv_r
。它使用pthread_once
函数来确保不管多少线程同时竞争getenv_r
,每个进程只调用thread_init
函数一次。(下一节会详细讲述pthread_once
函数)#include <string.h> #include <errno.h> #include <pthread.h> #include <stdlib.h> extern char **environ; pthread_mutex_t env_mutex; static pthread_once_t init_done = PTHREAD_ONCE_INIT; static void thread_init(void) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&env_mutex, &attr); pthread_mutexattr_destroy(&attr); } /*要使getenv_r可重入,需要改变接口,调用者必须提供自己的缓冲区, 这样每个线程可以使用各自不同的缓冲区避免其他线程的干扰*/ int getenv_r(const char *name, char *buf, int buflen) { int i, len, olen; pthread_once(&init_done, thread_init); len = strlen(name); /*需要在搜索请求的字符时保护环境不被修改,所以需要对互斥量加锁*/ pthread_mutex_lock(&env_mutex); for (i = 0; environ[i] != NULL; i++) { if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) { olen = strlen(&environ[i][len+1]); if (olen >= buflen) { pthread_mutex_unlock(&env_mutex); return(ENOSPC); } strcpy(buf, &environ[i][len+1]); pthread_mutex_unlock(&env_mutex); return(0); } } pthread_mutex_unlock(&env_mutex); return(ENOENT); }
- 可以使用读写锁,从而允许对
getenv_t
进行多次并发访问,但增加的并发性可能并不会在很大程度上改善程序的性能,因为:- 环境列表通常不会很长,所以扫描列表时并不需要长时间占有互斥量
- 对
getenv
和putenv
的调用不是频繁发生的。
- 即使可以把
getenv_r
变成线程安全的,这也不意味着它对信号处理程序是可重入的。- 如果使用的是非递归的互斥量,线程从信号处理程序中调用
getenv_r
就可能出现死锁。 - 如果信号处理程序在线程执行
getenv_r
时中断了该线程,这时我们已经占有加锁的env_mutex
,这样其他线程试图对这个互斥量的加锁就会被阻塞,最终导致线程进入死锁状态。 - 结合上面两点,我们必须使用递归互斥量来阻止其他线程改变我们正需要的数据结构,还要阻止来自信号处理程序的死锁。
- 如果使用的是非递归的互斥量,线程从信号处理程序中调用
- 注意:
pthread
函数并不保证是异步信号安全的,所以不能把pthread
函数用于其他函数,让该函数成为异步信号安全。
- 可以使用读写锁,从而允许对
6、线程特定数据
-
线程特定数据也称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。我们希望每个线程可以访问它自己单独的数据副本,而不需要担心与其他线程同步访问的问题。
-
例如
errno
就是线程私有数据,每个线程都拥有它自己单独的errno
数据副本。一个线程重置了errno
的操作也不会影响进程中其他线程的errno
值。 -
一个进程中的所有线程都可以访问这个进程的整个地址空间,除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。线程私有数据也不例外,虽然底层的实现不能阻止这种访问能力,但是管理线程私有数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。(即通过管理线程私有数据的函数与线程私有数据进行交互,而并非直接通过内存地址存取)。
-
在分配线程私有数据之前,需要通过
pthread_key_create
函数创建与该数据关联的键,这个键用于后续对线程私有数据的访问。int pthread_key_create (pthread_key_t *key,void (*destructor) (void *))
-
其中
pthread_key_t
代表一个键,这个键可以被进程中的所有线程使用,但是每个线程把这个键与不同的线程私有数据地址进行关联。创建新键时,每个线程的数据地址设为空值。 -
destructor
参数是与该键关联的析构函数(可以设为NULL
)。当这个线程退出时,如果数据地址已经被置为非空,那么析构函数就会被调用,它唯一的参数就是该线程私有数据地址(通常通过malloc
为线程私有数据分配内存,如果析构函数不释放会导致内存泄漏)。- 当线程调用
pthread_exit
或者线程return
返回,析构函数会被调用;当线程被pthread_cancel
取消时,在最后的清理处理程序返回之后,析构函数被调用。 - 如果线程调用了
exit
、_exit
、_Exit
、abort
或出现其他非正常的退出时,析构函数不会被调用。 - 当所有的析构函数都被调用完成以后,系统会检查是否还有非空的线程私有数据值与键关联,如果有的话,再次调用析构函数。这个过程将一直重复直到超过
PTHREAD_DESTRUCTOR_ITERATIONS
中定义的最大次数限制。
- 当线程调用
-
对于所有的线程,通过pthread_key_delete取消键和线程特定数据值之间的关联
int pthread_key_delete (pthread_key_t key)
- 注意该函数并不会调用与键关联的析构函数。
-
pthread_once
函数pthread_once_t
必须是非本地变量(全局变量或静态变量),并且必须初始化PTHREAD_ONCE_INIT
pthread_once_t initflag = PTHREAD_ONCE_INIT;
- 如果每个线程都调用
pthread_once
,系统保证__init_routine
函数只被调用一次,即系统首次调用pthread_once
时。int pthread_once (pthread_once_t *__once_control, void (*__init_routine) (void));
- 例如有多个线程都要创建与线程私有数据关联的键时,就可以使用
pthread_once
来保证该键只被pthread_key_create
了一次。如
-
键一旦创建以后,通过
pthread_setspecific
设置线程私有数据(线程私有数据本质上是一个指向一块内存的指针,或者是一个类型转换为void*
的数值):参数__pointer
通常指向由调用者分配的一块内存,当线程终止时,会将该指针作为参数传递给与key
相关联的destructor
函数 -
通过
pthread_getspecific
获取线程私有数据。在使用返回的值前最好是将void*
转换成原始数据类型的指针,或者转型为原始类型。void *pthread_getspecific (pthread_key_t __key); int pthread_setspecific (pthread_key_t __key,const void *__pointer);
-
实例:
getenv
的另一个实现版本:使用线程特定数据来维护每个线程的数据缓冲区副本。#include <limits.h> #include <string.h> #include <pthread.h> #include <stdlib.h> #define MAXSTRINGSZ 4096 static pthread_key_t key; static pthread_once_t init_done = PTHREAD_ONCE_INIT; pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER; extern char **environ; static void thread_init(void) { /*对析构函数,使用free来释放之前有malloc分配的内存。 只有当线程特定数据值为非空时,析构函数才会被调用*/ pthread_key_create(&key, free); } char * getenv(const char *name) { int i, len; char *envbuf; /*使用pthread_once确保只为我们将使用的线程特定数据创建一个键*/ pthread_once(&init_done, thread_init); pthread_mutex_lock(&env_mutex); /*如果pthread_getspecific返回的是NULL,就需要先分配内存缓冲区,然后再把键与该内存缓冲区关联。 否则,如果返回的不是空指针,就使用pthread_getspecific返回的内存缓冲区*/ envbuf = (char *)pthread_getspecific(key); if (envbuf == NULL) { envbuf = malloc(MAXSTRINGSZ); if (envbuf == NULL) { pthread_mutex_unlock(&env_mutex); return(NULL); } pthread_setspecific(key, envbuf); } len = strlen(name); for (i = 0; environ[i] != NULL; i++) { if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) { strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ-1); pthread_mutex_unlock(&env_mutex); return(envbuf); } } pthread_mutex_unlock(&env_mutex); return(NULL); }
- 注意:虽然这个版本的
getenv
是线程安全的,但它并不是异步信号安全的。对信号处理程序而言,即使使用递归的互斥量,这个版本的getenv
也不可能是可重入的,因为它调用了malloc
,而malloc
函数本身并不是异步信号安全的。
- 注意:虽然这个版本的
7、取消选项
- 有两个线程属性没有包含在
pthread_attr_t
结构中:可取消状态和可取消类型。这两个属性影响着线程响应pthread_cancel
函数调用时所呈现的行为。 - 可取消状态属性
- 通过
pthread_setcancelstate
修改它的可取消状态,并获取原来的可取消状态。int pthread_setcancelstate (int __state, int *__oldstate);
PTHREAD_CANCEL_ENABLE
(默认)PTHREAD_CANCEL_DISABLE
- 通过
- 取消点
-
pthread_cancel
调用不等待线程终止。默认情况下,被请求取消的线程在取消请求发出后继续运行,直到线程到达某个取消点。 -
取消点是线程检查它是否被取消的一个位置,如果取消了则按照请求行事。
-
当可取消状态为
PTHREAD_CANCEL_DISABLE
时,pthread_cancel
并不会杀死指定线程。相反,取消请求对这个线程来说处于挂起状态,当可取消状态再次变为PTHREAD_CANCEL_ENABLE
时,线程将在下一个取消点上对所有挂起的取消请求再次进行处理。 -
如果应用程序在很长一段时间都不会调用取消点函数,那么可以调用
pthread_testcancel
手动增加取消点void pthread_testcancel (void);
-
- 可取消类型属性
- 通过
pthread_setcanceltype
修改可取消类型属性int pthread_setcanceltype (int __type, int *__oldtype);
PTHREAD_CANCEL_DEFERRED
(默认):推迟取消。调用pthread_cancel之后,在线程到达取消点之前并不会出现真正的取消。PTHREAD_CANCEL_ASYNCHRONOUS
:异步取消。线程可以在任意时间取消,不是非要遇到取消点才能被取消。
- 通过
8、线程和信号
- 每个线程都有自己的信号屏蔽字,但是信号处理动作是所有线程共享的。这意味着单个线程可以阻止某些信号,但是当某个线程修改了与某个给定信号相关的处理行为以后,所有线程都共享这个处理行为的改变。
- 进程中的信号是递送到单个线程的:如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。
pthread_sigmask
- 第10章中介绍了
sigprocmask
函数设置信号屏蔽字,但是在多线程的进程中该函数行为没有定义。因此在多线程进程中必须使用pthread_sigmask
函数设置属于该线程的信号屏蔽字。int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
-
set
参数:- 线程用于修改信号屏蔽字的信号集
-
oset
参数:- 如果非空,则返回之前的线程信号屏蔽字
-
how
参数:SIG_BLOCK
:把set信号集添加到信号屏蔽字中SIG_SETMASK
:用set信号集设置为信号屏蔽字SIG_UNBLOCK
:把set信号集从信号屏蔽字中移除。
-
- 第10章中介绍了
sigwait
函数- 线程可以通过
sigwait
函数等待一个或多个信号的出现int sigwait(const sigset_t *set, int *sig);
set
参数:线程等待的信号集(在调用sigwait
之前必须阻塞这些信号)sig
参数:包含发送信号的数量
sigwait
函数一直阻塞直到set
指定的任何一个信号被挂起为止,并从进程中移除该挂起信号。如果支持排队信号,并且信号的多个实例被挂起,那么sigwait
将会移除该信号中的一个实例,其他的继续排队。- 为了避免错误,线程调用
sigwait
之前,必须阻塞那些它正在等待的信号。 - 如果多个线程在
sigwait
调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从sigwait
返回。 - 如果一个信号被捕获(如使用
sigaction
建立信号处理程序),而一个线程正在sigwait
调用中等待同一信号,那么这时将由操作系统决定以何种方式递送信号。可以让sigwait
返回,也可以激活信号处理程序,二者只能选其一。 - 使用
sigwait
函数的好处:- 可以简化信号处理,把异步产生的信号用同步的方式处理。
- 为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中,然后可以安排专用线程通过
sigwait
函数等待处理指定信号,这样使得信号不会中断其他线程,只会在指定线程中通过sigwait
函数返回。
- 线程可以通过
- 通过
pthread_kill
将信号发送给指定线程int pthread_kill (pthread_t __threadid, int __signo);
- 注意,闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以,进程中的多个线程不可能互不干扰地使用闹钟定时器
- 实例:在线程中,使用互斥量来保护标志(注意与
10.16
小节的内容进行对比)#include "apue.h" #include <pthread.h> int quitflag; /* set nonzero by thread */ sigset_t mask; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER; void * thr_fn(void *arg) { int err, signo; for (;;) { err = sigwait(&mask, &signo); if (err != 0) err_exit(err, "sigwait failed"); switch (signo) { case SIGINT: printf("\ninterrupt\n"); break; case SIGQUIT: /*这里加锁不会产生死锁吗?当然不会,请复习条件变量的相关内容*/ pthread_mutex_lock(&lock); quitflag = 1; pthread_mutex_unlock(&lock); pthread_cond_signal(&waitloc); return(0); default: printf("unexpected signal %d\n", signo); exit(1); } } } int main(void) { int err; sigset_t oldmask; pthread_t tid; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0) err_exit(err, "SIG_BLOCK error"); err = pthread_create(&tid, NULL, thr_fn, 0); if (err != 0) err_exit(err, "can't create thread"); pthread_mutex_lock(&lock); while (quitflag == 0) /*条件变量*/ pthread_cond_wait(&waitloc, &lock); pthread_mutex_unlock(&lock); /* SIGQUIT has been caught and is now blocked; do whatever */ quitflag = 0; /* reset signal mask which unblocks SIGQUIT */ if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) err_sys("SIG_SETMASK error"); exit(0); }
-
下面是命令行输出结果
lh@LH_LINUX:~/桌面/apue.3e/threadctl$ ./suspend ^C interrupt ^C interrupt ^C interrupt ^Z [3]+ 已停止 ./suspend
-
在主线程开始时阻塞
SIGINT
和SIGQUIT
。当创建线程进行信号处理时,新建线程继承了现有的信号屏蔽字。因为sigwait
会解除信号的阻塞状态,所以只有一个线程可以用于信号的接收,这使得我们对主线程进行编码时不用担心来自这些信号的中断。 -
本例不用依赖信号处理程序中断主控线程,有专门的独立控制线程进行信号处理。在互斥量的保护下改动
quitflag
的值,这样主控线程不会在调用pthread_cond_signal
时错失唤醒调用。
-
9、线程和fork
-
当线程调用
fork
时,就为子进程创建了整个进程地址空间的副本。 -
子进程通过继承整个地址空间的副本,还从父进程那里继承了每个互斥量、读写锁、条件变量的状态。如果父进程包含一个以上的线程,子进程在
fork
返回之后,如果不紧接着马上调用exec
,就要清理锁状态。 -
注意,
fork
的子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程将同样占有这些锁。但是子进程并不包含其他占有锁的线程的副本,所以子进程不知道它占有了哪些锁,需要释放哪些锁。 -
在多线程进程中,为了避免不一致状态的问题,
fork
返回和子进程调用exec
函数之间,子进程只能调用异步信号安全的函数。 -
清除锁状态
要清除锁状态,可以通过pthread_atfork
函数建立fork
处理程序int pthread_atfork (void (*prepare) (void), void (*parent) (void),void (*child) (void));
- prepare:
- 由父进程在
fork
创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁(加锁)
- 由父进程在
- parent:
fork
创建子进程之后、返回之前在父进程上下文中调用。这个fork
处理程序的任务是对prepare
程序获取的所有锁进行解锁。
- child:
fork
返回之前在子进程上下文中调用。这个fork
处理程序释放(解锁)prepare
中获取的所有锁。
- 该函数使得就好像出现了下列事件序列:
- 父进程获取所有锁
- 子进程获取所有锁
- 父进程释放它的锁
- 子进程释放它的锁
- 注意,
parent
和child
程序以它们注册时的顺序调用;prepare
程序调用顺序则与注册顺序相反。这样可以允许多个模块注册它们自己的fork
处理程序,而且保持锁的层次。 - 假如模块A和模块B都有自己的一套锁,并且A的层次在B之前(比如模块A调用模块B)。那么就需要模块B在模块A前调用
pthread_atfork
函数,然后事件序列如下:- 调用模块A的
prepare fork
处理程序获取模块A的所有锁。 - 调用模块B的
prepare fork
处理程序获取模块B的所有锁。 - 创建子进程。
- 调用模块B的
child fork
处理程序释放子进程中模块B的所有锁。 - 调用模块A的
child fork
处理程序释放子进程中模块A的所有锁。 fork
函数返回到子进程。- 调用模块B的
parentfork
处理程序释放父进程中模块B的所有锁。 - 调用模块A的
parentfork
处理程序释放父进程中模块A的所有锁。 fork
函数返回到父进程。
- 调用模块A的
- prepare:
-
实例:描述如何使用
pthread_atfork
和fork
处理程序。#include "apue.h" #include <pthread.h> pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER; void prepare(void) { int err; printf("preparing locks...\n"); if ((err = pthread_mutex_lock(&lock1)) != 0) err_cont(err, "can't lock lock1 in prepare handler"); if ((err = pthread_mutex_lock(&lock2)) != 0) err_cont(err, "can't lock lock2 in prepare handler"); } void parent(void) { int err; printf("parent unlocking locks...\n"); if ((err = pthread_mutex_unlock(&lock1)) != 0) err_cont(err, "can't unlock lock1 in parent handler"); if ((err = pthread_mutex_unlock(&lock2)) != 0) err_cont(err, "can't unlock lock2 in parent handler"); } void child(void) { int err; printf("child unlocking locks...\n"); if ((err = pthread_mutex_unlock(&lock1)) != 0) err_cont(err, "can't unlock lock1 in child handler"); if ((err = pthread_mutex_unlock(&lock2)) != 0) err_cont(err, "can't unlock lock2 in child handler"); } void * thr_fn(void *arg) { printf("thread started...\n"); pause(); return(0); } int main(void) { int err; pid_t pid; pthread_t tid; if ((err = pthread_atfork(prepare, parent, child)) != 0) err_exit(err, "can't install fork handlers"); if ((err = pthread_create(&tid, NULL, thr_fn, 0)) != 0) err_exit(err, "can't create thread"); sleep(2); printf("parent about to fork...\n"); if ((pid = fork()) < 0) err_quit("fork failed"); else if (pid == 0) /* child */ printf("child returned from fork\n"); else /* parent */ printf("parent returned from fork\n"); exit(0); }
- 该程序定义了两个互斥量,
lock1
和lock2
,prepare fork
处理程序获取这两把锁,child fork
处理程序在子进程上下文中释放它们,parent fork
处理程序在子进程上下文中释放它们。 - 运行程序,命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/threadctl$ ./atfork thread started... parent about to fork... preparing locks... parent unlocking locks... parent returned from fork child unlocking locks... child returned from fork
- 该程序定义了两个互斥量,
-
pthread_atfork
函数限制:- 没有很好的方法对比较复杂的同步变量(如条件变量或屏障等)进行状态的重新初始化
- 对于错误检查类型互斥量,在
child
处理程序试图对父进程加锁的互斥量进行解锁时可能产生错误。 - 不能在
child
中清理递归互斥量,因为不知道互斥量被加锁次数 - 如果子进程只允许调用异步信号安全函数,则
child
处理程序就不能清理同步对象,因为用于操作清理的所有函数都不是异步信号安全的。
10、线程和I/O
pread
和pwrite
函数在多线程环境下是非常有用的,因为进程中所有的线程共享相同的文件描述符。- 考虑两个线程,在同一时间对同一个文件描述符进行读写操作,显然这样不安全。
- 为了解决这个问题,我们可以使用
pread
使偏移量的设定和数据的读取成为一个原子操作。