APUE第12章 线程控制

线程的限制

sysconf可以获取例如最大线程数、一个线程的最小栈大小等。

线程属性

1
2
3
4
5
6
7
8
9
10
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
//成功返回0,否则返回错误编号

//detach分离线程
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int *detachstate);
//成功返回0,否则返回错误码
//PTHREAD_CREATE_DETACHED或PTHREAD_CREATE_JOINABLE

线程同步

互斥量属性

1
2
3
4
5
6
7
8
9
10
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr, int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

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

  1. 进程共享属性默认是PTHREAD_PROCESS_PRIVATE,允许线程库提供更为有效的互斥量实现。还有的可选值是PTHREAD_PROCESS_SHARED,从多个进程进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步。
  2. 健壮属性决定当持有互斥量的进程结束后互斥量的恢复动作。默认是PTHREAD_MUTEX_STALLED,另一个是PTHREAD_MUTEX_ROBUST。如果设置为PTHREAD_MUTEX_ROBUST的话,这将会导致线程结束时调用pthread_mutex_lock获取锁,从而阻塞。
  3. 类型属性:PTHREAD_MUTEX_NORAMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD_MUTEX_DEFAULT。

Note: 线程可以调用pthread_mutex_consistent函数,指明与该互斥量相关的状态在互斥量解锁之前是一致的。

读写锁属性

读写锁唯一支持的属性是进程共享属性,函数如下。

1
2
3
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(const pthread_rwlockattr_t *restrict attr, int pshared);

条件变量属性和屏障属性

  1. 条件变量支持进程共享属性和时钟属性
  2. 屏障变量支持进程共享属性

重入

如果一个函数在相同的时间点可以别多个线程安全地调用,那么这个函数就是线程安全的。支持线程安全函数的操作系统实现会在

1
2
3
4
5
6
7
8
9

## 线程特定数据
同时称为线程私有属性,是存储和查询某个特定线程相关数据的一种机制。
```cpp
#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
//若成功返回0,否则返回错误编号
int pthread_key_delete(pthread_key_t key);
//若成功返回0,否则返回错误编号

前者用于创建和数据的关联的键,后者用于断开关联,但是不会调用对应的析构函数

1
2
3
4
5
int pthread_once(pthread_once_t *initflag, void(*initfn)(void));
//键创建后,就可以通过pthread_setspecific函数把键和线程特定数据关联起来。
//可以通过pthread_getspecific函数活的线程特定数据的地址。
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);

总结:就是创建了一个可存储任意结构的变量,但是在每个线程中的值都是这些线程特有的。

取消选项

可取消状态和可取消类型

它们并不包含在pthread_attr_t结构中。

可取消状态

可以是PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE。可以用函数pthread_setcancelstate函数修改。

1
2
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);//这是一个原子操作

默认状态是PTHREAD_CANCEL_ENABLE,当设置为PTHREAD_CANCEL_DISABLE的时候,pthread_cancel并不会杀死线程,反而这个取消请求会被挂起,当线程重新变为PTHREAD_CANCEL_ENABLE的时候,线程会在下一个取消点对取消请求进行处理。

可取消类型

一般默认的取消类型叫推迟徐晓。即调用了pthread_cancel后,在线程到达取消点之前并不会真正的取消。可以通过pthread_setcanceltype来修改取消类型。

1
2
3
4
5
6
#include <pthread.h>
//可以是PTHREAD_CANCEL_DEFERRED或者PTHREAD_CANCEL_ASYNCHRONOUS
int pthread_setcanceltype(int type, int *oldtype);

//添加自己的取消点
void pthread_testcancel(void);

线程和信号

每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有的线程共享的。所以如果一个线程忽略某个特定信号,那么其他所有线程都得共享这个改变,除非重置了这个信号的处理函数。

可能用到的函数见《APUE》P364-365

线程fork

问题背景

如果某个线程执行了fork,那会怎么样呢?答案是子进程会复制父进程,包括每个互斥量、读写锁和条件变量。但是在子进程中只存在一个线程,他是调用fork的那个线程的副本,所以除非fork完立即调用exec(这样会抛弃就得地址空间),不然都是需要清理锁状态的。

pthread_atfork

1
2
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void) );

pthread_atfork会安装三个清理函数,prepare在父进程fork之前调用,任务是获取父进程定义的所有锁,parent是在子进程创建后尚未返回父进程前,在父进程上下文中解锁所有prepare中的锁,child则是在子进程的上下文中间释放prepare中获得的所有锁。

注意事项

  1. 递归互斥不能清理,因为不知道加锁的次数
  2. 如果子进程只允许调用一步信号安全的函数,child的处理程序就不可能清理同步对象,因为他们都不是异步信号安全的。

线程I/O

由于lseek和read、write不是原子操作,在多线程环境中可能会出现问题,所以要使用pread和pwrite。