本文来自个人博客:https://dunkwan.cn
重入
如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数为线程安全的。以下是不能保证线程安全的函数。
下图则是一些非线程安全函数的线程安全的替代版。
一个函数时线程安全的并不代表它就是对信号处理程序是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。
flockfile
和ftrylockfile
函数用于获取给定的FILE
对象关联的锁。该锁是递归的:当锁被占有时,还可以再次获取该锁,而且不会造成死锁。所有操作FILE
对象的标准I/O例程的动作行为必须看起来就像是它们内部调用了flockfile
和funlockfile
。
#include <stdio.h>
int ftrylockfile(FILE *fp);
返回值:若成功,返回0;若不能获取锁,返回非0数值。
void flockfile(FILE *fp);
void funlockfile(FILE *fp);
如果标准I/O例程都获取各自的锁,那么在做一次一个字符的I/O时就会出现严重的性能下降。在这种情况下,需要对每个字符的读写操作进行获取锁和释放锁动作。为了避免这种开销,出现了不加锁版本的基于字符的标准I/O例程。
#include <stdio.h>
int getchar_unlocked(void);
int getc_unlocked(FILE *fp);
两个函数的返回值:若成功,返回下一个字符;若遇到文件尾或者出错,返回EOF。
int putchar_unlocked(int c);
int putc_unlocked(int c, FILE *fp);
两个函数的返回值:若成功,返回c;若出错,返回EOF。
线程特定数据
线程特定数据也被称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。采用线程私有数据的原因如下:
- 有时候需要维护线程私有数据。
- 线程私有数据提供了让基于进程的接口适应多线程环境的机制。
在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问。使用pthread_key_create
创建一个键。
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));
返回值:若成功,返回0;否则,返回错误编号。
在
pthread_key_create
创建键的时候可以为键关联一个析构函数,即destructor
参数的值。当线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。如果传入的析构函数为空,就表明没有析构函数与这个键关联。当线程调用pthrea_exit
或者线程执行返回,正常退出时,析构函数就会被调用。同样,线程取消时,只有在最后的清理处理程序返回之后,析构函数才会被调用。如果线程调用了exit
、_exit
、_Exit
或abort
,或者出现其他非正常的退出时,就不会调用析构函数。
线程通常使用malloc
为线程特定数据分配内存。析构函数通常释放已分配的内存。如果线程在没有释放内存之前就退出了,那么这块内存就会丢失,即线程所属进程就出现了内存泄漏。
对所有线程,我们可以通过调用pthread_key_delete
来取消与线程特定数据值之间的关联关系。
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
返回值:若成功,返回0;否则,返回错误编号。
有些线程可能看到一个键值,而其他线程可能看到另一个不同的键值。这取决于系统是如何调度线程的,解决这种竞争的办法是使用pthread_once
。
#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
返回值:若成功,返回0;否则,返回错误编号。
initflag
必须是一个非本地的变量(如全局变量或静态变量),而且必须初始化为PTHREAD_ONCE_INIT
。
键一旦创建后,就可以通过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_attr_t
结构中,它们是可取消状态和可取消类型。这两个属性影响着线程在响应pthread_cancel
函数调用时所做出的的行为。
可取消状态属性
可取消状态属性可为PTHREAD_CANCEL_ENABLE
和PTHREAD_CANCEL_DISABLE
。线程可通过调用pthread_setcancelstate
修改它的可取消状态。
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
返回值:若成功,返回0;否则,返回错误编号。
在默认情况下,线程在取消请求发出以后还是继续运行,直到线程达到某个取消点。取消点:线程检查它是否被曲线的一个位置,如果取消了,则按照请求行事。
下图是POSIX.1中保证在被线程调用时,取消点都会出现的函数。
同时POSIX.1还指定了一些函数作为可选的取消点。
如果应用程序很长时间不能调用会出现取消点的函数和可选取消点的函数时,可调用pthread_testcancel
函数来在程序中添加自己的取消点。
#include <pthread.h>
void pthread_testcancel(void);
可取消类型属性
可取消类型属性可为PTHREAD_CANCEL_DEFERRED
,也可为PTHREAD_CANCEL_ASYNCHRONOUS
。
pthread_setcanceltype
用来修改取消类型。
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
返回值:若成功,返回0;否则,返回错误编号。
推迟取消是在线程到达取消点之前,并不会出现真正的取消。异步取消与推迟取消不同,因为使用异步取消时,线程可在任意时间撤消,不是非得遇到取消点才能被取消。
线程和信号
sigprocmask
函数描述了在进程中阻止信号的发送,而pthread_sigmask
则描述了在多线程进程中的阻止信号发送的行为。
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
返回值:若成功,返回0;否则,返回错误编号。
sigwait
函数用于等待一个或多个信号的出现。
#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict signop);
返回值:若成功,返回0;否则,返回错误编号。
在进程中,调用kill
函数可以把信号发送给进程,而在线程中,则可以调用pthread_kill
来发送信号。
#include <signal.h>
int pthread_kill(pthread_t thread, int signo);
返回值:若成功,返回0;否则,返回错误编号。
测试示例:
等待信号处理程序设置标志表明主程序应该退出。
#include "../../include/apue.h"
#include <pthread.h>
int quitflag;
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);
quitflag = 0;
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
exit(0);
}
结果如下:
线程和fork
在多线程中,为了避免不一致状态的问题,POSIX.1声明,在fork
返回和子进程调用其中一个exec
函数之间,子进程只能调用异步信号安全的函数。这就限制了在调用exec
之前子进程能做什么,但不涉及子进程中锁的状态的问题。
要清除锁状态,可以通过pthread_atfork
函数建立fork
处理程序。
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
函数返回值:若成功,返回0;否则,返回错误编号。
线程和I/O
pread
和pwrite
函数是将对文件的读写操作和偏移量的设定操作合并的原子操作。从而避免并发读写的造成的问题。
#include <unistd.h>
ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
返回值:若成功,返回读取或写入的字节数;否则,返回-1并设置errno。