第12章 线程控制
12.5 重入
可重入函数
中断一个可重入函数的执行,转而执行另外一个函数(一般为信号处理程序,注意此时依然为
同一个线程
),
返回可重入函数执行不会出现错误。可重入与异步信号安全等价(APUE 3 edition, 10.6 )
可重入函数除了使用自己栈上的变量以外不依赖于任何环境(包括 static),这样的函数就是可重入的,
可以允许有该函数的多个副本同时运行
线程安全:
如果一个函数在相同的时间点可以被
多个线程
安全地调用,就称该函数是线程安全的。1) 若一个函数如同可重入函数一样,也没有使用静态数据(全局变量或static局部变量),那么该函数也是线程安全的。
2) 或使用了静态数据,但采用了同步机制,保证线程的安全调用,那么也是线程安全的。
可重入与线程安全的关系:
-------------------------------[Figure 1]--------------------------------
举例说明:func是线程安全的,但不是可重入的:
1)线程安全是显然的,第b步在任意时刻只有一个线程访问。
2)假设进程执行完第a步,接收到一个信号,转而执行信号处理程序,恰巧该信号处理程序中也调用了函数func,那么就陷入了死锁。
3) 当然可以利用递归锁,那么func也是可重入的,但是递归锁的使用可能会带来其他严重问题
-------------------------------[Figure 2]--------------------------------
支持线程安全函数的操作系统实现会在
它会提供可替代的线程安全版本。下面列出这些函数的线程安全版本。
POSIX.1还提供了以线程安全的方式管理FILE对象的方法。可以使用flockfile和ftrylockfile获取给定FILE对象关联的锁。
这个锁是递归的:当你占有这把锁的时候,还是可以再次获取该锁,而且不会导致死锁。
int ftrylockfile(FILE *fp);
void flockfile(FILE *fp);
void funlockfile(FILE *fp);
12.6 线程特定数据(TSD)
使用线程特定数据理由:
需要基于线程来维护一些数据(比如,线程id)
希望能够提供一种机制可以在多线程的环境下使用基于进程的接口。(比如,errno)
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
void* pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);
int pthread_key_delete(pthread_key_t *key);
注意:取消键与当前线程TSD的关联关系(键与其他线程TSD的关联关系不变),但并不会激活与键关联的析构函数,
需要应用程序自己利用free函数
使用TSD过程:
1. 启动一个进程并创建了若干线程(A,B),其中一个线程(比如线程A),要申请线程私有数据,系统调用
pthread_key_creat在下图所示的key结构数组中找到第一个未用的元素,并把它的键(0)返回给调用者
2. 线程a通过pthrea_getspecific调用获得线程a的pkey[0]值,返回的是一个空指针NULL,
3. 用malloc在堆里分配一段空间,使用pthread_setspecific调用将pkey[0]指向刚才分配的内存区域。
4. 若线程b若使用相同的键,线程B只需要重复第三步。最终结果如下Figure 3所示。
------------------------------------------[Figure 3]----------------------------------------
需要确保分配的键并不是由于在初始化阶段的竞争而发生变化:
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
struct tsd { pthread_t tid; };
pthread_key_t key;
pthread_once_t create_done = PTHREAD_ONCE_INIT;
void destructor(void* arg){
struct tsd* tsdptr = (struct tsd*)arg;
free(tsdptr);
}
void key_create(void){
pthread_key_create(&key, destructor);
}
void key_init(){
struct tsd *tsdptr = (struct tsd*)malloc(sizeof(struct tsd));
tsdptr->tid = pthread_self();
pthread_setspecific(key, tsdptr);
}
void* p_fun(void* arg){
pthread_once(&create_done, key_create);
key_init();
return (void*)0;
}
12.8 线程和信号
1. 每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的.
2. 进程中的信号是传递到单个线程的,线程中必须使用pthread_sigmask设置信号屏蔽字.
3. 如果一个信号与硬件故障相关,该信号一般被发送到引起该事件的线程中,否则信号被发送到任意一个线程
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
int pthread_kill(pthread_t thread, int sig);
[pthread_xxx的函数一般返回错误码,而不是设置errno]
注意,用sigprocmask修改的是进程(主线程)的信号屏障字,而不是本线程的信号屏蔽字
int sigwait(const sigset_t * restrict set, int * restrict signop);
signop为当前接受到的信号[APUE 3 edition 中文版的解释错误]
1. set为等待的信号或信号集
2. 若信号集中的某些信号在sigwait调用时处于挂起状态,那么sigwait将无阻塞地返回,且移除这些挂起信号,若为排队信号,一次移除一个
3. 线程在调用sigwait之前,必须阻塞等待的信号(避免出现窗口期)。sigwait函数会原子地取消信号集的阻塞状态,直到信号到来
4. 任意信号都可以唤醒调用sigwait的线程(和进程的sigsuspend很不同)
5. 使用sigwait的好处在于它可以简化信号处理,利用一个或多个线程专门处理信号,从而将异步产生的信号以同步方式处理
---5.1 假设专用线程A处理SIGINT,SIGQUIT信号, 在创建线程A之前,主线程首先屏蔽这两个信号
---5.2 线程A创建之后,继承主线程的信号屏蔽字,线程A调用sigwait函数
---5.3 sigwait取消对SIGINT与SIGQUIT的屏蔽,接着线程A阻塞,等待任意信号的到来
---5.4 当一个信号到来时,唤醒线程A,在线程A的正常的上下文中调用信号的处理程序
6. 若多个线程在sigwait的调用中因为同一个信号而阻塞,那么在信号传递的时候,就只有一个线程从中返回
7. 若一个信号被捕获(使用sigaction建立了一个信号处理程序),而一个线程正在sigwait调用中等待同一个信号,那么不同操作系统的处理方式不同,所以尽量避免
void* p_fun(void* arg){
sigset_t sigs;
sigemptyset(&sigs);
sigaddset(&sigs, (int)arg);
int sig;
for(;;){
if (sigwait(&sigs, &sig) != 0){
fprintf(stderr, "sigwait err");
return (void*)-1;
}
if (sig == SIGINT){
printf("tid : %ld receive SIGINT.\n", pthread_self());
}
if (sig == SIGTSTP){
printf("tid : %ld receive SIGTSTP.\n", pthread_self());
}
}
return (void*)0;
}
int main(){
sigset_t sigs;
sigemptyset(&sigs);
sigaddset(&sigs, SIGINT);
sigaddset(&sigs, SIGTSTP);
sigprocmask(SIG_SETMASK, &sigs, NULL);
pthread_t tid[2];
if (pthread_create(&tid[0], NULL, p_fun, (void*)SIGINT) != 0){
fprintf(stderr, "pthread_create err");
exit(-1);
}
if (pthread_create(&tid[1], NULL, p_fun, (void*)SIGTSTP) != 0){
fprintf(stderr, "pthread_create err");
exit(-1);
}
for(;;){
/*main proccess*/
}
exit(0);
}
int pthread_kill(pthread_t tid, int signo);
可以传一个0值来检查线程tid是否存在,类似kill检查进程的存在性
闹钟定时器是进程资源,所有线程共享相同的闹钟
12.9 线程和fork
在子进程内部,只存在一个线程,如下图所示,子进程中只有线程T3,因为在父进程中由线程T3创建了子进程。
所以互斥量M1,M2在子进程中就无法解锁。
``
不一致解决方法:
1. 如果子进程从fork返回以后马上调用其中一个exec函数,可以避免这种不一致,在fork返回和子进程调用其中一个exec函数之间,
子进程只能调用异步信号安全的函数
2. 调用pthread_atfork函数建立fork处理程序
int pthread_atfork()(void (*prepare)(void),void (*parent)(void),void (*child)(void));
------2.1 prepare由父进程在fork创建子进程前调用,prepare的任务是获取父进程定义的所有锁
------2.2 parent是在fork创建了子进程以后,在fork返回之前在父进程环境中调用的,parent的任务是对prepare获得的锁进行解锁
------2.3 child在fork返回之前在子进程环境中调用,与parent一样,child必须释放prepare处理程序获得的所有锁
------2.4 可以多次调用pthread_atfork从而设置多套fork处理程序。parent和child是以他们注册时的顺序进行调用,
----------prepare的调用顺序与他们注册时的顺序相反,比如若模块A调用模块B,每个模块都有自己的一套锁。
----------那么模块B必须在模块A之前设置它的pthread_atfork函数